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 ¤t, 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"