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"