File indexing completed on 2024-04-28 15:29:34

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