File indexing completed on 2024-12-08 06:40:58
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 ¤t, 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"