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 | foreach (QLegendMarker *marker, m_legend->d_ptr->markers()) { |
127 | LegendMarkerItem *item = marker->d_ptr->item(); |
128 | if (item->isVisible()) { |
129 | QSizeF dummySize; |
130 | qreal itemWidth = item->sizeHint(which: Qt::PreferredSize, constraint: dummySize).width(); |
131 | LegendWidthStruct *structItem = new LegendWidthStruct; |
132 | structItem->item = item; |
133 | structItem->width = itemWidth; |
134 | legendWidthList.append(t: structItem); |
135 | markerItemsWidth += itemWidth; |
136 | itemMargins += marker->d_ptr->item()->m_margin; |
137 | } |
138 | } |
139 | std::sort(first: legendWidthList.begin(), last: legendWidthList.end(), comp: widthLongerThan); |
140 | |
141 | // If the items would occupy more space than is available, start truncating them |
142 | // from the longest one. |
143 | qreal availableGeometry = geometry.width() - itemMargins; |
144 | if (markerItemsWidth >= availableGeometry && legendWidthList.size() > 0) { |
145 | bool truncated(false); |
146 | int count = legendWidthList.size(); |
147 | for (int i = 1; i < count; i++) { |
148 | int truncateIndex = i - 1; |
149 | |
150 | while (legendWidthList.at(i: truncateIndex)->width >= legendWidthList.at(i)->width |
151 | && !truncated) { |
152 | legendWidthList.at(i: truncateIndex)->width--; |
153 | markerItemsWidth--; |
154 | if (i > 1) { |
155 | // Truncate the items that are before the truncated one in the list. |
156 | for (int j = truncateIndex - 1; j >= 0; j--) { |
157 | if (legendWidthList.at(i: truncateIndex)->width |
158 | < legendWidthList.at(i: j)->width) { |
159 | legendWidthList.at(i: j)->width--; |
160 | markerItemsWidth--; |
161 | } |
162 | } |
163 | } |
164 | if (markerItemsWidth < availableGeometry) |
165 | truncated = true; |
166 | } |
167 | // Truncate the last item if needed. |
168 | if (i == count - 1) { |
169 | if (legendWidthList.at(i: count - 1)->width |
170 | > legendWidthList.at(i: truncateIndex)->width) { |
171 | legendWidthList.at(i: count - 1)->width--; |
172 | markerItemsWidth--; |
173 | } |
174 | } |
175 | |
176 | if (truncated) |
177 | break; |
178 | } |
179 | // Items are of same width and all of them need to be truncated |
180 | // or there is just one item that is truncated. |
181 | while (markerItemsWidth >= availableGeometry) { |
182 | for (int i = 0; i < count; i++) { |
183 | legendWidthList.at(i)->width--; |
184 | markerItemsWidth--; |
185 | } |
186 | } |
187 | } |
188 | |
189 | QPointF point(0,0); |
190 | |
191 | int markerCount = m_legend->d_ptr->markers().size(); |
192 | for (int i = 0; i < markerCount; i++) { |
193 | QLegendMarker *marker; |
194 | if (m_legend->d_ptr->m_reverseMarkers) |
195 | marker = m_legend->d_ptr->markers().at(i: markerCount - 1 - i); |
196 | else |
197 | marker = m_legend->d_ptr->markers().at(i); |
198 | LegendMarkerItem *item = marker->d_ptr->item(); |
199 | if (item->isVisible()) { |
200 | QRectF itemRect = geometry; |
201 | qreal availableWidth = 0; |
202 | for (int i = 0; i < legendWidthList.size(); ++i) { |
203 | if (legendWidthList.at(i)->item == item) { |
204 | availableWidth = legendWidthList.at(i)->width; |
205 | break; |
206 | } |
207 | } |
208 | itemRect.setWidth(availableWidth); |
209 | item->setGeometry(itemRect); |
210 | item->setPos(ax: point.x(),ay: geometry.height()/2 - item->boundingRect().height()/2); |
211 | const QRectF &rect = item->boundingRect(); |
212 | size = size.expandedTo(otherSize: rect.size()); |
213 | qreal w = rect.width(); |
214 | m_width = m_width + w - item->m_margin; |
215 | point.setX(point.x() + w); |
216 | } |
217 | } |
218 | // Delete structs from the container |
219 | qDeleteAll(c: legendWidthList); |
220 | |
221 | // Round to full pixel via QPoint to avoid one pixel clipping on the edge in some cases |
222 | if (m_width < geometry.width()) { |
223 | m_legend->d_ptr->items()->setPos(QPoint(geometry.width() / 2 - m_width / 2, |
224 | geometry.top())); |
225 | } else { |
226 | m_legend->d_ptr->items()->setPos(geometry.topLeft().toPoint()); |
227 | } |
228 | m_height = size.height(); |
229 | } |
230 | break; |
231 | case Qt::AlignLeft: |
232 | case Qt::AlignRight: { |
233 | QPointF point(0,0); |
234 | int markerCount = m_legend->d_ptr->markers().size(); |
235 | for (int i = 0; i < markerCount; i++) { |
236 | QLegendMarker *marker; |
237 | if (m_legend->d_ptr->m_reverseMarkers) |
238 | marker = m_legend->d_ptr->markers().at(i: markerCount - 1 - i); |
239 | else |
240 | marker = m_legend->d_ptr->markers().at(i); |
241 | LegendMarkerItem *item = marker->d_ptr->item(); |
242 | if (item->isVisible()) { |
243 | item->setGeometry(geometry); |
244 | item->setPos(point); |
245 | const QRectF &rect = item->boundingRect(); |
246 | qreal h = rect.height(); |
247 | size = size.expandedTo(otherSize: rect.size()); |
248 | m_height+=h; |
249 | point.setY(point.y() + h); |
250 | } |
251 | } |
252 | |
253 | // Round to full pixel via QPoint to avoid one pixel clipping on the edge in some cases |
254 | if (m_height < geometry.height()) { |
255 | m_legend->d_ptr->items()->setPos(QPoint(geometry.left(), |
256 | geometry.height() / 2 - m_height / 2)); |
257 | } else { |
258 | m_legend->d_ptr->items()->setPos(geometry.topLeft().toPoint()); |
259 | } |
260 | m_width = size.width(); |
261 | break; |
262 | } |
263 | } |
264 | |
265 | m_minOffsetX = -left; |
266 | m_minOffsetY = - top; |
267 | m_maxOffsetX = m_width - geometry.width() - right; |
268 | m_maxOffsetY = m_height - geometry.height() - bottom; |
269 | |
270 | setOffset(x: oldOffsetX, y: oldOffsetY); |
271 | } |
272 | |
273 | void LegendLayout::setDettachedGeometry(const QRectF &rect) |
274 | { |
275 | if (!rect.isValid()) |
276 | return; |
277 | |
278 | // Detached layout is different. |
279 | // In detached mode legend may have multiple rows and columns, so layout calculations |
280 | // differ a log from attached mode. |
281 | // Also the scrolling logic is bit different. |
282 | |
283 | qreal oldOffsetX = m_offsetX; |
284 | qreal oldOffsetY = m_offsetY; |
285 | m_offsetX = 0; |
286 | m_offsetY = 0; |
287 | |
288 | qreal left, top, right, bottom; |
289 | getContentsMargins(left: &left, top: &top, right: &right, bottom: &bottom); |
290 | QRectF geometry = rect.adjusted(xp1: left, yp1: top, xp2: -right, yp2: -bottom); |
291 | |
292 | QList<QLegendMarker *> markers = m_legend->d_ptr->markers(); |
293 | |
294 | if (markers.isEmpty()) |
295 | return; |
296 | |
297 | Qt::Alignment align = m_legend->alignment(); |
298 | switch (align) { |
299 | case Qt::AlignTop: |
300 | case Qt::AlignBottom: { |
301 | QPointF point(0, (align == Qt::AlignTop) ? 0 : geometry.height()); |
302 | m_width = 0; |
303 | m_height = 0; |
304 | for (int i = 0; i < markers.size(); i++) { |
305 | LegendMarkerItem *item = markers.at(i)->d_ptr->item(); |
306 | if (item->isVisible()) { |
307 | item->setGeometry(geometry); |
308 | const QRectF &boundingRect = item->boundingRect(); |
309 | qreal w = boundingRect.width(); |
310 | qreal h = boundingRect.height(); |
311 | m_width = qMax(a: m_width,b: w); |
312 | m_height = qMax(a: m_height,b: h); |
313 | item->setPos(ax: point.x(),ay: point.y() - ((align == Qt::AlignTop) ? 0 : h)); |
314 | point.setX(point.x() + w); |
315 | if (point.x() + w > geometry.left() + geometry.width() - right) { |
316 | // Next item would go off rect. |
317 | point.setX(0); |
318 | point.setY(point.y() + ((align == Qt::AlignTop) ? h : -h)); |
319 | if (i+1 < markers.size()) { |
320 | m_height += h; |
321 | } |
322 | } |
323 | } |
324 | } |
325 | m_legend->d_ptr->items()->setPos(geometry.topLeft()); |
326 | |
327 | m_minOffsetX = -left; |
328 | m_maxOffsetX = m_width - geometry.width() - right; |
329 | if (align == Qt::AlignTop) { |
330 | m_minOffsetY = -top; |
331 | m_maxOffsetY = m_height - geometry.height() - bottom; |
332 | } else { |
333 | m_minOffsetY = -m_height + geometry.height() - top; |
334 | m_maxOffsetY = -bottom; |
335 | } |
336 | } |
337 | break; |
338 | case Qt::AlignLeft: |
339 | case Qt::AlignRight: { |
340 | QPointF point((align == Qt::AlignLeft) ? 0 : geometry.width(), 0); |
341 | m_width = 0; |
342 | m_height = 0; |
343 | qreal maxWidth = 0; |
344 | for (int i = 0; i < markers.size(); i++) { |
345 | LegendMarkerItem *item = markers.at(i)->d_ptr->item(); |
346 | if (item->isVisible()) { |
347 | item->setGeometry(geometry); |
348 | const QRectF &boundingRect = item->boundingRect(); |
349 | qreal w = boundingRect.width(); |
350 | qreal h = boundingRect.height(); |
351 | m_height = qMax(a: m_height,b: h); |
352 | maxWidth = qMax(a: maxWidth,b: w); |
353 | item->setPos(ax: point.x() - ((align == Qt::AlignLeft) ? 0 : w),ay: point.y()); |
354 | point.setY(point.y() + h); |
355 | if (point.y() + h > geometry.bottom()-bottom) { |
356 | // Next item would go off rect. |
357 | point.setX(point.x() + ((align == Qt::AlignLeft) ? maxWidth : -maxWidth)); |
358 | point.setY(0); |
359 | if (i+1 < markers.size()) { |
360 | m_width += maxWidth; |
361 | maxWidth = 0; |
362 | } |
363 | } |
364 | } |
365 | } |
366 | m_width += maxWidth; |
367 | m_legend->d_ptr->items()->setPos(geometry.topLeft()); |
368 | |
369 | m_minOffsetY = -top; |
370 | m_maxOffsetY = m_height - geometry.height() - bottom; |
371 | if (align == Qt::AlignLeft) { |
372 | m_minOffsetX = -left; |
373 | m_maxOffsetX = m_width - geometry.width() - right; |
374 | } else { |
375 | m_minOffsetX = - m_width + geometry.width() - left; |
376 | m_maxOffsetX = - right; |
377 | } |
378 | } |
379 | break; |
380 | default: |
381 | break; |
382 | } |
383 | |
384 | setOffset(x: oldOffsetX, y: oldOffsetY); |
385 | } |
386 | |
387 | QSizeF LegendLayout::sizeHint(Qt::SizeHint which, const QSizeF &constraint) const |
388 | { |
389 | QSizeF size(0, 0); |
390 | qreal left, top, right, bottom; |
391 | getContentsMargins(left: &left, top: &top, right: &right, bottom: &bottom); |
392 | |
393 | if(constraint.isValid()) { |
394 | foreach(QLegendMarker *marker, m_legend->d_ptr->markers()) { |
395 | LegendMarkerItem *item = marker->d_ptr->item(); |
396 | size = size.expandedTo(otherSize: item->effectiveSizeHint(which)); |
397 | } |
398 | size = size.boundedTo(otherSize: constraint); |
399 | } |
400 | else if (constraint.width() >= 0) { |
401 | qreal width = 0; |
402 | qreal height = 0; |
403 | foreach(QLegendMarker *marker, m_legend->d_ptr->markers()) { |
404 | LegendMarkerItem *item = marker->d_ptr->item(); |
405 | width+=item->effectiveSizeHint(which).width(); |
406 | height=qMax(a: height,b: item->effectiveSizeHint(which).height()); |
407 | } |
408 | |
409 | size = QSizeF(qMin(a: constraint.width(),b: width), height); |
410 | } |
411 | else if (constraint.height() >= 0) { |
412 | qreal width = 0; |
413 | qreal height = 0; |
414 | foreach(QLegendMarker *marker, m_legend->d_ptr->markers()) { |
415 | LegendMarkerItem *item = marker->d_ptr->item(); |
416 | width=qMax(a: width,b: item->effectiveSizeHint(which).width()); |
417 | height+=height,item->effectiveSizeHint(which).height(); |
418 | } |
419 | size = QSizeF(width,qMin(a: constraint.height(),b: height)); |
420 | } |
421 | else { |
422 | foreach(QLegendMarker *marker, m_legend->d_ptr->markers()) { |
423 | LegendMarkerItem *item = marker->d_ptr->item(); |
424 | size = size.expandedTo(otherSize: item->effectiveSizeHint(which)); |
425 | } |
426 | } |
427 | size += QSize(left + right, top + bottom); |
428 | return size; |
429 | } |
430 | |
431 | bool LegendLayout::widthLongerThan(const LegendWidthStruct *item1, |
432 | const LegendWidthStruct *item2) |
433 | { |
434 | return item1->width > item2->width; |
435 | } |
436 | |
437 | QT_END_NAMESPACE |
438 | |