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