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
13QT_BEGIN_NAMESPACE
14
15LegendLayout::LegendLayout(QLegend *legend)
16 : m_legend(legend),
17 m_offsetX(0),
18 m_offsetY(0)
19{
20
21}
22
23LegendLayout::~LegendLayout()
24{
25
26}
27
28void 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
71QPointF LegendLayout::offset() const
72{
73 return QPointF(m_offsetX, m_offsetY);
74}
75
76void LegendLayout::invalidate()
77{
78 QGraphicsLayout::invalidate();
79 if (m_legend->isAttachedToChart())
80 m_legend->d_ptr->m_presenter->layout()->invalidate();
81}
82
83void 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
95void 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
273void 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
387QSizeF 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
431bool LegendLayout::widthLongerThan(const LegendWidthStruct *item1,
432 const LegendWidthStruct *item2)
433{
434 return item1->width > item2->width;
435}
436
437QT_END_NAMESPACE
438

source code of qtcharts/src/charts/legend/legendlayout.cpp