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"