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 | #include <private/legendlayout_p.h> |
31 | #include <private/chartpresenter_p.h> |
32 | #include <private/qlegend_p.h> |
33 | #include <private/abstractchartlayout_p.h> |
34 | |
35 | #include <private/qlegendmarker_p.h> |
36 | #include <private/legendmarkeritem_p.h> |
37 | #include <QtCharts/QLegendMarker> |
38 | |
39 | QT_CHARTS_BEGIN_NAMESPACE |
40 | |
41 | LegendLayout::LegendLayout(QLegend *legend) |
42 | : m_legend(legend), |
43 | m_offsetX(0), |
44 | m_offsetY(0) |
45 | { |
46 | |
47 | } |
48 | |
49 | LegendLayout::~LegendLayout() |
50 | { |
51 | |
52 | } |
53 | |
54 | void LegendLayout::setOffset(qreal x, qreal y) |
55 | { |
56 | bool scrollHorizontal = true; |
57 | switch (m_legend->alignment()) { |
58 | case Qt::AlignTop: |
59 | case Qt::AlignBottom: |
60 | scrollHorizontal = true; |
61 | break; |
62 | case Qt::AlignLeft: |
63 | case Qt::AlignRight: |
64 | scrollHorizontal = false; |
65 | break; |
66 | } |
67 | |
68 | // If detached, the scrolling direction is vertical instead of horizontal and vice versa. |
69 | if (!m_legend->isAttachedToChart()) |
70 | scrollHorizontal = !scrollHorizontal; |
71 | |
72 | QRectF boundingRect = geometry(); |
73 | qreal left, top, right, bottom; |
74 | getContentsMargins(left: &left, top: &top, right: &right, bottom: &bottom); |
75 | boundingRect.adjust(xp1: left, yp1: top, xp2: -right, yp2: -bottom); |
76 | |
77 | // Limit offset between m_minOffset and m_maxOffset |
78 | if (scrollHorizontal) { |
79 | if (m_width <= boundingRect.width()) |
80 | return; |
81 | |
82 | if (x != m_offsetX) { |
83 | m_offsetX = qBound(min: m_minOffsetX, val: x, max: m_maxOffsetX); |
84 | m_legend->d_ptr->items()->setPos(ax: -m_offsetX, ay: boundingRect.top()); |
85 | } |
86 | } else { |
87 | if (m_height <= boundingRect.height()) |
88 | return; |
89 | |
90 | if (y != m_offsetY) { |
91 | m_offsetY = qBound(min: m_minOffsetY, val: y, max: m_maxOffsetY); |
92 | m_legend->d_ptr->items()->setPos(ax: boundingRect.left(), ay: -m_offsetY); |
93 | } |
94 | } |
95 | } |
96 | |
97 | QPointF LegendLayout::offset() const |
98 | { |
99 | return QPointF(m_offsetX, m_offsetY); |
100 | } |
101 | |
102 | void LegendLayout::invalidate() |
103 | { |
104 | QGraphicsLayout::invalidate(); |
105 | if (m_legend->isAttachedToChart()) |
106 | m_legend->d_ptr->m_presenter->layout()->invalidate(); |
107 | } |
108 | |
109 | void LegendLayout::setGeometry(const QRectF &rect) |
110 | { |
111 | m_legend->d_ptr->items()->setVisible(m_legend->isVisible()); |
112 | |
113 | QGraphicsLayout::setGeometry(rect); |
114 | |
115 | if (m_legend->isAttachedToChart()) |
116 | setAttachedGeometry(rect); |
117 | else |
118 | setDettachedGeometry(rect); |
119 | } |
120 | |
121 | void LegendLayout::setAttachedGeometry(const QRectF &rect) |
122 | { |
123 | if (!rect.isValid()) |
124 | return; |
125 | |
126 | qreal oldOffsetX = m_offsetX; |
127 | qreal oldOffsetY = m_offsetY; |
128 | m_offsetX = 0; |
129 | m_offsetY = 0; |
130 | |
131 | QSizeF size(0, 0); |
132 | |
133 | if (m_legend->d_ptr->markers().isEmpty()) { |
134 | return; |
135 | } |
136 | |
137 | m_width = 0; |
138 | m_height = 0; |
139 | |
140 | qreal left, top, right, bottom; |
141 | getContentsMargins(left: &left, top: &top, right: &right, bottom: &bottom); |
142 | |
143 | QRectF geometry = rect.adjusted(xp1: left, yp1: top, xp2: -right, yp2: -bottom); |
144 | |
145 | switch(m_legend->alignment()) { |
146 | case Qt::AlignTop: |
147 | case Qt::AlignBottom: { |
148 | // Calculate the space required for items and add them to a sorted list. |
149 | qreal markerItemsWidth = 0; |
150 | qreal itemMargins = 0; |
151 | QList<LegendWidthStruct *> legendWidthList; |
152 | foreach (QLegendMarker *marker, m_legend->d_ptr->markers()) { |
153 | LegendMarkerItem *item = marker->d_ptr->item(); |
154 | if (item->isVisible()) { |
155 | QSizeF dummySize; |
156 | qreal itemWidth = item->sizeHint(which: Qt::PreferredSize, constraint: dummySize).width(); |
157 | LegendWidthStruct *structItem = new LegendWidthStruct; |
158 | structItem->item = item; |
159 | structItem->width = itemWidth; |
160 | legendWidthList.append(t: structItem); |
161 | markerItemsWidth += itemWidth; |
162 | itemMargins += marker->d_ptr->item()->m_margin; |
163 | } |
164 | } |
165 | std::sort(first: legendWidthList.begin(), last: legendWidthList.end(), comp: widthLongerThan); |
166 | |
167 | // If the items would occupy more space than is available, start truncating them |
168 | // from the longest one. |
169 | qreal availableGeometry = geometry.width() - right - left * 2 - itemMargins; |
170 | if (markerItemsWidth >= availableGeometry && legendWidthList.count() > 0) { |
171 | bool truncated(false); |
172 | int count = legendWidthList.count(); |
173 | for (int i = 1; i < count; i++) { |
174 | int truncateIndex = i - 1; |
175 | |
176 | while (legendWidthList.at(i: truncateIndex)->width >= legendWidthList.at(i)->width |
177 | && !truncated) { |
178 | legendWidthList.at(i: truncateIndex)->width--; |
179 | markerItemsWidth--; |
180 | if (i > 1) { |
181 | // Truncate the items that are before the truncated one in the list. |
182 | for (int j = truncateIndex - 1; j >= 0; j--) { |
183 | if (legendWidthList.at(i: truncateIndex)->width |
184 | < legendWidthList.at(i: j)->width) { |
185 | legendWidthList.at(i: j)->width--; |
186 | markerItemsWidth--; |
187 | } |
188 | } |
189 | } |
190 | if (markerItemsWidth < availableGeometry) |
191 | truncated = true; |
192 | } |
193 | // Truncate the last item if needed. |
194 | if (i == count - 1) { |
195 | if (legendWidthList.at(i: count - 1)->width |
196 | > legendWidthList.at(i: truncateIndex)->width) { |
197 | legendWidthList.at(i: count - 1)->width--; |
198 | markerItemsWidth--; |
199 | } |
200 | } |
201 | |
202 | if (truncated) |
203 | break; |
204 | } |
205 | // Items are of same width and all of them need to be truncated |
206 | // or there is just one item that is truncated. |
207 | while (markerItemsWidth >= availableGeometry) { |
208 | for (int i = 0; i < count; i++) { |
209 | legendWidthList.at(i)->width--; |
210 | markerItemsWidth--; |
211 | } |
212 | } |
213 | } |
214 | |
215 | QPointF point(0,0); |
216 | |
217 | int markerCount = m_legend->d_ptr->markers().count(); |
218 | for (int i = 0; i < markerCount; i++) { |
219 | QLegendMarker *marker; |
220 | if (m_legend->d_ptr->m_reverseMarkers) |
221 | marker = m_legend->d_ptr->markers().at(i: markerCount - 1 - i); |
222 | else |
223 | marker = m_legend->d_ptr->markers().at(i); |
224 | LegendMarkerItem *item = marker->d_ptr->item(); |
225 | if (item->isVisible()) { |
226 | QRectF itemRect = geometry; |
227 | qreal availableWidth = 0; |
228 | for (int i = 0; i < legendWidthList.size(); ++i) { |
229 | if (legendWidthList.at(i)->item == item) { |
230 | availableWidth = legendWidthList.at(i)->width; |
231 | break; |
232 | } |
233 | } |
234 | itemRect.setWidth(availableWidth); |
235 | item->setGeometry(itemRect); |
236 | item->setPos(ax: point.x(),ay: geometry.height()/2 - item->boundingRect().height()/2); |
237 | const QRectF &rect = item->boundingRect(); |
238 | size = size.expandedTo(otherSize: rect.size()); |
239 | qreal w = rect.width(); |
240 | m_width = m_width + w - item->m_margin; |
241 | point.setX(point.x() + w); |
242 | } |
243 | } |
244 | // Delete structs from the container |
245 | qDeleteAll(c: legendWidthList); |
246 | |
247 | // Round to full pixel via QPoint to avoid one pixel clipping on the edge in some cases |
248 | if (m_width < geometry.width()) { |
249 | m_legend->d_ptr->items()->setPos(QPoint(geometry.width() / 2 - m_width / 2, |
250 | geometry.top())); |
251 | } else { |
252 | m_legend->d_ptr->items()->setPos(geometry.topLeft().toPoint()); |
253 | } |
254 | m_height = size.height(); |
255 | } |
256 | break; |
257 | case Qt::AlignLeft: |
258 | case Qt::AlignRight: { |
259 | QPointF point(0,0); |
260 | int markerCount = m_legend->d_ptr->markers().count(); |
261 | for (int i = 0; i < markerCount; i++) { |
262 | QLegendMarker *marker; |
263 | if (m_legend->d_ptr->m_reverseMarkers) |
264 | marker = m_legend->d_ptr->markers().at(i: markerCount - 1 - i); |
265 | else |
266 | marker = m_legend->d_ptr->markers().at(i); |
267 | LegendMarkerItem *item = marker->d_ptr->item(); |
268 | if (item->isVisible()) { |
269 | item->setGeometry(geometry); |
270 | item->setPos(point); |
271 | const QRectF &rect = item->boundingRect(); |
272 | qreal h = rect.height(); |
273 | size = size.expandedTo(otherSize: rect.size()); |
274 | m_height+=h; |
275 | point.setY(point.y() + h); |
276 | } |
277 | } |
278 | |
279 | // Round to full pixel via QPoint to avoid one pixel clipping on the edge in some cases |
280 | if (m_height < geometry.height()) { |
281 | m_legend->d_ptr->items()->setPos(QPoint(geometry.left(), |
282 | geometry.height() / 2 - m_height / 2)); |
283 | } else { |
284 | m_legend->d_ptr->items()->setPos(geometry.topLeft().toPoint()); |
285 | } |
286 | m_width = size.width(); |
287 | break; |
288 | } |
289 | } |
290 | |
291 | m_minOffsetX = -left; |
292 | m_minOffsetY = - top; |
293 | m_maxOffsetX = m_width - geometry.width() - right; |
294 | m_maxOffsetY = m_height - geometry.height() - bottom; |
295 | |
296 | setOffset(x: oldOffsetX, y: oldOffsetY); |
297 | } |
298 | |
299 | void LegendLayout::setDettachedGeometry(const QRectF &rect) |
300 | { |
301 | if (!rect.isValid()) |
302 | return; |
303 | |
304 | // Detached layout is different. |
305 | // In detached mode legend may have multiple rows and columns, so layout calculations |
306 | // differ a log from attached mode. |
307 | // Also the scrolling logic is bit different. |
308 | |
309 | qreal oldOffsetX = m_offsetX; |
310 | qreal oldOffsetY = m_offsetY; |
311 | m_offsetX = 0; |
312 | m_offsetY = 0; |
313 | |
314 | qreal left, top, right, bottom; |
315 | getContentsMargins(left: &left, top: &top, right: &right, bottom: &bottom); |
316 | QRectF geometry = rect.adjusted(xp1: left, yp1: top, xp2: -right, yp2: -bottom); |
317 | |
318 | QList<QLegendMarker *> markers = m_legend->d_ptr->markers(); |
319 | |
320 | if (markers.isEmpty()) |
321 | return; |
322 | |
323 | switch (m_legend->alignment()) { |
324 | case Qt::AlignTop: { |
325 | QPointF point(0, 0); |
326 | m_width = 0; |
327 | m_height = 0; |
328 | for (int i = 0; i < markers.count(); i++) { |
329 | LegendMarkerItem *item = markers.at(i)->d_ptr->item(); |
330 | if (item->isVisible()) { |
331 | item->setGeometry(geometry); |
332 | item->setPos(ax: point.x(),ay: point.y()); |
333 | const QRectF &boundingRect = item->boundingRect(); |
334 | qreal w = boundingRect.width(); |
335 | qreal h = boundingRect.height(); |
336 | m_width = qMax(a: m_width,b: w); |
337 | m_height = qMax(a: m_height,b: h); |
338 | point.setX(point.x() + w); |
339 | if (point.x() + w > geometry.left() + geometry.width() - right) { |
340 | // Next item would go off rect. |
341 | point.setX(0); |
342 | point.setY(point.y() + h); |
343 | if (i+1 < markers.count()) { |
344 | m_height += h; |
345 | } |
346 | } |
347 | } |
348 | } |
349 | m_legend->d_ptr->items()->setPos(geometry.topLeft()); |
350 | |
351 | m_minOffsetX = -left; |
352 | m_minOffsetY = -top; |
353 | m_maxOffsetX = m_width - geometry.width() - right; |
354 | m_maxOffsetY = m_height - geometry.height() - bottom; |
355 | } |
356 | break; |
357 | case Qt::AlignBottom: { |
358 | QPointF point(0, geometry.height()); |
359 | m_width = 0; |
360 | m_height = 0; |
361 | for (int i = 0; i < markers.count(); i++) { |
362 | LegendMarkerItem *item = markers.at(i)->d_ptr->item(); |
363 | if (item->isVisible()) { |
364 | item->setGeometry(geometry); |
365 | const QRectF &boundingRect = item->boundingRect(); |
366 | qreal w = boundingRect.width(); |
367 | qreal h = boundingRect.height(); |
368 | m_width = qMax(a: m_width,b: w); |
369 | m_height = qMax(a: m_height,b: h); |
370 | item->setPos(ax: point.x(),ay: point.y() - h); |
371 | point.setX(point.x() + w); |
372 | if (point.x() + w > geometry.left() + geometry.width() - right) { |
373 | // Next item would go off rect. |
374 | point.setX(0); |
375 | point.setY(point.y() - h); |
376 | if (i+1 < markers.count()) { |
377 | m_height += h; |
378 | } |
379 | } |
380 | } |
381 | } |
382 | m_legend->d_ptr->items()->setPos(geometry.topLeft()); |
383 | |
384 | m_minOffsetX = -left; |
385 | m_minOffsetY = -m_height + geometry.height() - top; |
386 | m_maxOffsetX = m_width - geometry.width() - right; |
387 | m_maxOffsetY = -bottom; |
388 | } |
389 | break; |
390 | case Qt::AlignLeft: { |
391 | QPointF point(0, 0); |
392 | m_width = 0; |
393 | m_height = 0; |
394 | qreal maxWidth = 0; |
395 | for (int i = 0; i < markers.count(); i++) { |
396 | LegendMarkerItem *item = markers.at(i)->d_ptr->item(); |
397 | if (item->isVisible()) { |
398 | item->setGeometry(geometry); |
399 | const QRectF &boundingRect = item->boundingRect(); |
400 | qreal w = boundingRect.width(); |
401 | qreal h = boundingRect.height(); |
402 | m_height = qMax(a: m_height,b: h); |
403 | maxWidth = qMax(a: maxWidth,b: w); |
404 | item->setPos(ax: point.x(),ay: point.y()); |
405 | point.setY(point.y() + h); |
406 | if (point.y() + h > geometry.bottom() - bottom) { |
407 | // Next item would go off rect. |
408 | point.setX(point.x() + maxWidth); |
409 | point.setY(0); |
410 | if (i+1 < markers.count()) { |
411 | m_width += maxWidth; |
412 | maxWidth = 0; |
413 | } |
414 | } |
415 | } |
416 | } |
417 | m_width += maxWidth; |
418 | m_legend->d_ptr->items()->setPos(geometry.topLeft()); |
419 | |
420 | m_minOffsetX = -left; |
421 | m_minOffsetY = -top; |
422 | m_maxOffsetX = m_width - geometry.width() - right; |
423 | m_maxOffsetY = m_height - geometry.height() - bottom; |
424 | } |
425 | break; |
426 | case Qt::AlignRight: { |
427 | QPointF point(geometry.width(), 0); |
428 | m_width = 0; |
429 | m_height = 0; |
430 | qreal maxWidth = 0; |
431 | for (int i = 0; i < markers.count(); i++) { |
432 | LegendMarkerItem *item = markers.at(i)->d_ptr->item(); |
433 | if (item->isVisible()) { |
434 | item->setGeometry(geometry); |
435 | const QRectF &boundingRect = item->boundingRect(); |
436 | qreal w = boundingRect.width(); |
437 | qreal h = boundingRect.height(); |
438 | m_height = qMax(a: m_height,b: h); |
439 | maxWidth = qMax(a: maxWidth,b: w); |
440 | item->setPos(ax: point.x() - w,ay: point.y()); |
441 | point.setY(point.y() + h); |
442 | if (point.y() + h > geometry.bottom()-bottom) { |
443 | // Next item would go off rect. |
444 | point.setX(point.x() - maxWidth); |
445 | point.setY(0); |
446 | if (i+1 < markers.count()) { |
447 | m_width += maxWidth; |
448 | maxWidth = 0; |
449 | } |
450 | } |
451 | } |
452 | } |
453 | m_width += maxWidth; |
454 | m_legend->d_ptr->items()->setPos(geometry.topLeft()); |
455 | |
456 | m_minOffsetX = - m_width + geometry.width() - left; |
457 | m_minOffsetY = -top; |
458 | m_maxOffsetX = - right; |
459 | m_maxOffsetY = m_height - geometry.height() - bottom; |
460 | } |
461 | break; |
462 | default: |
463 | break; |
464 | } |
465 | |
466 | setOffset(x: oldOffsetX, y: oldOffsetY); |
467 | } |
468 | |
469 | QSizeF LegendLayout::sizeHint(Qt::SizeHint which, const QSizeF &constraint) const |
470 | { |
471 | QSizeF size(0, 0); |
472 | qreal left, top, right, bottom; |
473 | getContentsMargins(left: &left, top: &top, right: &right, bottom: &bottom); |
474 | |
475 | if(constraint.isValid()) { |
476 | foreach(QLegendMarker *marker, m_legend->d_ptr->markers()) { |
477 | LegendMarkerItem *item = marker->d_ptr->item(); |
478 | size = size.expandedTo(otherSize: item->effectiveSizeHint(which)); |
479 | } |
480 | size = size.boundedTo(otherSize: constraint); |
481 | } |
482 | else if (constraint.width() >= 0) { |
483 | qreal width = 0; |
484 | qreal height = 0; |
485 | foreach(QLegendMarker *marker, m_legend->d_ptr->markers()) { |
486 | LegendMarkerItem *item = marker->d_ptr->item(); |
487 | width+=item->effectiveSizeHint(which).width(); |
488 | height=qMax(a: height,b: item->effectiveSizeHint(which).height()); |
489 | } |
490 | |
491 | size = QSizeF(qMin(a: constraint.width(),b: width), height); |
492 | } |
493 | else if (constraint.height() >= 0) { |
494 | qreal width = 0; |
495 | qreal height = 0; |
496 | foreach(QLegendMarker *marker, m_legend->d_ptr->markers()) { |
497 | LegendMarkerItem *item = marker->d_ptr->item(); |
498 | width=qMax(a: width,b: item->effectiveSizeHint(which).width()); |
499 | height+=height,item->effectiveSizeHint(which).height(); |
500 | } |
501 | size = QSizeF(width,qMin(a: constraint.height(),b: height)); |
502 | } |
503 | else { |
504 | foreach(QLegendMarker *marker, m_legend->d_ptr->markers()) { |
505 | LegendMarkerItem *item = marker->d_ptr->item(); |
506 | size = size.expandedTo(otherSize: item->effectiveSizeHint(which)); |
507 | } |
508 | } |
509 | size += QSize(left + right, top + bottom); |
510 | return size; |
511 | } |
512 | |
513 | bool LegendLayout::widthLongerThan(const LegendWidthStruct *item1, |
514 | const LegendWidthStruct *item2) |
515 | { |
516 | return item1->width > item2->width; |
517 | } |
518 | |
519 | QT_CHARTS_END_NAMESPACE |
520 | |