File indexing completed on 2024-05-05 07:58:31
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"