File indexing completed on 2024-10-13 12:16:00

0001 /*
0002     This file is part of the KDE project
0003     SPDX-FileCopyrightText: 2007, 2009 Rafael Fernández López <ereslibre@kde.org>
0004 
0005     SPDX-License-Identifier: LGPL-2.0-or-later
0006 */
0007 
0008 /**
0009  * IMPLEMENTATION NOTES:
0010  *
0011  * QListView::setRowHidden() and QListView::isRowHidden() are not taken into
0012  * account. This methods should actually not exist. This effect should be handled
0013  * by an hypothetical QSortFilterProxyModel which filters out the desired rows.
0014  *
0015  * In case this needs to be implemented, contact me, but I consider this a faulty
0016  * design.
0017  */
0018 
0019 #include "kcategorizedview.h"
0020 #include "kcategorizedview_p.h"
0021 
0022 #include <QPaintEvent>
0023 #include <QPainter>
0024 #include <QScrollBar>
0025 
0026 #include "kcategorizedsortfilterproxymodel.h"
0027 #include "kcategorydrawer.h"
0028 
0029 // BEGIN: Private part
0030 
0031 struct KCategorizedViewPrivate::Item {
0032     Item()
0033         : topLeft(QPoint())
0034         , size(QSize())
0035     {
0036     }
0037 
0038     QPoint topLeft;
0039     QSize size;
0040 };
0041 
0042 struct KCategorizedViewPrivate::Block {
0043     Block()
0044         : topLeft(QPoint())
0045         , firstIndex(QModelIndex())
0046         , quarantineStart(QModelIndex())
0047         , items(QList<Item>())
0048     {
0049     }
0050 
0051     bool operator!=(const Block &rhs) const
0052     {
0053         return firstIndex != rhs.firstIndex;
0054     }
0055 
0056     static bool lessThan(const Block &left, const Block &right)
0057     {
0058         Q_ASSERT(left.firstIndex.isValid());
0059         Q_ASSERT(right.firstIndex.isValid());
0060         return left.firstIndex.row() < right.firstIndex.row();
0061     }
0062 
0063     QPoint topLeft;
0064     int height = -1;
0065     QPersistentModelIndex firstIndex;
0066     // if we have n elements on this block, and we inserted an element at position i. The quarantine
0067     // will start at index (i, column, parent). This means that for all elements j where i <= j <= n, the
0068     // visual rect position of item j will have to be recomputed (cannot use the cached point). The quarantine
0069     // will only affect the current block, since the rest of blocks can be affected only in the way
0070     // that the whole block will have different offset, but items will keep the same relative position
0071     // in terms of their parent blocks.
0072     QPersistentModelIndex quarantineStart;
0073     QList<Item> items;
0074 
0075     // this affects the whole block, not items separately. items contain the topLeft point relative
0076     // to the block. Because of insertions or removals a whole block can be moved, so the whole block
0077     // will enter in quarantine, what is faster than moving all items in absolute terms.
0078     bool outOfQuarantine = false;
0079 
0080     // should we alternate its color ? is just a hint, could not be used
0081     bool alternate = false;
0082     bool collapsed = false;
0083 };
0084 
0085 KCategorizedViewPrivate::KCategorizedViewPrivate(KCategorizedView *qq)
0086     : q(qq)
0087     , hoveredBlock(new Block())
0088     , hoveredIndex(QModelIndex())
0089     , pressedPosition(QPoint())
0090     , rubberBandRect(QRect())
0091 {
0092 }
0093 
0094 KCategorizedViewPrivate::~KCategorizedViewPrivate()
0095 {
0096     delete hoveredBlock;
0097 }
0098 
0099 bool KCategorizedViewPrivate::isCategorized() const
0100 {
0101     return proxyModel && categoryDrawer && proxyModel->isCategorizedModel();
0102 }
0103 
0104 QStyleOptionViewItem KCategorizedViewPrivate::viewOpts()
0105 {
0106 #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
0107     QStyleOptionViewItem option;
0108     q->initViewItemOption(&option);
0109 #else
0110     QStyleOptionViewItem option(q->viewOptions());
0111 #endif
0112     return option;
0113 }
0114 
0115 QStyleOptionViewItem KCategorizedViewPrivate::blockRect(const QModelIndex &representative)
0116 {
0117     QStyleOptionViewItem option = viewOpts();
0118 
0119     const int height = categoryDrawer->categoryHeight(representative, option);
0120     const QString categoryDisplay = representative.data(KCategorizedSortFilterProxyModel::CategoryDisplayRole).toString();
0121     QPoint pos = blockPosition(categoryDisplay);
0122     pos.ry() -= height;
0123     option.rect.setTopLeft(pos);
0124     option.rect.setWidth(viewportWidth() + categoryDrawer->leftMargin() + categoryDrawer->rightMargin());
0125     option.rect.setHeight(height + blockHeight(categoryDisplay));
0126     option.rect = mapToViewport(option.rect);
0127 
0128     return option;
0129 }
0130 
0131 std::pair<QModelIndex, QModelIndex> KCategorizedViewPrivate::intersectingIndexesWithRect(const QRect &_rect) const
0132 {
0133     const int rowCount = proxyModel->rowCount();
0134 
0135     const QRect rect = _rect.normalized();
0136 
0137     // binary search to find out the top border
0138     int bottom = 0;
0139     int top = rowCount - 1;
0140     while (bottom <= top) {
0141         const int middle = (bottom + top) / 2;
0142         const QModelIndex index = proxyModel->index(middle, q->modelColumn(), q->rootIndex());
0143         const QRect itemRect = q->visualRect(index);
0144         if (itemRect.bottomRight().y() <= rect.topLeft().y()) {
0145             bottom = middle + 1;
0146         } else {
0147             top = middle - 1;
0148         }
0149     }
0150 
0151     const QModelIndex bottomIndex = proxyModel->index(bottom, q->modelColumn(), q->rootIndex());
0152 
0153     // binary search to find out the bottom border
0154     bottom = 0;
0155     top = rowCount - 1;
0156     while (bottom <= top) {
0157         const int middle = (bottom + top) / 2;
0158         const QModelIndex index = proxyModel->index(middle, q->modelColumn(), q->rootIndex());
0159         const QRect itemRect = q->visualRect(index);
0160         if (itemRect.topLeft().y() <= rect.bottomRight().y()) {
0161             bottom = middle + 1;
0162         } else {
0163             top = middle - 1;
0164         }
0165     }
0166 
0167     const QModelIndex topIndex = proxyModel->index(top, q->modelColumn(), q->rootIndex());
0168 
0169     return {bottomIndex, topIndex};
0170 }
0171 
0172 QPoint KCategorizedViewPrivate::blockPosition(const QString &category)
0173 {
0174     Block &block = blocks[category];
0175 
0176     if (block.outOfQuarantine && !block.topLeft.isNull()) {
0177         return block.topLeft;
0178     }
0179 
0180     QPoint res(categorySpacing, 0);
0181 
0182     const QModelIndex index = block.firstIndex;
0183 
0184     for (auto it = blocks.begin(); it != blocks.end(); ++it) {
0185         Block &block = *it;
0186         const QModelIndex categoryIndex = block.firstIndex;
0187         if (index.row() < categoryIndex.row()) {
0188             continue;
0189         }
0190 
0191         res.ry() += categoryDrawer->categoryHeight(categoryIndex, viewOpts()) + categorySpacing;
0192         if (index.row() == categoryIndex.row()) {
0193             continue;
0194         }
0195         res.ry() += blockHeight(it.key());
0196     }
0197 
0198     block.outOfQuarantine = true;
0199     block.topLeft = res;
0200 
0201     return res;
0202 }
0203 
0204 int KCategorizedViewPrivate::blockHeight(const QString &category)
0205 {
0206     Block &block = blocks[category];
0207 
0208     if (block.collapsed) {
0209         return 0;
0210     }
0211 
0212     if (block.height > -1) {
0213         return block.height;
0214     }
0215 
0216     const QModelIndex firstIndex = block.firstIndex;
0217     const QModelIndex lastIndex = proxyModel->index(firstIndex.row() + block.items.count() - 1, q->modelColumn(), q->rootIndex());
0218     const QRect topLeft = q->visualRect(firstIndex);
0219     QRect bottomRight = q->visualRect(lastIndex);
0220 
0221     if (hasGrid()) {
0222         bottomRight.setHeight(qMax(bottomRight.height(), q->gridSize().height()));
0223     } else {
0224         if (!q->uniformItemSizes()) {
0225             bottomRight.setHeight(highestElementInLastRow(block) + q->spacing() * 2);
0226         }
0227     }
0228 
0229     const int height = bottomRight.bottomRight().y() - topLeft.topLeft().y() + 1;
0230     block.height = height;
0231 
0232     return height;
0233 }
0234 
0235 int KCategorizedViewPrivate::viewportWidth() const
0236 {
0237     return q->viewport()->width() - categorySpacing * 2 - categoryDrawer->leftMargin() - categoryDrawer->rightMargin();
0238 }
0239 
0240 void KCategorizedViewPrivate::regenerateAllElements()
0241 {
0242     for (QHash<QString, Block>::Iterator it = blocks.begin(); it != blocks.end(); ++it) {
0243         Block &block = *it;
0244         block.outOfQuarantine = false;
0245         block.quarantineStart = block.firstIndex;
0246         block.height = -1;
0247     }
0248 }
0249 
0250 void KCategorizedViewPrivate::rowsInserted(const QModelIndex &parent, int start, int end)
0251 {
0252     if (!isCategorized()) {
0253         return;
0254     }
0255 
0256     for (int i = start; i <= end; ++i) {
0257         const QModelIndex index = proxyModel->index(i, q->modelColumn(), parent);
0258 
0259         Q_ASSERT(index.isValid());
0260 
0261         const QString category = categoryForIndex(index);
0262 
0263         Block &block = blocks[category];
0264 
0265         // BEGIN: update firstIndex
0266         // save as firstIndex in block if
0267         //     - it forced the category creation (first element on this category)
0268         //     - it is before the first row on that category
0269         const QModelIndex firstIndex = block.firstIndex;
0270         if (!firstIndex.isValid() || index.row() < firstIndex.row()) {
0271             block.firstIndex = index;
0272         }
0273         // END: update firstIndex
0274 
0275         Q_ASSERT(block.firstIndex.isValid());
0276 
0277         const int firstIndexRow = block.firstIndex.row();
0278 
0279         block.items.insert(index.row() - firstIndexRow, KCategorizedViewPrivate::Item());
0280         block.height = -1;
0281 
0282         q->visualRect(index);
0283         q->viewport()->update();
0284     }
0285 
0286     // BEGIN: update the items that are in quarantine in affected categories
0287     {
0288         const QModelIndex lastIndex = proxyModel->index(end, q->modelColumn(), parent);
0289         const QString category = categoryForIndex(lastIndex);
0290         KCategorizedViewPrivate::Block &block = blocks[category];
0291         block.quarantineStart = block.firstIndex;
0292     }
0293     // END: update the items that are in quarantine in affected categories
0294 
0295     // BEGIN: mark as in quarantine those categories that are under the affected ones
0296     {
0297         const QModelIndex firstIndex = proxyModel->index(start, q->modelColumn(), parent);
0298         const QString category = categoryForIndex(firstIndex);
0299         const QModelIndex firstAffectedCategory = blocks[category].firstIndex;
0300         // BEGIN: order for marking as alternate those blocks that are alternate
0301         QList<Block> blockList = blocks.values();
0302         std::sort(blockList.begin(), blockList.end(), Block::lessThan);
0303         QList<int> firstIndexesRows;
0304         for (const Block &block : std::as_const(blockList)) {
0305             firstIndexesRows << block.firstIndex.row();
0306         }
0307         // END: order for marking as alternate those blocks that are alternate
0308         for (auto it = blocks.begin(); it != blocks.end(); ++it) {
0309             KCategorizedViewPrivate::Block &block = *it;
0310             if (block.firstIndex.row() > firstAffectedCategory.row()) {
0311                 block.outOfQuarantine = false;
0312                 block.alternate = firstIndexesRows.indexOf(block.firstIndex.row()) % 2;
0313             } else if (block.firstIndex.row() == firstAffectedCategory.row()) {
0314                 block.alternate = firstIndexesRows.indexOf(block.firstIndex.row()) % 2;
0315             }
0316         }
0317     }
0318     // END: mark as in quarantine those categories that are under the affected ones
0319 }
0320 
0321 QRect KCategorizedViewPrivate::mapToViewport(const QRect &rect) const
0322 {
0323     const int dx = -q->horizontalOffset();
0324     const int dy = -q->verticalOffset();
0325     return rect.adjusted(dx, dy, dx, dy);
0326 }
0327 
0328 QRect KCategorizedViewPrivate::mapFromViewport(const QRect &rect) const
0329 {
0330     const int dx = q->horizontalOffset();
0331     const int dy = q->verticalOffset();
0332     return rect.adjusted(dx, dy, dx, dy);
0333 }
0334 
0335 int KCategorizedViewPrivate::highestElementInLastRow(const Block &block) const
0336 {
0337     // Find the highest element in the last row
0338     const QModelIndex lastIndex = proxyModel->index(block.firstIndex.row() + block.items.count() - 1, q->modelColumn(), q->rootIndex());
0339     const QRect prevRect = q->visualRect(lastIndex);
0340     int res = prevRect.height();
0341     QModelIndex prevIndex = proxyModel->index(lastIndex.row() - 1, q->modelColumn(), q->rootIndex());
0342     if (!prevIndex.isValid()) {
0343         return res;
0344     }
0345     Q_FOREVER {
0346         const QRect tempRect = q->visualRect(prevIndex);
0347         if (tempRect.topLeft().y() < prevRect.topLeft().y()) {
0348             break;
0349         }
0350         res = qMax(res, tempRect.height());
0351         if (prevIndex == block.firstIndex) {
0352             break;
0353         }
0354         prevIndex = proxyModel->index(prevIndex.row() - 1, q->modelColumn(), q->rootIndex());
0355     }
0356 
0357     return res;
0358 }
0359 
0360 bool KCategorizedViewPrivate::hasGrid() const
0361 {
0362     const QSize gridSize = q->gridSize();
0363     return gridSize.isValid() && !gridSize.isNull();
0364 }
0365 
0366 QString KCategorizedViewPrivate::categoryForIndex(const QModelIndex &index) const
0367 {
0368     const QModelIndex categoryIndex = index.model()->index(index.row(), proxyModel->sortColumn(), index.parent());
0369     return categoryIndex.data(KCategorizedSortFilterProxyModel::CategoryDisplayRole).toString();
0370 }
0371 
0372 void KCategorizedViewPrivate::leftToRightVisualRect(const QModelIndex &index, Item &item, const Block &block, const QPoint &blockPos) const
0373 {
0374     const int firstIndexRow = block.firstIndex.row();
0375 
0376     if (hasGrid()) {
0377         const int relativeRow = index.row() - firstIndexRow;
0378         const int maxItemsPerRow = qMax(viewportWidth() / q->gridSize().width(), 1);
0379         if (q->layoutDirection() == Qt::LeftToRight) {
0380             item.topLeft.rx() = (relativeRow % maxItemsPerRow) * q->gridSize().width() + blockPos.x() + categoryDrawer->leftMargin();
0381         } else {
0382             item.topLeft.rx() = viewportWidth() - ((relativeRow % maxItemsPerRow) + 1) * q->gridSize().width() + categoryDrawer->leftMargin() + categorySpacing;
0383         }
0384         item.topLeft.ry() = (relativeRow / maxItemsPerRow) * q->gridSize().height();
0385     } else {
0386         if (q->uniformItemSizes()) {
0387             const int relativeRow = index.row() - firstIndexRow;
0388             const QSize itemSize = q->sizeHintForIndex(index);
0389             const int maxItemsPerRow = qMax((viewportWidth() - q->spacing()) / (itemSize.width() + q->spacing()), 1);
0390             if (q->layoutDirection() == Qt::LeftToRight) {
0391                 item.topLeft.rx() = (relativeRow % maxItemsPerRow) * itemSize.width() + blockPos.x() + categoryDrawer->leftMargin();
0392             } else {
0393                 item.topLeft.rx() = viewportWidth() - (relativeRow % maxItemsPerRow) * itemSize.width() + categoryDrawer->leftMargin() + categorySpacing;
0394             }
0395             item.topLeft.ry() = (relativeRow / maxItemsPerRow) * itemSize.height();
0396         } else {
0397             const QSize currSize = q->sizeHintForIndex(index);
0398             if (index != block.firstIndex) {
0399                 const int viewportW = viewportWidth() - q->spacing();
0400                 QModelIndex prevIndex = proxyModel->index(index.row() - 1, q->modelColumn(), q->rootIndex());
0401                 QRect prevRect = q->visualRect(prevIndex);
0402                 prevRect = mapFromViewport(prevRect);
0403                 if ((prevRect.bottomRight().x() + 1) + currSize.width() - blockPos.x() + q->spacing() > viewportW) {
0404                     // we have to check the whole previous row, and see which one was the
0405                     // highest.
0406                     Q_FOREVER {
0407                         prevIndex = proxyModel->index(prevIndex.row() - 1, q->modelColumn(), q->rootIndex());
0408                         const QRect tempRect = q->visualRect(prevIndex);
0409                         if (tempRect.topLeft().y() < prevRect.topLeft().y()) {
0410                             break;
0411                         }
0412                         if (tempRect.bottomRight().y() > prevRect.bottomRight().y()) {
0413                             prevRect = tempRect;
0414                         }
0415                         if (prevIndex == block.firstIndex) {
0416                             break;
0417                         }
0418                     }
0419                     if (q->layoutDirection() == Qt::LeftToRight) {
0420                         item.topLeft.rx() = categoryDrawer->leftMargin() + blockPos.x() + q->spacing();
0421                     } else {
0422                         item.topLeft.rx() = viewportWidth() - currSize.width() + categoryDrawer->leftMargin() + categorySpacing;
0423                     }
0424                     item.topLeft.ry() = (prevRect.bottomRight().y() + 1) + q->spacing() - blockPos.y();
0425                 } else {
0426                     if (q->layoutDirection() == Qt::LeftToRight) {
0427                         item.topLeft.rx() = (prevRect.bottomRight().x() + 1) + q->spacing();
0428                     } else {
0429                         item.topLeft.rx() = (prevRect.bottomLeft().x() - 1) - q->spacing() - item.size.width() + categoryDrawer->leftMargin() + categorySpacing;
0430                     }
0431                     item.topLeft.ry() = prevRect.topLeft().y() - blockPos.y();
0432                 }
0433             } else {
0434                 if (q->layoutDirection() == Qt::LeftToRight) {
0435                     item.topLeft.rx() = blockPos.x() + categoryDrawer->leftMargin() + q->spacing();
0436                 } else {
0437                     item.topLeft.rx() = viewportWidth() - currSize.width() + categoryDrawer->leftMargin() + categorySpacing;
0438                 }
0439                 item.topLeft.ry() = q->spacing();
0440             }
0441         }
0442     }
0443     item.size = q->sizeHintForIndex(index);
0444 }
0445 
0446 void KCategorizedViewPrivate::topToBottomVisualRect(const QModelIndex &index, Item &item, const Block &block, const QPoint &blockPos) const
0447 {
0448     const int firstIndexRow = block.firstIndex.row();
0449 
0450     if (hasGrid()) {
0451         const int relativeRow = index.row() - firstIndexRow;
0452         item.topLeft.rx() = blockPos.x() + categoryDrawer->leftMargin();
0453         item.topLeft.ry() = relativeRow * q->gridSize().height();
0454     } else {
0455         if (q->uniformItemSizes()) {
0456             const int relativeRow = index.row() - firstIndexRow;
0457             const QSize itemSize = q->sizeHintForIndex(index);
0458             item.topLeft.rx() = blockPos.x() + categoryDrawer->leftMargin();
0459             item.topLeft.ry() = relativeRow * itemSize.height();
0460         } else {
0461             if (index != block.firstIndex) {
0462                 QModelIndex prevIndex = proxyModel->index(index.row() - 1, q->modelColumn(), q->rootIndex());
0463                 QRect prevRect = q->visualRect(prevIndex);
0464                 prevRect = mapFromViewport(prevRect);
0465                 item.topLeft.rx() = blockPos.x() + categoryDrawer->leftMargin() + q->spacing();
0466                 item.topLeft.ry() = (prevRect.bottomRight().y() + 1) + q->spacing() - blockPos.y();
0467             } else {
0468                 item.topLeft.rx() = blockPos.x() + categoryDrawer->leftMargin() + q->spacing();
0469                 item.topLeft.ry() = q->spacing();
0470             }
0471         }
0472     }
0473     item.size = q->sizeHintForIndex(index);
0474     item.size.setWidth(viewportWidth());
0475 }
0476 
0477 void KCategorizedViewPrivate::_k_slotCollapseOrExpandClicked(QModelIndex)
0478 {
0479 }
0480 
0481 // END: Private part
0482 
0483 // BEGIN: Public part
0484 
0485 KCategorizedView::KCategorizedView(QWidget *parent)
0486     : QListView(parent)
0487     , d(new KCategorizedViewPrivate(this))
0488 {
0489 }
0490 
0491 KCategorizedView::~KCategorizedView() = default;
0492 
0493 void KCategorizedView::setModel(QAbstractItemModel *model)
0494 {
0495     if (d->proxyModel == model) {
0496         return;
0497     }
0498 
0499     d->blocks.clear();
0500 
0501     if (d->proxyModel) {
0502         disconnect(d->proxyModel, SIGNAL(layoutChanged()), this, SLOT(slotLayoutChanged()));
0503     }
0504 
0505     d->proxyModel = dynamic_cast<KCategorizedSortFilterProxyModel *>(model);
0506 
0507     if (d->proxyModel) {
0508         connect(d->proxyModel, SIGNAL(layoutChanged()), this, SLOT(slotLayoutChanged()));
0509     }
0510 
0511     QListView::setModel(model);
0512 
0513     // if the model already had information inserted, update our data structures to it
0514     if (model->rowCount()) {
0515         slotLayoutChanged();
0516     }
0517 }
0518 
0519 void KCategorizedView::setGridSize(const QSize &size)
0520 {
0521     setGridSizeOwn(size);
0522 }
0523 
0524 void KCategorizedView::setGridSizeOwn(const QSize &size)
0525 {
0526     d->regenerateAllElements();
0527     QListView::setGridSize(size);
0528 }
0529 
0530 QRect KCategorizedView::visualRect(const QModelIndex &index) const
0531 {
0532     if (!d->isCategorized()) {
0533         return QListView::visualRect(index);
0534     }
0535 
0536     if (!index.isValid()) {
0537         return QRect();
0538     }
0539 
0540     const QString category = d->categoryForIndex(index);
0541 
0542     if (!d->blocks.contains(category)) {
0543         return QRect();
0544     }
0545 
0546     KCategorizedViewPrivate::Block &block = d->blocks[category];
0547     const int firstIndexRow = block.firstIndex.row();
0548 
0549     Q_ASSERT(block.firstIndex.isValid());
0550 
0551     if (index.row() - firstIndexRow < 0 || index.row() - firstIndexRow >= block.items.count()) {
0552         return QRect();
0553     }
0554 
0555     const QPoint blockPos = d->blockPosition(category);
0556 
0557     KCategorizedViewPrivate::Item &ritem = block.items[index.row() - firstIndexRow];
0558 
0559     if (ritem.topLeft.isNull() //
0560         || (block.quarantineStart.isValid() && index.row() >= block.quarantineStart.row())) {
0561         if (flow() == LeftToRight) {
0562             d->leftToRightVisualRect(index, ritem, block, blockPos);
0563         } else {
0564             d->topToBottomVisualRect(index, ritem, block, blockPos);
0565         }
0566 
0567         // BEGIN: update the quarantine start
0568         const bool wasLastIndex = (index.row() == (block.firstIndex.row() + block.items.count() - 1));
0569         if (index.row() == block.quarantineStart.row()) {
0570             if (wasLastIndex) {
0571                 block.quarantineStart = QModelIndex();
0572             } else {
0573                 const QModelIndex nextIndex = d->proxyModel->index(index.row() + 1, modelColumn(), rootIndex());
0574                 block.quarantineStart = nextIndex;
0575             }
0576         }
0577         // END: update the quarantine start
0578     }
0579 
0580     // we get now the absolute position through the relative position of the parent block. do not
0581     // save this on ritem, since this would override the item relative position in block terms.
0582     KCategorizedViewPrivate::Item item(ritem);
0583     item.topLeft.ry() += blockPos.y();
0584 
0585     const QSize sizeHint = item.size;
0586 
0587     if (d->hasGrid()) {
0588         const QSize sizeGrid = gridSize();
0589         const QSize resultingSize = sizeHint.boundedTo(sizeGrid);
0590         QRect res(item.topLeft.x() + ((sizeGrid.width() - resultingSize.width()) / 2), item.topLeft.y(), resultingSize.width(), resultingSize.height());
0591         if (block.collapsed) {
0592             // we can still do binary search, while we "hide" items. We move those items in collapsed
0593             // blocks to the left and set a 0 height.
0594             res.setLeft(-resultingSize.width());
0595             res.setHeight(0);
0596         }
0597         return d->mapToViewport(res);
0598     }
0599 
0600     QRect res(item.topLeft.x(), item.topLeft.y(), sizeHint.width(), sizeHint.height());
0601     if (block.collapsed) {
0602         // we can still do binary search, while we "hide" items. We move those items in collapsed
0603         // blocks to the left and set a 0 height.
0604         res.setLeft(-sizeHint.width());
0605         res.setHeight(0);
0606     }
0607     return d->mapToViewport(res);
0608 }
0609 
0610 KCategoryDrawer *KCategorizedView::categoryDrawer() const
0611 {
0612     return d->categoryDrawer;
0613 }
0614 
0615 void KCategorizedView::setCategoryDrawer(KCategoryDrawer *categoryDrawer)
0616 {
0617     if (d->categoryDrawer) {
0618         disconnect(d->categoryDrawer, SIGNAL(collapseOrExpandClicked(QModelIndex)), this, SLOT(_k_slotCollapseOrExpandClicked(QModelIndex)));
0619     }
0620 
0621     d->categoryDrawer = categoryDrawer;
0622 
0623     connect(d->categoryDrawer, SIGNAL(collapseOrExpandClicked(QModelIndex)), this, SLOT(_k_slotCollapseOrExpandClicked(QModelIndex)));
0624 }
0625 
0626 int KCategorizedView::categorySpacing() const
0627 {
0628     return d->categorySpacing;
0629 }
0630 
0631 void KCategorizedView::setCategorySpacing(int categorySpacing)
0632 {
0633     if (d->categorySpacing == categorySpacing) {
0634         return;
0635     }
0636 
0637     d->categorySpacing = categorySpacing;
0638 
0639     for (auto it = d->blocks.begin(); it != d->blocks.end(); ++it) {
0640         KCategorizedViewPrivate::Block &block = *it;
0641         block.outOfQuarantine = false;
0642     }
0643 }
0644 
0645 bool KCategorizedView::alternatingBlockColors() const
0646 {
0647     return d->alternatingBlockColors;
0648 }
0649 
0650 void KCategorizedView::setAlternatingBlockColors(bool enable)
0651 {
0652     d->alternatingBlockColors = enable;
0653 }
0654 
0655 bool KCategorizedView::collapsibleBlocks() const
0656 {
0657     return d->collapsibleBlocks;
0658 }
0659 
0660 void KCategorizedView::setCollapsibleBlocks(bool enable)
0661 {
0662     d->collapsibleBlocks = enable;
0663 }
0664 
0665 QModelIndexList KCategorizedView::block(const QString &category)
0666 {
0667     QModelIndexList res;
0668     const KCategorizedViewPrivate::Block &block = d->blocks[category];
0669     if (block.height == -1) {
0670         return res;
0671     }
0672     QModelIndex current = block.firstIndex;
0673     const int first = current.row();
0674     for (int i = 1; i <= block.items.count(); ++i) {
0675         if (current.isValid()) {
0676             res << current;
0677         }
0678         current = d->proxyModel->index(first + i, modelColumn(), rootIndex());
0679     }
0680     return res;
0681 }
0682 
0683 QModelIndexList KCategorizedView::block(const QModelIndex &representative)
0684 {
0685     return block(representative.data(KCategorizedSortFilterProxyModel::CategoryDisplayRole).toString());
0686 }
0687 
0688 QModelIndex KCategorizedView::indexAt(const QPoint &point) const
0689 {
0690     if (!d->isCategorized()) {
0691         return QListView::indexAt(point);
0692     }
0693 
0694     const int rowCount = d->proxyModel->rowCount();
0695     if (!rowCount) {
0696         return QModelIndex();
0697     }
0698 
0699     // Binary search that will try to spot if there is an index under point
0700     int bottom = 0;
0701     int top = rowCount - 1;
0702     while (bottom <= top) {
0703         const int middle = (bottom + top) / 2;
0704         const QModelIndex index = d->proxyModel->index(middle, modelColumn(), rootIndex());
0705         const QRect rect = visualRect(index);
0706         if (rect.contains(point)) {
0707             if (index.model()->flags(index) & Qt::ItemIsEnabled) {
0708                 return index;
0709             }
0710             return QModelIndex();
0711         }
0712         bool directionCondition;
0713         if (layoutDirection() == Qt::LeftToRight) {
0714             directionCondition = point.x() >= rect.bottomLeft().x();
0715         } else {
0716             directionCondition = point.x() <= rect.bottomRight().x();
0717         }
0718         if (point.y() < rect.topLeft().y()) {
0719             top = middle - 1;
0720         } else if (directionCondition) {
0721             bottom = middle + 1;
0722         } else if (point.y() <= rect.bottomRight().y()) {
0723             top = middle - 1;
0724         } else {
0725             bool after = true;
0726             for (int i = middle - 1; i >= bottom; i--) {
0727                 const QModelIndex newIndex = d->proxyModel->index(i, modelColumn(), rootIndex());
0728                 const QRect newRect = visualRect(newIndex);
0729                 if (newRect.topLeft().y() < rect.topLeft().y()) {
0730                     break;
0731                 } else if (newRect.contains(point)) {
0732                     if (newIndex.model()->flags(newIndex) & Qt::ItemIsEnabled) {
0733                         return newIndex;
0734                     }
0735                     return QModelIndex();
0736                     // clang-format off
0737                 } else if ((layoutDirection() == Qt::LeftToRight) ?
0738                            (newRect.topLeft().x() <= point.x()) :
0739                            (newRect.topRight().x() >= point.x())) {
0740                     // clang-format on
0741                     break;
0742                 } else if (newRect.bottomRight().y() >= point.y()) {
0743                     after = false;
0744                 }
0745             }
0746             if (!after) {
0747                 return QModelIndex();
0748             }
0749             bottom = middle + 1;
0750         }
0751     }
0752     return QModelIndex();
0753 }
0754 
0755 void KCategorizedView::reset()
0756 {
0757     d->blocks.clear();
0758     QListView::reset();
0759 }
0760 
0761 void KCategorizedView::paintEvent(QPaintEvent *event)
0762 {
0763     if (!d->isCategorized()) {
0764         QListView::paintEvent(event);
0765         return;
0766     }
0767 
0768     const std::pair<QModelIndex, QModelIndex> intersecting = d->intersectingIndexesWithRect(viewport()->rect().intersected(event->rect()));
0769 
0770     QPainter p(viewport());
0771     p.save();
0772 
0773     Q_ASSERT(selectionModel()->model() == d->proxyModel);
0774 
0775     // BEGIN: draw categories
0776     auto it = d->blocks.constBegin();
0777     while (it != d->blocks.constEnd()) {
0778         const KCategorizedViewPrivate::Block &block = *it;
0779         const QModelIndex categoryIndex = d->proxyModel->index(block.firstIndex.row(), d->proxyModel->sortColumn(), rootIndex());
0780 
0781         QStyleOptionViewItem option = d->viewOpts();
0782         option.features |= d->alternatingBlockColors && block.alternate //
0783             ? QStyleOptionViewItem::Alternate
0784             : QStyleOptionViewItem::None;
0785         option.state |= !d->collapsibleBlocks || !block.collapsed //
0786             ? QStyle::State_Open
0787             : QStyle::State_None;
0788         const int height = d->categoryDrawer->categoryHeight(categoryIndex, option);
0789         QPoint pos = d->blockPosition(it.key());
0790         pos.ry() -= height;
0791         option.rect.setTopLeft(pos);
0792         option.rect.setWidth(d->viewportWidth() + d->categoryDrawer->leftMargin() + d->categoryDrawer->rightMargin());
0793         option.rect.setHeight(height + d->blockHeight(it.key()));
0794         option.rect = d->mapToViewport(option.rect);
0795         if (!option.rect.intersects(viewport()->rect())) {
0796             ++it;
0797             continue;
0798         }
0799         d->categoryDrawer->drawCategory(categoryIndex, d->proxyModel->sortRole(), option, &p);
0800         ++it;
0801     }
0802     // END: draw categories
0803 
0804     if (intersecting.first.isValid() && intersecting.second.isValid()) {
0805         // BEGIN: draw items
0806         int i = intersecting.first.row();
0807         int indexToCheckIfBlockCollapsed = i;
0808         QModelIndex categoryIndex;
0809         QString category;
0810         KCategorizedViewPrivate::Block *block = nullptr;
0811         while (i <= intersecting.second.row()) {
0812             // BEGIN: first check if the block is collapsed. if so, we have to skip the item painting
0813             if (i == indexToCheckIfBlockCollapsed) {
0814                 categoryIndex = d->proxyModel->index(i, d->proxyModel->sortColumn(), rootIndex());
0815                 category = categoryIndex.data(KCategorizedSortFilterProxyModel::CategoryDisplayRole).toString();
0816                 block = &d->blocks[category];
0817                 indexToCheckIfBlockCollapsed = block->firstIndex.row() + block->items.count();
0818                 if (block->collapsed) {
0819                     i = indexToCheckIfBlockCollapsed;
0820                     continue;
0821                 }
0822             }
0823             // END: first check if the block is collapsed. if so, we have to skip the item painting
0824 
0825             Q_ASSERT(block);
0826 
0827             const bool alternateItem = (i - block->firstIndex.row()) % 2;
0828 
0829             const QModelIndex index = d->proxyModel->index(i, modelColumn(), rootIndex());
0830             const Qt::ItemFlags flags = d->proxyModel->flags(index);
0831             QStyleOptionViewItem option(d->viewOpts());
0832             option.rect = visualRect(index);
0833             option.widget = this;
0834             option.features |= wordWrap() ? QStyleOptionViewItem::WrapText : QStyleOptionViewItem::None;
0835             option.features |= alternatingRowColors() && alternateItem ? QStyleOptionViewItem::Alternate : QStyleOptionViewItem::None;
0836             if (flags & Qt::ItemIsSelectable) {
0837                 option.state |= selectionModel()->isSelected(index) ? QStyle::State_Selected : QStyle::State_None;
0838             } else {
0839                 option.state &= ~QStyle::State_Selected;
0840             }
0841             option.state |= (index == currentIndex()) ? QStyle::State_HasFocus : QStyle::State_None;
0842             if (!(flags & Qt::ItemIsEnabled)) {
0843                 option.state &= ~QStyle::State_Enabled;
0844             } else {
0845                 option.state |= (index == d->hoveredIndex) ? QStyle::State_MouseOver : QStyle::State_None;
0846             }
0847 
0848             itemDelegate(index)->paint(&p, option, index);
0849             ++i;
0850         }
0851         // END: draw items
0852     }
0853 
0854     // BEGIN: draw selection rect
0855     if (isSelectionRectVisible() && d->rubberBandRect.isValid()) {
0856         QStyleOptionRubberBand opt;
0857         opt.initFrom(this);
0858         opt.shape = QRubberBand::Rectangle;
0859         opt.opaque = false;
0860         opt.rect = d->mapToViewport(d->rubberBandRect).intersected(viewport()->rect().adjusted(-16, -16, 16, 16));
0861         p.save();
0862         style()->drawControl(QStyle::CE_RubberBand, &opt, &p);
0863         p.restore();
0864     }
0865     // END: draw selection rect
0866 
0867     p.restore();
0868 }
0869 
0870 void KCategorizedView::resizeEvent(QResizeEvent *event)
0871 {
0872     d->regenerateAllElements();
0873     QListView::resizeEvent(event);
0874 }
0875 
0876 void KCategorizedView::setSelection(const QRect &rect, QItemSelectionModel::SelectionFlags flags)
0877 {
0878     if (!d->isCategorized()) {
0879         QListView::setSelection(rect, flags);
0880         return;
0881     }
0882 
0883     if (rect.topLeft() == rect.bottomRight()) {
0884         const QModelIndex index = indexAt(rect.topLeft());
0885         selectionModel()->select(index, flags);
0886         return;
0887     }
0888 
0889     const std::pair<QModelIndex, QModelIndex> intersecting = d->intersectingIndexesWithRect(rect);
0890 
0891     QItemSelection selection;
0892 
0893     // TODO: think of a faster implementation
0894     QModelIndex firstIndex;
0895     QModelIndex lastIndex;
0896     for (int i = intersecting.first.row(); i <= intersecting.second.row(); ++i) {
0897         const QModelIndex index = d->proxyModel->index(i, modelColumn(), rootIndex());
0898         const bool visualRectIntersects = visualRect(index).intersects(rect);
0899         if (firstIndex.isValid()) {
0900             if (visualRectIntersects) {
0901                 lastIndex = index;
0902             } else {
0903                 selection << QItemSelectionRange(firstIndex, lastIndex);
0904                 firstIndex = QModelIndex();
0905             }
0906         } else if (visualRectIntersects) {
0907             firstIndex = index;
0908             lastIndex = index;
0909         }
0910     }
0911 
0912     if (firstIndex.isValid()) {
0913         selection << QItemSelectionRange(firstIndex, lastIndex);
0914     }
0915 
0916     selectionModel()->select(selection, flags);
0917 }
0918 
0919 void KCategorizedView::mouseMoveEvent(QMouseEvent *event)
0920 {
0921     QListView::mouseMoveEvent(event);
0922     d->hoveredIndex = indexAt(event->pos());
0923     const SelectionMode itemViewSelectionMode = selectionMode();
0924     if (state() == DragSelectingState //
0925         && isSelectionRectVisible() //
0926         && itemViewSelectionMode != SingleSelection //
0927         && itemViewSelectionMode != NoSelection) {
0928         QRect rect(d->pressedPosition, event->pos() + QPoint(horizontalOffset(), verticalOffset()));
0929         rect = rect.normalized();
0930         update(rect.united(d->rubberBandRect));
0931         d->rubberBandRect = rect;
0932     }
0933     if (!d->categoryDrawer) {
0934         return;
0935     }
0936     auto it = d->blocks.constBegin();
0937     while (it != d->blocks.constEnd()) {
0938         const KCategorizedViewPrivate::Block &block = *it;
0939         const QModelIndex categoryIndex = d->proxyModel->index(block.firstIndex.row(), d->proxyModel->sortColumn(), rootIndex());
0940         QStyleOptionViewItem option(d->viewOpts());
0941         const int height = d->categoryDrawer->categoryHeight(categoryIndex, option);
0942         QPoint pos = d->blockPosition(it.key());
0943         pos.ry() -= height;
0944         option.rect.setTopLeft(pos);
0945         option.rect.setWidth(d->viewportWidth() + d->categoryDrawer->leftMargin() + d->categoryDrawer->rightMargin());
0946         option.rect.setHeight(height + d->blockHeight(it.key()));
0947         option.rect = d->mapToViewport(option.rect);
0948         const QPoint mousePos = viewport()->mapFromGlobal(QCursor::pos());
0949         if (option.rect.contains(mousePos)) {
0950             if (d->hoveredBlock->height != -1 && *d->hoveredBlock != block) {
0951                 const QModelIndex categoryIndex = d->proxyModel->index(d->hoveredBlock->firstIndex.row(), d->proxyModel->sortColumn(), rootIndex());
0952                 const QStyleOptionViewItem option = d->blockRect(categoryIndex);
0953                 d->categoryDrawer->mouseLeft(categoryIndex, option.rect);
0954                 *d->hoveredBlock = block;
0955                 d->hoveredCategory = it.key();
0956                 viewport()->update(option.rect);
0957             } else if (d->hoveredBlock->height == -1) {
0958                 *d->hoveredBlock = block;
0959                 d->hoveredCategory = it.key();
0960             } else {
0961                 d->categoryDrawer->mouseMoved(categoryIndex, option.rect, event);
0962             }
0963             viewport()->update(option.rect);
0964             return;
0965         }
0966         ++it;
0967     }
0968     if (d->hoveredBlock->height != -1) {
0969         const QModelIndex categoryIndex = d->proxyModel->index(d->hoveredBlock->firstIndex.row(), d->proxyModel->sortColumn(), rootIndex());
0970         const QStyleOptionViewItem option = d->blockRect(categoryIndex);
0971         d->categoryDrawer->mouseLeft(categoryIndex, option.rect);
0972         *d->hoveredBlock = KCategorizedViewPrivate::Block();
0973         d->hoveredCategory = QString();
0974         viewport()->update(option.rect);
0975     }
0976 }
0977 
0978 void KCategorizedView::mousePressEvent(QMouseEvent *event)
0979 {
0980     if (event->button() == Qt::LeftButton) {
0981         d->pressedPosition = event->pos();
0982         d->pressedPosition.rx() += horizontalOffset();
0983         d->pressedPosition.ry() += verticalOffset();
0984     }
0985     if (!d->categoryDrawer) {
0986         QListView::mousePressEvent(event);
0987         return;
0988     }
0989     auto it = d->blocks.constBegin();
0990     while (it != d->blocks.constEnd()) {
0991         const KCategorizedViewPrivate::Block &block = *it;
0992         const QModelIndex categoryIndex = d->proxyModel->index(block.firstIndex.row(), d->proxyModel->sortColumn(), rootIndex());
0993         const QStyleOptionViewItem option = d->blockRect(categoryIndex);
0994         const QPoint mousePos = viewport()->mapFromGlobal(QCursor::pos());
0995         if (option.rect.contains(mousePos)) {
0996             d->categoryDrawer->mouseButtonPressed(categoryIndex, option.rect, event);
0997             viewport()->update(option.rect);
0998             if (!event->isAccepted()) {
0999                 QListView::mousePressEvent(event);
1000             }
1001             return;
1002         }
1003         ++it;
1004     }
1005     QListView::mousePressEvent(event);
1006 }
1007 
1008 void KCategorizedView::mouseReleaseEvent(QMouseEvent *event)
1009 {
1010     d->pressedPosition = QPoint();
1011     d->rubberBandRect = QRect();
1012     if (!d->categoryDrawer) {
1013         QListView::mouseReleaseEvent(event);
1014         return;
1015     }
1016     auto it = d->blocks.constBegin();
1017     while (it != d->blocks.constEnd()) {
1018         const KCategorizedViewPrivate::Block &block = *it;
1019         const QModelIndex categoryIndex = d->proxyModel->index(block.firstIndex.row(), d->proxyModel->sortColumn(), rootIndex());
1020         const QStyleOptionViewItem option = d->blockRect(categoryIndex);
1021         const QPoint mousePos = viewport()->mapFromGlobal(QCursor::pos());
1022         if (option.rect.contains(mousePos)) {
1023             d->categoryDrawer->mouseButtonReleased(categoryIndex, option.rect, event);
1024             viewport()->update(option.rect);
1025             if (!event->isAccepted()) {
1026                 QListView::mouseReleaseEvent(event);
1027             }
1028             return;
1029         }
1030         ++it;
1031     }
1032     QListView::mouseReleaseEvent(event);
1033 }
1034 
1035 void KCategorizedView::leaveEvent(QEvent *event)
1036 {
1037     QListView::leaveEvent(event);
1038     if (d->hoveredIndex.isValid()) {
1039         viewport()->update(visualRect(d->hoveredIndex));
1040         d->hoveredIndex = QModelIndex();
1041     }
1042     if (d->categoryDrawer && d->hoveredBlock->height != -1) {
1043         const QModelIndex categoryIndex = d->proxyModel->index(d->hoveredBlock->firstIndex.row(), d->proxyModel->sortColumn(), rootIndex());
1044         const QStyleOptionViewItem option = d->blockRect(categoryIndex);
1045         d->categoryDrawer->mouseLeft(categoryIndex, option.rect);
1046         *d->hoveredBlock = KCategorizedViewPrivate::Block();
1047         d->hoveredCategory = QString();
1048         viewport()->update(option.rect);
1049     }
1050 }
1051 
1052 void KCategorizedView::startDrag(Qt::DropActions supportedActions)
1053 {
1054     QListView::startDrag(supportedActions);
1055 }
1056 
1057 void KCategorizedView::dragMoveEvent(QDragMoveEvent *event)
1058 {
1059     QListView::dragMoveEvent(event);
1060     d->hoveredIndex = indexAt(event->pos());
1061 }
1062 
1063 void KCategorizedView::dragEnterEvent(QDragEnterEvent *event)
1064 {
1065     QListView::dragEnterEvent(event);
1066 }
1067 
1068 void KCategorizedView::dragLeaveEvent(QDragLeaveEvent *event)
1069 {
1070     QListView::dragLeaveEvent(event);
1071 }
1072 
1073 void KCategorizedView::dropEvent(QDropEvent *event)
1074 {
1075     QListView::dropEvent(event);
1076 }
1077 
1078 // TODO: improve se we take into account collapsed blocks
1079 // TODO: take into account when there is no grid and no uniformItemSizes
1080 QModelIndex KCategorizedView::moveCursor(CursorAction cursorAction, Qt::KeyboardModifiers modifiers)
1081 {
1082     if (!d->isCategorized() || viewMode() == QListView::ListMode) {
1083         return QListView::moveCursor(cursorAction, modifiers);
1084     }
1085 
1086     const QModelIndex current = currentIndex();
1087     const QRect currentRect = visualRect(current);
1088     if (!current.isValid()) {
1089         const int rowCount = d->proxyModel->rowCount(rootIndex());
1090         if (!rowCount) {
1091             return QModelIndex();
1092         }
1093         return d->proxyModel->index(0, modelColumn(), rootIndex());
1094     }
1095 
1096     switch (cursorAction) {
1097     case MoveLeft: {
1098         if (!current.row()) {
1099             return QModelIndex();
1100         }
1101         const QModelIndex previous = d->proxyModel->index(current.row() - 1, modelColumn(), rootIndex());
1102         const QRect previousRect = visualRect(previous);
1103         if (previousRect.top() == currentRect.top()) {
1104             return previous;
1105         }
1106 
1107         return QModelIndex();
1108     }
1109     case MoveRight: {
1110         if (current.row() == d->proxyModel->rowCount() - 1) {
1111             return QModelIndex();
1112         }
1113         const QModelIndex next = d->proxyModel->index(current.row() + 1, modelColumn(), rootIndex());
1114         const QRect nextRect = visualRect(next);
1115         if (nextRect.top() == currentRect.top()) {
1116             return next;
1117         }
1118 
1119         return QModelIndex();
1120     }
1121     case MoveDown: {
1122         if (d->hasGrid() || uniformItemSizes()) {
1123             const QModelIndex current = currentIndex();
1124             const QSize itemSize = d->hasGrid() ? gridSize() : sizeHintForIndex(current);
1125             const KCategorizedViewPrivate::Block &block = d->blocks[d->categoryForIndex(current)];
1126             const int maxItemsPerRow = qMax(d->viewportWidth() / itemSize.width(), 1);
1127             const bool canMove = current.row() + maxItemsPerRow < block.firstIndex.row() + block.items.count();
1128 
1129             if (canMove) {
1130                 return d->proxyModel->index(current.row() + maxItemsPerRow, modelColumn(), rootIndex());
1131             }
1132 
1133             const int currentRelativePos = (current.row() - block.firstIndex.row()) % maxItemsPerRow;
1134             const QModelIndex nextIndex = d->proxyModel->index(block.firstIndex.row() + block.items.count(), modelColumn(), rootIndex());
1135 
1136             if (!nextIndex.isValid()) {
1137                 return QModelIndex();
1138             }
1139 
1140             const KCategorizedViewPrivate::Block &nextBlock = d->blocks[d->categoryForIndex(nextIndex)];
1141 
1142             if (nextBlock.items.count() <= currentRelativePos) {
1143                 return QModelIndex();
1144             }
1145 
1146             if (currentRelativePos < (block.items.count() % maxItemsPerRow)) {
1147                 return d->proxyModel->index(nextBlock.firstIndex.row() + currentRelativePos, modelColumn(), rootIndex());
1148             }
1149         }
1150         return QModelIndex();
1151     }
1152     case MoveUp: {
1153         if (d->hasGrid() || uniformItemSizes()) {
1154             const QModelIndex current = currentIndex();
1155             const QSize itemSize = d->hasGrid() ? gridSize() : sizeHintForIndex(current);
1156             const KCategorizedViewPrivate::Block &block = d->blocks[d->categoryForIndex(current)];
1157             const int maxItemsPerRow = qMax(d->viewportWidth() / itemSize.width(), 1);
1158             const bool canMove = current.row() - maxItemsPerRow >= block.firstIndex.row();
1159 
1160             if (canMove) {
1161                 return d->proxyModel->index(current.row() - maxItemsPerRow, modelColumn(), rootIndex());
1162             }
1163 
1164             const int currentRelativePos = (current.row() - block.firstIndex.row()) % maxItemsPerRow;
1165             const QModelIndex prevIndex = d->proxyModel->index(block.firstIndex.row() - 1, modelColumn(), rootIndex());
1166 
1167             if (!prevIndex.isValid()) {
1168                 return QModelIndex();
1169             }
1170 
1171             const KCategorizedViewPrivate::Block &prevBlock = d->blocks[d->categoryForIndex(prevIndex)];
1172 
1173             if (prevBlock.items.count() <= currentRelativePos) {
1174                 return QModelIndex();
1175             }
1176 
1177             const int remainder = prevBlock.items.count() % maxItemsPerRow;
1178             if (currentRelativePos < remainder) {
1179                 return d->proxyModel->index(prevBlock.firstIndex.row() + prevBlock.items.count() - remainder + currentRelativePos, modelColumn(), rootIndex());
1180             }
1181 
1182             return QModelIndex();
1183         }
1184         break;
1185     }
1186     default:
1187         break;
1188     }
1189 
1190     return QModelIndex();
1191 }
1192 
1193 void KCategorizedView::rowsAboutToBeRemoved(const QModelIndex &parent, int start, int end)
1194 {
1195     if (!d->isCategorized()) {
1196         QListView::rowsAboutToBeRemoved(parent, start, end);
1197         return;
1198     }
1199 
1200     *d->hoveredBlock = KCategorizedViewPrivate::Block();
1201     d->hoveredCategory = QString();
1202 
1203     if (end - start + 1 == d->proxyModel->rowCount()) {
1204         d->blocks.clear();
1205         QListView::rowsAboutToBeRemoved(parent, start, end);
1206         return;
1207     }
1208 
1209     // Removal feels a bit more complicated than insertion. Basically we can consider there are
1210     // 3 different cases when going to remove items. (*) represents an item, Items between ([) and
1211     // (]) are the ones which are marked for removal.
1212     //
1213     // - 1st case:
1214     //              ... * * * * * * [ * * * ...
1215     //
1216     //   The items marked for removal are the last part of this category. No need to mark any item
1217     //   of this category as in quarantine, because no special offset will be pushed to items at
1218     //   the right because of any changes (since the removed items are those on the right most part
1219     //   of the category).
1220     //
1221     // - 2nd case:
1222     //              ... * * * * * * ] * * * ...
1223     //
1224     //   The items marked for removal are the first part of this category. We have to mark as in
1225     //   quarantine all items in this category. Absolutely all. All items will have to be moved to
1226     //   the left (or moving up, because rows got a different offset).
1227     //
1228     // - 3rd case:
1229     //              ... * * [ * * * * ] * * ...
1230     //
1231     //   The items marked for removal are in between of this category. We have to mark as in
1232     //   quarantine only those items that are at the right of the end of the removal interval,
1233     //   (starting on "]").
1234     //
1235     // It hasn't been explicitly said, but when we remove, we have to mark all blocks that are
1236     // located under the top most affected category as in quarantine (the block itself, as a whole),
1237     // because such a change can force it to have a different offset (note that items themselves
1238     // contain relative positions to the block, so marking the block as in quarantine is enough).
1239     //
1240     // Also note that removal implicitly means that we have to update correctly firstIndex of each
1241     // block, and in general keep updated the internal information of elements.
1242 
1243     QStringList listOfCategoriesMarkedForRemoval;
1244 
1245     QString lastCategory;
1246     int alreadyRemoved = 0;
1247     for (int i = start; i <= end; ++i) {
1248         const QModelIndex index = d->proxyModel->index(i, modelColumn(), parent);
1249 
1250         Q_ASSERT(index.isValid());
1251 
1252         const QString category = d->categoryForIndex(index);
1253 
1254         if (lastCategory != category) {
1255             lastCategory = category;
1256             alreadyRemoved = 0;
1257         }
1258 
1259         KCategorizedViewPrivate::Block &block = d->blocks[category];
1260         block.items.removeAt(i - block.firstIndex.row() - alreadyRemoved);
1261         ++alreadyRemoved;
1262 
1263         if (block.items.isEmpty()) {
1264             listOfCategoriesMarkedForRemoval << category;
1265         }
1266 
1267         block.height = -1;
1268 
1269         viewport()->update();
1270     }
1271 
1272     // BEGIN: update the items that are in quarantine in affected categories
1273     {
1274         const QModelIndex lastIndex = d->proxyModel->index(end, modelColumn(), parent);
1275         const QString category = d->categoryForIndex(lastIndex);
1276         KCategorizedViewPrivate::Block &block = d->blocks[category];
1277         if (!block.items.isEmpty() && start <= block.firstIndex.row() && end >= block.firstIndex.row()) {
1278             block.firstIndex = d->proxyModel->index(end + 1, modelColumn(), parent);
1279         }
1280         block.quarantineStart = block.firstIndex;
1281     }
1282     // END: update the items that are in quarantine in affected categories
1283 
1284     for (const QString &category : std::as_const(listOfCategoriesMarkedForRemoval)) {
1285         d->blocks.remove(category);
1286     }
1287 
1288     // BEGIN: mark as in quarantine those categories that are under the affected ones
1289     {
1290         // BEGIN: order for marking as alternate those blocks that are alternate
1291         QList<KCategorizedViewPrivate::Block> blockList = d->blocks.values();
1292         std::sort(blockList.begin(), blockList.end(), KCategorizedViewPrivate::Block::lessThan);
1293         QList<int> firstIndexesRows;
1294         for (const KCategorizedViewPrivate::Block &block : std::as_const(blockList)) {
1295             firstIndexesRows << block.firstIndex.row();
1296         }
1297         // END: order for marking as alternate those blocks that are alternate
1298         for (auto it = d->blocks.begin(); it != d->blocks.end(); ++it) {
1299             KCategorizedViewPrivate::Block &block = *it;
1300             if (block.firstIndex.row() > start) {
1301                 block.outOfQuarantine = false;
1302                 block.alternate = firstIndexesRows.indexOf(block.firstIndex.row()) % 2;
1303             } else if (block.firstIndex.row() == start) {
1304                 block.alternate = firstIndexesRows.indexOf(block.firstIndex.row()) % 2;
1305             }
1306         }
1307     }
1308     // END: mark as in quarantine those categories that are under the affected ones
1309 
1310     QListView::rowsAboutToBeRemoved(parent, start, end);
1311 }
1312 
1313 void KCategorizedView::updateGeometries()
1314 {
1315     const int oldVerticalOffset = verticalOffset();
1316     const Qt::ScrollBarPolicy verticalP = verticalScrollBarPolicy();
1317     const Qt::ScrollBarPolicy horizontalP = horizontalScrollBarPolicy();
1318 
1319     // BEGIN bugs 213068, 287847 ------------------------------------------------------------
1320     /*
1321      * QListView::updateGeometries() has it's own opinion on whether the scrollbars should be visible (valid range) or not
1322      * and triggers a (sometimes additionally timered) resize through ::layoutChildren()
1323      * http://qt.gitorious.org/qt/qt/blobs/4.7/src/gui/itemviews/qlistview.cpp#line1499
1324      * (the comment above the main block isn't all accurate, layoutChldren is called regardless of the policy)
1325      *
1326      * As a result QListView and KCategorizedView occasionally started a race on the scrollbar visibility, effectively blocking the UI
1327      * So we prevent QListView from having an own opinion on the scrollbar visibility by
1328      * fixing it before calling the baseclass QListView::updateGeometries()
1329      *
1330      * Since the implicit show/hide by the following range setting will cause further resizes if the policy is Qt::ScrollBarAsNeeded
1331      * we keep it static until we're done, then restore the original value and ultimately change the scrollbar visibility ourself.
1332      */
1333     if (d->isCategorized()) { // important! - otherwise we'd pollute the setting if the view is initially not categorized
1334         setVerticalScrollBarPolicy((verticalP == Qt::ScrollBarAlwaysOn || verticalScrollBar()->isVisibleTo(this)) ? Qt::ScrollBarAlwaysOn
1335                                                                                                                   : Qt::ScrollBarAlwaysOff);
1336         setHorizontalScrollBarPolicy((horizontalP == Qt::ScrollBarAlwaysOn || horizontalScrollBar()->isVisibleTo(this)) ? Qt::ScrollBarAlwaysOn
1337                                                                                                                         : Qt::ScrollBarAlwaysOff);
1338     }
1339     // END bugs 213068, 287847 --------------------------------------------------------------
1340 
1341     QListView::updateGeometries();
1342 
1343     if (!d->isCategorized()) {
1344         return;
1345     }
1346 
1347     const int rowCount = d->proxyModel->rowCount();
1348     if (!rowCount) {
1349         verticalScrollBar()->setRange(0, 0);
1350         // unconditional, see function end todo
1351         // BEGIN bugs 213068, 287847 ------------------------------------------------------------
1352         // restoring values from above ...
1353         horizontalScrollBar()->setRange(0, 0);
1354         setVerticalScrollBarPolicy(verticalP);
1355         setHorizontalScrollBarPolicy(horizontalP);
1356         // END bugs 213068, 287847 --------------------------------------------------------------
1357         return;
1358     }
1359 
1360     const QModelIndex lastIndex = d->proxyModel->index(rowCount - 1, modelColumn(), rootIndex());
1361     Q_ASSERT(lastIndex.isValid());
1362     QRect lastItemRect = visualRect(lastIndex);
1363 
1364     if (d->hasGrid()) {
1365         lastItemRect.setSize(lastItemRect.size().expandedTo(gridSize()));
1366     } else {
1367         if (uniformItemSizes()) {
1368             QSize itemSize = sizeHintForIndex(lastIndex);
1369             itemSize.setHeight(itemSize.height() + spacing());
1370             lastItemRect.setSize(itemSize);
1371         } else {
1372             QSize itemSize = sizeHintForIndex(lastIndex);
1373             const QString category = d->categoryForIndex(lastIndex);
1374             itemSize.setHeight(d->highestElementInLastRow(d->blocks[category]) + spacing());
1375             lastItemRect.setSize(itemSize);
1376         }
1377     }
1378 
1379     const int bottomRange = lastItemRect.bottomRight().y() + verticalOffset() - viewport()->height();
1380 
1381     if (verticalScrollMode() == ScrollPerItem) {
1382         verticalScrollBar()->setSingleStep(lastItemRect.height());
1383         const int rowsPerPage = qMax(viewport()->height() / lastItemRect.height(), 1);
1384         verticalScrollBar()->setPageStep(rowsPerPage * lastItemRect.height());
1385     }
1386 
1387     verticalScrollBar()->setRange(0, bottomRange);
1388     verticalScrollBar()->setValue(oldVerticalOffset);
1389 
1390     // TODO: also consider working with the horizontal scroll bar. since at this level I am not still
1391     //      supporting "top to bottom" flow, there is no real problem. If I support that someday
1392     //      (think how to draw categories), we would have to take care of the horizontal scroll bar too.
1393     //      In theory, as KCategorizedView has been designed, there is no need of horizontal scroll bar.
1394     horizontalScrollBar()->setRange(0, 0);
1395 
1396     // BEGIN bugs 213068, 287847 ------------------------------------------------------------
1397     // restoring values from above ...
1398     setVerticalScrollBarPolicy(verticalP);
1399     setHorizontalScrollBarPolicy(horizontalP);
1400     // ... and correct the visibility
1401     bool validRange = verticalScrollBar()->maximum() != verticalScrollBar()->minimum();
1402     if (verticalP == Qt::ScrollBarAsNeeded && (verticalScrollBar()->isVisibleTo(this) != validRange)) {
1403         verticalScrollBar()->setVisible(validRange);
1404     }
1405     validRange = horizontalScrollBar()->maximum() > horizontalScrollBar()->minimum();
1406     if (horizontalP == Qt::ScrollBarAsNeeded && (horizontalScrollBar()->isVisibleTo(this) != validRange)) {
1407         horizontalScrollBar()->setVisible(validRange);
1408     }
1409     // END bugs 213068, 287847 --------------------------------------------------------------
1410 }
1411 
1412 void KCategorizedView::currentChanged(const QModelIndex &current, const QModelIndex &previous)
1413 {
1414     QListView::currentChanged(current, previous);
1415 }
1416 
1417 void KCategorizedView::dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector<int> &roles)
1418 {
1419     QListView::dataChanged(topLeft, bottomRight, roles);
1420     if (!d->isCategorized()) {
1421         return;
1422     }
1423 
1424     *d->hoveredBlock = KCategorizedViewPrivate::Block();
1425     d->hoveredCategory = QString();
1426 
1427     // BEGIN: since the model changed data, we need to reconsider item sizes
1428     int i = topLeft.row();
1429     int indexToCheck = i;
1430     QModelIndex categoryIndex;
1431     QString category;
1432     KCategorizedViewPrivate::Block *block;
1433     while (i <= bottomRight.row()) {
1434         const QModelIndex currIndex = d->proxyModel->index(i, modelColumn(), rootIndex());
1435         if (i == indexToCheck) {
1436             categoryIndex = d->proxyModel->index(i, d->proxyModel->sortColumn(), rootIndex());
1437             category = categoryIndex.data(KCategorizedSortFilterProxyModel::CategoryDisplayRole).toString();
1438             block = &d->blocks[category];
1439             block->quarantineStart = currIndex;
1440             indexToCheck = block->firstIndex.row() + block->items.count();
1441         }
1442         visualRect(currIndex);
1443         ++i;
1444     }
1445     // END: since the model changed data, we need to reconsider item sizes
1446 }
1447 
1448 void KCategorizedView::rowsInserted(const QModelIndex &parent, int start, int end)
1449 {
1450     QListView::rowsInserted(parent, start, end);
1451     if (!d->isCategorized()) {
1452         return;
1453     }
1454 
1455     *d->hoveredBlock = KCategorizedViewPrivate::Block();
1456     d->hoveredCategory = QString();
1457     d->rowsInserted(parent, start, end);
1458 }
1459 
1460 #if KITEMVIEWS_BUILD_DEPRECATED_SINCE(4, 4)
1461 void KCategorizedView::rowsInsertedArtifficial(const QModelIndex &parent, int start, int end)
1462 {
1463     Q_UNUSED(parent);
1464     Q_UNUSED(start);
1465     Q_UNUSED(end);
1466 }
1467 #endif
1468 
1469 #if KITEMVIEWS_BUILD_DEPRECATED_SINCE(4, 4)
1470 void KCategorizedView::rowsRemoved(const QModelIndex &parent, int start, int end)
1471 {
1472     Q_UNUSED(parent);
1473     Q_UNUSED(start);
1474     Q_UNUSED(end);
1475 }
1476 #endif
1477 
1478 void KCategorizedView::slotLayoutChanged()
1479 {
1480     if (!d->isCategorized()) {
1481         return;
1482     }
1483 
1484     d->blocks.clear();
1485     *d->hoveredBlock = KCategorizedViewPrivate::Block();
1486     d->hoveredCategory = QString();
1487     if (d->proxyModel->rowCount()) {
1488         d->rowsInserted(rootIndex(), 0, d->proxyModel->rowCount() - 1);
1489     }
1490 }
1491 
1492 // END: Public part
1493 
1494 #include "moc_kcategorizedview.cpp"