File indexing completed on 2024-04-21 03:56:42

0001 /*
0002  * SPDX-FileCopyrightText: 2020 Arjen Hiemstra <ahiemstra@heimr.nl>
0003  *
0004  * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
0005  */
0006 
0007 #include "LegendLayout.h"
0008 
0009 #include <cmath>
0010 
0011 #include "Chart.h"
0012 #include "ItemBuilder.h"
0013 #include "datasource/ChartDataSource.h"
0014 
0015 qreal sizeWithSpacing(int count, qreal size, qreal spacing)
0016 {
0017     return size * count + spacing * (count - 1);
0018 }
0019 
0020 LegendLayoutAttached::LegendLayoutAttached(QObject *parent)
0021     : QObject(parent)
0022 {
0023 }
0024 
0025 qreal LegendLayoutAttached::minimumWidth() const
0026 {
0027     return m_minimumWidth.value_or(0.0);
0028 }
0029 
0030 void LegendLayoutAttached::setMinimumWidth(qreal newMinimumWidth)
0031 {
0032     if (newMinimumWidth == m_minimumWidth) {
0033         return;
0034     }
0035 
0036     m_minimumWidth = newMinimumWidth;
0037     Q_EMIT minimumWidthChanged();
0038 }
0039 
0040 bool LegendLayoutAttached::isMinimumWidthValid() const
0041 {
0042     return m_minimumWidth.has_value();
0043 }
0044 
0045 qreal LegendLayoutAttached::preferredWidth() const
0046 {
0047     return m_preferredWidth.value_or(0.0);
0048 }
0049 
0050 void LegendLayoutAttached::setPreferredWidth(qreal newPreferredWidth)
0051 {
0052     if (newPreferredWidth == m_preferredWidth) {
0053         return;
0054     }
0055 
0056     m_preferredWidth = newPreferredWidth;
0057     Q_EMIT preferredWidthChanged();
0058 }
0059 
0060 bool LegendLayoutAttached::isPreferredWidthValid() const
0061 {
0062     return m_preferredWidth.has_value();
0063 }
0064 
0065 qreal LegendLayoutAttached::maximumWidth() const
0066 {
0067     return m_maximumWidth.value_or(0.0);
0068 }
0069 
0070 void LegendLayoutAttached::setMaximumWidth(qreal newMaximumWidth)
0071 {
0072     if (newMaximumWidth == m_maximumWidth) {
0073         return;
0074     }
0075 
0076     m_maximumWidth = newMaximumWidth;
0077     Q_EMIT maximumWidthChanged();
0078 }
0079 
0080 bool LegendLayoutAttached::isMaximumWidthValid() const
0081 {
0082     return m_maximumWidth.has_value();
0083 }
0084 
0085 LegendLayout::LegendLayout(QQuickItem *parent)
0086     : QQuickItem(parent)
0087 {
0088 }
0089 
0090 qreal LegendLayout::horizontalSpacing() const
0091 {
0092     return m_horizontalSpacing;
0093 }
0094 
0095 void LegendLayout::setHorizontalSpacing(qreal newHorizontalSpacing)
0096 {
0097     if (newHorizontalSpacing == m_horizontalSpacing) {
0098         return;
0099     }
0100 
0101     m_horizontalSpacing = newHorizontalSpacing;
0102     polish();
0103     Q_EMIT horizontalSpacingChanged();
0104 }
0105 
0106 qreal LegendLayout::verticalSpacing() const
0107 {
0108     return m_verticalSpacing;
0109 }
0110 
0111 void LegendLayout::setVerticalSpacing(qreal newVerticalSpacing)
0112 {
0113     if (newVerticalSpacing == m_verticalSpacing) {
0114         return;
0115     }
0116 
0117     m_verticalSpacing = newVerticalSpacing;
0118     polish();
0119     Q_EMIT verticalSpacingChanged();
0120 }
0121 
0122 qreal LegendLayout::preferredWidth() const
0123 {
0124     return m_preferredWidth;
0125 }
0126 
0127 void LegendLayout::componentComplete()
0128 {
0129     QQuickItem::componentComplete();
0130 
0131     m_completed = true;
0132     polish();
0133 }
0134 
0135 void LegendLayout::updatePolish()
0136 {
0137     if (!m_completed) {
0138         return;
0139     }
0140 
0141     int columns = 0;
0142     int rows = 0;
0143     qreal itemWidth = 0.0;
0144     qreal itemHeight = 0.0;
0145 
0146     qreal layoutWidth = width();
0147 
0148     std::tie(columns, rows, itemWidth, itemHeight) = determineColumns();
0149 
0150     auto column = 0;
0151     auto row = 0;
0152 
0153     const auto items = childItems();
0154     for (auto item : items) {
0155         if (!item->isVisible() || item->implicitWidth() <= 0 || item->implicitHeight() <= 0) {
0156             continue;
0157         }
0158 
0159         auto attached = static_cast<LegendLayoutAttached *>(qmlAttachedPropertiesObject<LegendLayout>(item, true));
0160 
0161         auto x = (itemWidth + m_horizontalSpacing) * column;
0162         auto y = (itemHeight + m_verticalSpacing) * row;
0163 
0164         item->setPosition(QPointF{x, y});
0165         item->setWidth(std::clamp(itemWidth, attached->minimumWidth(), attached->maximumWidth()));
0166 
0167         // If we are in single column mode, we are most likely width constrained.
0168         // In that case, we should make sure items do not exceed our own width,
0169         // so we can trigger things like text eliding.
0170         if (layoutWidth > 0 && item->width() > layoutWidth && columns == 1) {
0171             item->setWidth(layoutWidth);
0172         }
0173 
0174         column++;
0175         if (column >= columns) {
0176             row++;
0177             column = 0;
0178         }
0179     }
0180 
0181     setImplicitSize(sizeWithSpacing(columns, itemWidth, m_horizontalSpacing), sizeWithSpacing(rows, itemHeight, m_verticalSpacing));
0182 }
0183 
0184 void LegendLayout::geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry)
0185 {
0186     if (newGeometry != oldGeometry) {
0187         polish();
0188     }
0189     QQuickItem::geometryChange(newGeometry, oldGeometry);
0190 }
0191 
0192 void LegendLayout::itemChange(QQuickItem::ItemChange change, const QQuickItem::ItemChangeData &data)
0193 {
0194     if (change == QQuickItem::ItemVisibleHasChanged || change == QQuickItem::ItemSceneChange) {
0195         polish();
0196     }
0197 
0198     if (change == QQuickItem::ItemChildAddedChange) {
0199         auto item = data.item;
0200 
0201         connect(item, &QQuickItem::implicitWidthChanged, this, &LegendLayout::polish);
0202         connect(item, &QQuickItem::implicitHeightChanged, this, &LegendLayout::polish);
0203         connect(item, &QQuickItem::visibleChanged, this, &LegendLayout::polish);
0204 
0205         auto attached = static_cast<LegendLayoutAttached *>(qmlAttachedPropertiesObject<LegendLayout>(item, true));
0206         connect(attached, &LegendLayoutAttached::minimumWidthChanged, this, &LegendLayout::polish);
0207         connect(attached, &LegendLayoutAttached::preferredWidthChanged, this, &LegendLayout::polish);
0208         connect(attached, &LegendLayoutAttached::maximumWidthChanged, this, &LegendLayout::polish);
0209 
0210         polish();
0211     }
0212 
0213     if (change == QQuickItem::ItemChildRemovedChange) {
0214         auto item = data.item;
0215 
0216         item->disconnect(this);
0217         auto attached = static_cast<LegendLayoutAttached *>(qmlAttachedPropertiesObject<LegendLayout>(item, false));
0218         if (attached) {
0219             attached->disconnect(this);
0220         }
0221 
0222         polish();
0223     }
0224 
0225     QQuickItem::itemChange(change, data);
0226 }
0227 
0228 // Determine how many columns and rows should be used for placing items and how
0229 // large each item should be.
0230 std::tuple<int, int, qreal, qreal> LegendLayout::determineColumns()
0231 {
0232     auto minWidth = -std::numeric_limits<qreal>::max();
0233     auto preferredWidth = -std::numeric_limits<qreal>::max();
0234     auto maxWidth = std::numeric_limits<qreal>::max();
0235     auto maxHeight = -std::numeric_limits<qreal>::max();
0236 
0237     const auto items = childItems();
0238 
0239     // Keep track of actual visual and visible items, since childItems() also
0240     // includes stuff like repeaters.
0241     auto itemCount = 0;
0242 
0243     // First, we determine the minimum, preferred and maximum width of all
0244     // items. These are determined from the attached object, or implicitWidth
0245     // for minimum size if minimumWidth has not been set.
0246     //
0247     // We also determine the maximum height of items so we do not need to do
0248     // that later.
0249     for (auto item : items) {
0250         if (!item->isVisible() || item->implicitWidth() <= 0 || item->implicitHeight() <= 0) {
0251             continue;
0252         }
0253 
0254         auto attached = static_cast<LegendLayoutAttached *>(qmlAttachedPropertiesObject<LegendLayout>(item, true));
0255 
0256         if (attached->isMinimumWidthValid()) {
0257             minWidth = std::max(minWidth, attached->minimumWidth());
0258         } else {
0259             minWidth = std::max(minWidth, item->implicitWidth());
0260         }
0261 
0262         if (attached->isPreferredWidthValid()) {
0263             preferredWidth = std::max(preferredWidth, attached->preferredWidth());
0264         }
0265 
0266         if (attached->isMaximumWidthValid()) {
0267             maxWidth = std::min(maxWidth, attached->maximumWidth());
0268         }
0269 
0270         maxHeight = std::max(maxHeight, item->implicitHeight());
0271 
0272         itemCount++;
0273     }
0274 
0275     if (itemCount == 0) {
0276         return std::make_tuple(0, 0, 0, 0);
0277     }
0278 
0279     auto availableWidth = width();
0280     // Check if we have a valid width. If we cannot even fit a horizontalSpacing
0281     // we cannot do anything with the width and most likely did not get a width
0282     // assigned, so come up with some reasonable default width.
0283     //
0284     // For the default, layout everything in a full row, using either maxWidth
0285     // for each item if we have it or minWidth if we do not.
0286     if (availableWidth <= m_horizontalSpacing) {
0287         if (maxWidth <= 0.0) {
0288             availableWidth = sizeWithSpacing(itemCount, minWidth, m_horizontalSpacing);
0289         } else {
0290             availableWidth = sizeWithSpacing(itemCount, maxWidth, m_horizontalSpacing);
0291         }
0292     }
0293 
0294     // If none of the items have a maximum width set, default to filling all
0295     // available space.
0296     if (maxWidth <= 0.0 || maxWidth >= std::numeric_limits<qreal>::max()) {
0297         maxWidth = availableWidth;
0298     }
0299 
0300     // Ensure we don't try to size things below their minimum size.
0301     if (maxWidth < minWidth) {
0302         maxWidth = minWidth;
0303     }
0304 
0305     if (preferredWidth != m_preferredWidth) {
0306         m_preferredWidth = preferredWidth;
0307         Q_EMIT preferredWidthChanged();
0308     }
0309 
0310     auto columns = 1;
0311     auto rows = itemCount;
0312     bool fit = true;
0313 
0314     // Calculate the actual number of rows and columns by trying to fit items
0315     // until we find the right number.
0316     while (true) {
0317         auto minTotalWidth = sizeWithSpacing(columns, minWidth, m_horizontalSpacing);
0318         auto maxTotalWidth = sizeWithSpacing(columns, maxWidth, m_horizontalSpacing);
0319 
0320         // If the minimum width is less than our width, but the maximum is
0321         // larger, we found a correct solution since we can resize the items to
0322         // fit within the provided bounds.
0323         if (minTotalWidth <= availableWidth && maxTotalWidth >= availableWidth) {
0324             break;
0325         }
0326 
0327         // As long as we have more space available than the items' max size,
0328         // decrease the number of rows and that way increase the number of
0329         // columns we use to place items - unless that results in no rows, as
0330         // that means we've reached a state where we simply have more space than
0331         // needed.
0332         if (maxTotalWidth < availableWidth) {
0333             rows--;
0334             if (rows >= 1) {
0335                 columns = std::ceil(itemCount / float(rows));
0336             } else {
0337                 fit = false;
0338                 break;
0339             }
0340         }
0341 
0342         // In certain cases, we hit a corner case where decreasing the number of
0343         // rows leads to things ending up outside of the item's bounds. If that
0344         // happens, increase the number of rows by one and exit the loop.
0345         if (minTotalWidth > availableWidth) {
0346             rows += 1;
0347             columns = std::ceil(itemCount / float(rows));
0348             break;
0349         }
0350     }
0351 
0352     // Calculate item width based on the calculated number of columns.
0353     // If it turns out we have more space than needed, use maxWidth
0354     // instead to avoid awkward gaps.
0355     auto itemWidth = fit ? (availableWidth - m_horizontalSpacing * (columns - 1)) / columns : maxWidth;
0356 
0357     // Recalculate the number of rows, otherwise we may end up with "ghost" rows
0358     // since the items wrapped into a new column, but no all of them.
0359     rows = std::ceil(itemCount / float(columns));
0360 
0361     return std::make_tuple(columns, rows, itemWidth, maxHeight);
0362 }
0363 
0364 #include "moc_LegendLayout.cpp"