File indexing completed on 2024-03-24 03:59:06

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