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 
LegendLayout(QLegend * legend)41 LegendLayout::LegendLayout(QLegend *legend)
42     : m_legend(legend),
43       m_offsetX(0),
44       m_offsetY(0)
45 {
46 
47 }
48 
~LegendLayout()49 LegendLayout::~LegendLayout()
50 {
51 
52 }
53 
setOffset(qreal x,qreal y)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, &top, &right, &bottom);
75     boundingRect.adjust(left, top, -right, -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(m_minOffsetX, x, m_maxOffsetX);
84             m_legend->d_ptr->items()->setPos(-m_offsetX, boundingRect.top());
85         }
86     } else {
87         if (m_height <= boundingRect.height())
88             return;
89 
90         if (y != m_offsetY) {
91             m_offsetY = qBound(m_minOffsetY, y, m_maxOffsetY);
92             m_legend->d_ptr->items()->setPos(boundingRect.left(), -m_offsetY);
93         }
94     }
95 }
96 
offset() const97 QPointF LegendLayout::offset() const
98 {
99     return QPointF(m_offsetX, m_offsetY);
100 }
101 
invalidate()102 void LegendLayout::invalidate()
103 {
104     QGraphicsLayout::invalidate();
105     if (m_legend->isAttachedToChart())
106         m_legend->d_ptr->m_presenter->layout()->invalidate();
107 }
108 
setGeometry(const QRectF & rect)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 
setAttachedGeometry(const QRectF & rect)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, &top, &right, &bottom);
142 
143     QRectF geometry = rect.adjusted(left, top, -right, -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(Qt::PreferredSize, dummySize).width();
157                     LegendWidthStruct *structItem = new LegendWidthStruct;
158                     structItem->item = item;
159                     structItem->width = itemWidth;
160                     legendWidthList.append(structItem);
161                     markerItemsWidth += itemWidth;
162                     itemMargins += marker->d_ptr->item()->m_margin;
163                 }
164             }
165             std::sort(legendWidthList.begin(), legendWidthList.end(), 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(truncateIndex)->width >= legendWidthList.at(i)->width
177                            && !truncated) {
178                         legendWidthList.at(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(truncateIndex)->width
184                                         < legendWidthList.at(j)->width) {
185                                     legendWidthList.at(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(count - 1)->width
196                                 > legendWidthList.at(truncateIndex)->width) {
197                             legendWidthList.at(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(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(point.x(),geometry.height()/2 - item->boundingRect().height()/2);
237                     const QRectF &rect = item->boundingRect();
238                     size = size.expandedTo(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(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(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(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(oldOffsetX, oldOffsetY);
297 }
298 
setDettachedGeometry(const QRectF & rect)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, &top, &right, &bottom);
316     QRectF geometry = rect.adjusted(left, top, -right, -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(point.x(),point.y());
333                 const QRectF &boundingRect = item->boundingRect();
334                 qreal w = boundingRect.width();
335                 qreal h = boundingRect.height();
336                 m_width = qMax(m_width,w);
337                 m_height = qMax(m_height,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(m_width,w);
369                 m_height = qMax(m_height,h);
370                 item->setPos(point.x(),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(m_height,h);
403                 maxWidth = qMax(maxWidth,w);
404                 item->setPos(point.x(),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(m_height,h);
439                 maxWidth = qMax(maxWidth,w);
440                 item->setPos(point.x() - w,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(oldOffsetX, oldOffsetY);
467 }
468 
sizeHint(Qt::SizeHint which,const QSizeF & constraint) const469 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, &top, &right, &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(item->effectiveSizeHint(which));
479         }
480         size = size.boundedTo(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(height,item->effectiveSizeHint(which).height());
489         }
490 
491         size = QSizeF(qMin(constraint.width(),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(width,item->effectiveSizeHint(which).width());
499             height+=height,item->effectiveSizeHint(which).height();
500         }
501         size = QSizeF(width,qMin(constraint.height(),height));
502     }
503     else {
504         foreach(QLegendMarker *marker, m_legend->d_ptr->markers()) {
505             LegendMarkerItem *item = marker->d_ptr->item();
506             size = size.expandedTo(item->effectiveSizeHint(which));
507         }
508     }
509     size += QSize(left + right, top + bottom);
510     return size;
511 }
512 
widthLongerThan(const LegendWidthStruct * item1,const LegendWidthStruct * item2)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