File indexing completed on 2024-05-12 05:47:31
0001 /* 0002 * SPDX-FileCopyrightText: 2011 Peter Penz <peter.penz19@gmail.com> 0003 * 0004 * SPDX-License-Identifier: GPL-2.0-or-later 0005 */ 0006 0007 #include "kitemlistviewlayouter.h" 0008 #include "dolphindebug.h" 0009 #include "kitemlistsizehintresolver.h" 0010 #include "kitemviews/kitemmodelbase.h" 0011 0012 #include <QGuiApplication> 0013 #include <QScopeGuard> 0014 0015 // #define KITEMLISTVIEWLAYOUTER_DEBUG 0016 0017 KItemListViewLayouter::KItemListViewLayouter(KItemListSizeHintResolver *sizeHintResolver, QObject *parent) 0018 : QObject(parent) 0019 , m_dirty(true) 0020 , m_visibleIndexesDirty(true) 0021 , m_scrollOrientation(Qt::Vertical) 0022 , m_size() 0023 , m_itemSize(128, 128) 0024 , m_itemMargin() 0025 , m_headerHeight(0) 0026 , m_model(nullptr) 0027 , m_sizeHintResolver(sizeHintResolver) 0028 , m_scrollOffset(0) 0029 , m_maximumScrollOffset(0) 0030 , m_itemOffset(0) 0031 , m_maximumItemOffset(0) 0032 , m_firstVisibleIndex(-1) 0033 , m_lastVisibleIndex(-1) 0034 , m_columnWidth(0) 0035 , m_xPosInc(0) 0036 , m_columnCount(0) 0037 , m_rowOffsets() 0038 , m_columnOffsets() 0039 , m_groupItemIndexes() 0040 , m_groupHeaderHeight(0) 0041 , m_groupHeaderMargin(0) 0042 , m_itemInfos() 0043 { 0044 Q_ASSERT(m_sizeHintResolver); 0045 } 0046 0047 KItemListViewLayouter::~KItemListViewLayouter() 0048 { 0049 } 0050 0051 void KItemListViewLayouter::setScrollOrientation(Qt::Orientation orientation) 0052 { 0053 if (m_scrollOrientation != orientation) { 0054 m_scrollOrientation = orientation; 0055 m_dirty = true; 0056 } 0057 } 0058 0059 Qt::Orientation KItemListViewLayouter::scrollOrientation() const 0060 { 0061 return m_scrollOrientation; 0062 } 0063 0064 void KItemListViewLayouter::setSize(const QSizeF &size) 0065 { 0066 if (m_size != size) { 0067 if (m_scrollOrientation == Qt::Vertical) { 0068 if (m_size.width() != size.width()) { 0069 m_dirty = true; 0070 } 0071 } else if (m_size.height() != size.height()) { 0072 m_dirty = true; 0073 } 0074 0075 m_size = size; 0076 m_visibleIndexesDirty = true; 0077 } 0078 } 0079 0080 QSizeF KItemListViewLayouter::size() const 0081 { 0082 return m_size; 0083 } 0084 0085 void KItemListViewLayouter::setItemSize(const QSizeF &size) 0086 { 0087 if (m_itemSize != size) { 0088 m_itemSize = size; 0089 m_dirty = true; 0090 } 0091 } 0092 0093 QSizeF KItemListViewLayouter::itemSize() const 0094 { 0095 return m_itemSize; 0096 } 0097 0098 void KItemListViewLayouter::setItemMargin(const QSizeF &margin) 0099 { 0100 if (m_itemMargin != margin) { 0101 m_itemMargin = margin; 0102 m_dirty = true; 0103 } 0104 } 0105 0106 QSizeF KItemListViewLayouter::itemMargin() const 0107 { 0108 return m_itemMargin; 0109 } 0110 0111 void KItemListViewLayouter::setHeaderHeight(qreal height) 0112 { 0113 if (m_headerHeight != height) { 0114 m_headerHeight = height; 0115 m_dirty = true; 0116 } 0117 } 0118 0119 qreal KItemListViewLayouter::headerHeight() const 0120 { 0121 return m_headerHeight; 0122 } 0123 0124 void KItemListViewLayouter::setGroupHeaderHeight(qreal height) 0125 { 0126 if (m_groupHeaderHeight != height) { 0127 m_groupHeaderHeight = height; 0128 m_dirty = true; 0129 } 0130 } 0131 0132 qreal KItemListViewLayouter::groupHeaderHeight() const 0133 { 0134 return m_groupHeaderHeight; 0135 } 0136 0137 void KItemListViewLayouter::setGroupHeaderMargin(qreal margin) 0138 { 0139 if (m_groupHeaderMargin != margin) { 0140 m_groupHeaderMargin = margin; 0141 m_dirty = true; 0142 } 0143 } 0144 0145 qreal KItemListViewLayouter::groupHeaderMargin() const 0146 { 0147 return m_groupHeaderMargin; 0148 } 0149 0150 void KItemListViewLayouter::setScrollOffset(qreal offset) 0151 { 0152 if (m_scrollOffset != offset) { 0153 m_scrollOffset = offset; 0154 m_visibleIndexesDirty = true; 0155 } 0156 } 0157 0158 qreal KItemListViewLayouter::scrollOffset() const 0159 { 0160 return m_scrollOffset; 0161 } 0162 0163 qreal KItemListViewLayouter::maximumScrollOffset() const 0164 { 0165 const_cast<KItemListViewLayouter *>(this)->doLayout(); 0166 return m_maximumScrollOffset; 0167 } 0168 0169 void KItemListViewLayouter::setItemOffset(qreal offset) 0170 { 0171 if (m_itemOffset != offset) { 0172 m_itemOffset = offset; 0173 m_visibleIndexesDirty = true; 0174 } 0175 } 0176 0177 qreal KItemListViewLayouter::itemOffset() const 0178 { 0179 return m_itemOffset; 0180 } 0181 0182 qreal KItemListViewLayouter::maximumItemOffset() const 0183 { 0184 const_cast<KItemListViewLayouter *>(this)->doLayout(); 0185 return m_maximumItemOffset; 0186 } 0187 0188 void KItemListViewLayouter::setModel(const KItemModelBase *model) 0189 { 0190 if (m_model != model) { 0191 m_model = model; 0192 m_dirty = true; 0193 } 0194 } 0195 0196 const KItemModelBase *KItemListViewLayouter::model() const 0197 { 0198 return m_model; 0199 } 0200 0201 int KItemListViewLayouter::firstVisibleIndex() const 0202 { 0203 const_cast<KItemListViewLayouter *>(this)->doLayout(); 0204 return m_firstVisibleIndex; 0205 } 0206 0207 int KItemListViewLayouter::lastVisibleIndex() const 0208 { 0209 const_cast<KItemListViewLayouter *>(this)->doLayout(); 0210 return m_lastVisibleIndex; 0211 } 0212 0213 QRectF KItemListViewLayouter::itemRect(int index) const 0214 { 0215 const_cast<KItemListViewLayouter *>(this)->doLayout(); 0216 if (index < 0 || index >= m_itemInfos.count()) { 0217 return QRectF(); 0218 } 0219 0220 QSizeF sizeHint = m_sizeHintResolver->sizeHint(index); 0221 0222 const qreal x = m_columnOffsets.at(m_itemInfos.at(index).column); 0223 const qreal y = m_rowOffsets.at(m_itemInfos.at(index).row); 0224 0225 if (m_scrollOrientation == Qt::Horizontal) { 0226 // Rotate the logical direction which is always vertical by 90° 0227 // to get the physical horizontal direction 0228 QPointF pos(y, x); 0229 pos.rx() -= m_scrollOffset; 0230 sizeHint.transpose(); 0231 return QRectF(pos, sizeHint); 0232 } 0233 0234 if (sizeHint.width() <= 0) { 0235 // In Details View, a size hint with negative width is used internally. 0236 sizeHint.rwidth() = m_itemSize.width(); 0237 } 0238 0239 const QPointF pos(x - m_itemOffset, y - m_scrollOffset); 0240 return QRectF(pos, sizeHint); 0241 } 0242 0243 QRectF KItemListViewLayouter::groupHeaderRect(int index) const 0244 { 0245 const_cast<KItemListViewLayouter *>(this)->doLayout(); 0246 0247 const QRectF firstItemRect = itemRect(index); 0248 QPointF pos = firstItemRect.topLeft(); 0249 if (pos.isNull()) { 0250 return QRectF(); 0251 } 0252 0253 QSizeF size; 0254 if (m_scrollOrientation == Qt::Vertical) { 0255 pos.rx() = 0; 0256 pos.ry() -= m_groupHeaderHeight; 0257 size = QSizeF(m_size.width(), m_groupHeaderHeight); 0258 } else { 0259 pos.rx() -= m_itemMargin.width(); 0260 pos.ry() = 0; 0261 0262 // Determine the maximum width used in the current column. As the 0263 // scroll-direction is Qt::Horizontal and m_itemRects is accessed 0264 // directly, the logical height represents the visual width, and 0265 // the logical row represents the column. 0266 qreal headerWidth = minimumGroupHeaderWidth(); 0267 const int row = m_itemInfos[index].row; 0268 const int maxIndex = m_itemInfos.count() - 1; 0269 while (index <= maxIndex) { 0270 if (m_itemInfos[index].row != row) { 0271 break; 0272 } 0273 0274 const qreal itemWidth = 0275 (m_scrollOrientation == Qt::Vertical) ? m_sizeHintResolver->sizeHint(index).width() : m_sizeHintResolver->sizeHint(index).height(); 0276 0277 if (itemWidth > headerWidth) { 0278 headerWidth = itemWidth; 0279 } 0280 0281 ++index; 0282 } 0283 0284 size = QSizeF(headerWidth, m_size.height()); 0285 } 0286 return QRectF(pos, size); 0287 } 0288 0289 int KItemListViewLayouter::itemColumn(int index) const 0290 { 0291 const_cast<KItemListViewLayouter *>(this)->doLayout(); 0292 if (index < 0 || index >= m_itemInfos.count()) { 0293 return -1; 0294 } 0295 0296 return (m_scrollOrientation == Qt::Vertical) ? m_itemInfos[index].column : m_itemInfos[index].row; 0297 } 0298 0299 int KItemListViewLayouter::itemRow(int index) const 0300 { 0301 const_cast<KItemListViewLayouter *>(this)->doLayout(); 0302 if (index < 0 || index >= m_itemInfos.count()) { 0303 return -1; 0304 } 0305 0306 return (m_scrollOrientation == Qt::Vertical) ? m_itemInfos[index].row : m_itemInfos[index].column; 0307 } 0308 0309 int KItemListViewLayouter::maximumVisibleItems() const 0310 { 0311 const_cast<KItemListViewLayouter *>(this)->doLayout(); 0312 0313 const int height = static_cast<int>(m_size.height()); 0314 const int rowHeight = static_cast<int>(m_itemSize.height()); 0315 int rows = height / rowHeight; 0316 if (height % rowHeight != 0) { 0317 ++rows; 0318 } 0319 0320 return rows * m_columnCount; 0321 } 0322 0323 bool KItemListViewLayouter::isFirstGroupItem(int itemIndex) const 0324 { 0325 const_cast<KItemListViewLayouter *>(this)->doLayout(); 0326 return m_groupItemIndexes.contains(itemIndex); 0327 } 0328 0329 void KItemListViewLayouter::markAsDirty() 0330 { 0331 m_dirty = true; 0332 } 0333 0334 #ifndef QT_NO_DEBUG 0335 bool KItemListViewLayouter::isDirty() 0336 { 0337 return m_dirty; 0338 } 0339 #endif 0340 0341 void KItemListViewLayouter::doLayout() 0342 { 0343 // we always want to update visible indexes after performing a layout 0344 auto qsg = qScopeGuard([this] { 0345 updateVisibleIndexes(); 0346 }); 0347 0348 if (!m_dirty) { 0349 return; 0350 } 0351 0352 #ifdef KITEMLISTVIEWLAYOUTER_DEBUG 0353 QElapsedTimer timer; 0354 timer.start(); 0355 #endif 0356 m_visibleIndexesDirty = true; 0357 0358 QSizeF itemSize = m_itemSize; 0359 QSizeF itemMargin = m_itemMargin; 0360 QSizeF size = m_size; 0361 0362 const bool grouped = createGroupHeaders(); 0363 0364 const bool horizontalScrolling = (m_scrollOrientation == Qt::Horizontal); 0365 if (horizontalScrolling) { 0366 // Flip everything so that the layout logically can work like having 0367 // a vertical scrolling 0368 itemSize.transpose(); 0369 itemMargin.transpose(); 0370 size.transpose(); 0371 0372 if (grouped) { 0373 // In the horizontal scrolling case all groups are aligned 0374 // at the top, which decreases the available height. For the 0375 // flipped data this means that the width must be decreased. 0376 size.rwidth() -= m_groupHeaderHeight; 0377 } 0378 } 0379 0380 m_columnWidth = itemSize.width() + itemMargin.width(); 0381 const qreal widthForColumns = size.width() - itemMargin.width(); 0382 m_columnCount = qMax(1, int(widthForColumns / m_columnWidth)); 0383 m_xPosInc = itemMargin.width(); 0384 0385 const int itemCount = m_model->count(); 0386 if (itemCount > m_columnCount && m_columnWidth >= 32) { 0387 // Apply the unused width equally to each column 0388 const qreal unusedWidth = widthForColumns - m_columnCount * m_columnWidth; 0389 if (unusedWidth > 0) { 0390 const qreal columnInc = unusedWidth / (m_columnCount + 1); 0391 m_columnWidth += columnInc; 0392 m_xPosInc += columnInc; 0393 } 0394 } 0395 0396 m_itemInfos.resize(itemCount); 0397 0398 // Calculate the offset of each column, i.e., the x-coordinate where the column starts. 0399 m_columnOffsets.resize(m_columnCount); 0400 qreal currentOffset = QGuiApplication::isRightToLeft() ? widthForColumns : m_xPosInc; 0401 0402 if (grouped && horizontalScrolling) { 0403 // All group headers will always be aligned on the top and not 0404 // flipped like the other properties. 0405 currentOffset += m_groupHeaderHeight; 0406 } 0407 0408 if (QGuiApplication::isLeftToRight()) 0409 for (int column = 0; column < m_columnCount; ++column) { 0410 m_columnOffsets[column] = currentOffset; 0411 currentOffset += m_columnWidth; 0412 } 0413 else 0414 for (int column = 0; column < m_columnCount; ++column) { 0415 m_columnOffsets[column] = currentOffset - m_columnWidth; 0416 currentOffset -= m_columnWidth; 0417 } 0418 0419 // Prepare the QVector which stores the y-coordinate for each new row. 0420 int numberOfRows = (itemCount + m_columnCount - 1) / m_columnCount; 0421 if (grouped && m_columnCount > 1) { 0422 // In the worst case, a new row will be started for every group. 0423 // We could calculate the exact number of rows now to prevent that we reserve 0424 // too much memory, but the code required to do that might need much more 0425 // memory than it would save in the average case. 0426 numberOfRows += m_groupItemIndexes.count(); 0427 } 0428 m_rowOffsets.resize(numberOfRows); 0429 0430 qreal y = m_headerHeight + itemMargin.height(); 0431 int row = 0; 0432 0433 int index = 0; 0434 while (index < itemCount) { 0435 qreal maxItemHeight = itemSize.height(); 0436 0437 if (grouped) { 0438 if (m_groupItemIndexes.contains(index)) { 0439 // The item is the first item of a group. 0440 // Increase the y-position to provide space 0441 // for the group header. 0442 if (index > 0) { 0443 // Only add a margin if there has been added another 0444 // group already before 0445 y += m_groupHeaderMargin; 0446 } else if (!horizontalScrolling) { 0447 // The first group header should be aligned on top 0448 y -= itemMargin.height(); 0449 } 0450 0451 if (!horizontalScrolling) { 0452 y += m_groupHeaderHeight; 0453 } 0454 } 0455 } 0456 0457 m_rowOffsets[row] = y; 0458 0459 int column = 0; 0460 while (index < itemCount && column < m_columnCount) { 0461 qreal requiredItemHeight = itemSize.height(); 0462 const QSizeF sizeHint = m_sizeHintResolver->sizeHint(index); 0463 const qreal sizeHintHeight = sizeHint.height(); 0464 if (sizeHintHeight > requiredItemHeight) { 0465 requiredItemHeight = sizeHintHeight; 0466 } 0467 0468 ItemInfo &itemInfo = m_itemInfos[index]; 0469 itemInfo.column = column; 0470 itemInfo.row = row; 0471 0472 if (grouped && horizontalScrolling) { 0473 // When grouping is enabled in the horizontal mode, the header alignment 0474 // looks like this: 0475 // Header-1 Header-2 Header-3 0476 // Item 1 Item 4 Item 7 0477 // Item 2 Item 5 Item 8 0478 // Item 3 Item 6 Item 9 0479 // In this case 'requiredItemHeight' represents the column-width. We don't 0480 // check the content of the header in the layouter to determine the required 0481 // width, hence assure that at least a minimal width of 15 characters is given 0482 // (in average a character requires the halve width of the font height). 0483 // 0484 // TODO: Let the group headers provide a minimum width and respect this width here 0485 const qreal headerWidth = minimumGroupHeaderWidth(); 0486 if (requiredItemHeight < headerWidth) { 0487 requiredItemHeight = headerWidth; 0488 } 0489 } 0490 0491 maxItemHeight = qMax(maxItemHeight, requiredItemHeight); 0492 ++index; 0493 ++column; 0494 0495 if (grouped && m_groupItemIndexes.contains(index)) { 0496 // The item represents the first index of a group 0497 // and must aligned in the first column 0498 break; 0499 } 0500 } 0501 0502 y += maxItemHeight + itemMargin.height(); 0503 ++row; 0504 } 0505 0506 if (itemCount > 0) { 0507 m_maximumScrollOffset = y; 0508 m_maximumItemOffset = m_columnCount * m_columnWidth; 0509 } else { 0510 m_maximumScrollOffset = 0; 0511 m_maximumItemOffset = 0; 0512 } 0513 0514 #ifdef KITEMLISTVIEWLAYOUTER_DEBUG 0515 qCDebug(DolphinDebug) << "[TIME] doLayout() for " << m_model->count() << "items:" << timer.elapsed(); 0516 #endif 0517 m_dirty = false; 0518 } 0519 0520 void KItemListViewLayouter::updateVisibleIndexes() 0521 { 0522 if (!m_visibleIndexesDirty) { 0523 return; 0524 } 0525 0526 Q_ASSERT(!m_dirty); 0527 0528 if (m_model->count() <= 0) { 0529 m_firstVisibleIndex = -1; 0530 m_lastVisibleIndex = -1; 0531 m_visibleIndexesDirty = false; 0532 return; 0533 } 0534 0535 const int maxIndex = m_model->count() - 1; 0536 0537 // Calculate the first visible index that is fully visible 0538 int min = 0; 0539 int max = maxIndex; 0540 int mid = 0; 0541 do { 0542 mid = (min + max) / 2; 0543 if (m_rowOffsets.at(m_itemInfos[mid].row) < m_scrollOffset) { 0544 min = mid + 1; 0545 } else { 0546 max = mid - 1; 0547 } 0548 } while (min <= max); 0549 0550 if (mid > 0) { 0551 // Include the row before the first fully visible index, as it might 0552 // be partly visible 0553 if (m_rowOffsets.at(m_itemInfos[mid].row) >= m_scrollOffset) { 0554 --mid; 0555 Q_ASSERT(m_rowOffsets.at(m_itemInfos[mid].row) < m_scrollOffset); 0556 } 0557 0558 const int firstVisibleRow = m_itemInfos[mid].row; 0559 while (mid > 0 && m_itemInfos[mid - 1].row == firstVisibleRow) { 0560 --mid; 0561 } 0562 } 0563 m_firstVisibleIndex = mid; 0564 0565 // Calculate the last visible index that is (at least partly) visible 0566 const int visibleHeight = (m_scrollOrientation == Qt::Horizontal) ? m_size.width() : m_size.height(); 0567 qreal bottom = m_scrollOffset + visibleHeight; 0568 if (m_model->groupedSorting()) { 0569 bottom += m_groupHeaderHeight; 0570 } 0571 0572 min = m_firstVisibleIndex; 0573 max = maxIndex; 0574 do { 0575 mid = (min + max) / 2; 0576 if (m_rowOffsets.at(m_itemInfos[mid].row) <= bottom) { 0577 min = mid + 1; 0578 } else { 0579 max = mid - 1; 0580 } 0581 } while (min <= max); 0582 0583 while (mid > 0 && m_rowOffsets.at(m_itemInfos[mid].row) > bottom) { 0584 --mid; 0585 } 0586 m_lastVisibleIndex = mid; 0587 0588 m_visibleIndexesDirty = false; 0589 } 0590 0591 bool KItemListViewLayouter::createGroupHeaders() 0592 { 0593 if (!m_model->groupedSorting()) { 0594 return false; 0595 } 0596 0597 m_groupItemIndexes.clear(); 0598 0599 const QList<QPair<int, QVariant>> groups = m_model->groups(); 0600 if (groups.isEmpty()) { 0601 return false; 0602 } 0603 0604 for (int i = 0; i < groups.count(); ++i) { 0605 const int firstItemIndex = groups.at(i).first; 0606 m_groupItemIndexes.insert(firstItemIndex); 0607 } 0608 0609 return true; 0610 } 0611 0612 qreal KItemListViewLayouter::minimumGroupHeaderWidth() const 0613 { 0614 return 100; 0615 } 0616 0617 #include "moc_kitemlistviewlayouter.cpp"