1/*
2 This file is part of the KDE project
3 SPDX-FileCopyrightText: 2007, 2009 Rafael Fernández López <ereslibre@kde.org>
4
5 SPDX-License-Identifier: LGPL-2.0-or-later
6*/
7
8/**
9 * IMPLEMENTATION NOTES:
10 *
11 * QListView::setRowHidden() and QListView::isRowHidden() are not taken into
12 * account. This methods should actually not exist. This effect should be handled
13 * by an hypothetical QSortFilterProxyModel which filters out the desired rows.
14 *
15 * In case this needs to be implemented, contact me, but I consider this a faulty
16 * design.
17 */
18
19#include "kcategorizedview.h"
20#include "kcategorizedview_p.h"
21
22#include <QPaintEvent>
23#include <QPainter>
24#include <QScrollBar>
25
26#include <kitemviews_debug.h>
27
28#include "kcategorizedsortfilterproxymodel.h"
29#include "kcategorydrawer.h"
30
31// BEGIN: Private part
32
33struct KCategorizedViewPrivate::Item {
34 Item()
35 : topLeft(QPoint())
36 , size(QSize())
37 {
38 }
39
40 QPoint topLeft;
41 QSize size;
42};
43
44struct KCategorizedViewPrivate::Block {
45 Block()
46 : topLeft(QPoint())
47 , firstIndex(QModelIndex())
48 , quarantineStart(QModelIndex())
49 , items(QList<Item>())
50 {
51 }
52
53 bool operator!=(const Block &rhs) const
54 {
55 return firstIndex != rhs.firstIndex;
56 }
57
58 static bool lessThan(const Block &left, const Block &right)
59 {
60 Q_ASSERT(left.firstIndex.isValid());
61 Q_ASSERT(right.firstIndex.isValid());
62 return left.firstIndex.row() < right.firstIndex.row();
63 }
64
65 QPoint topLeft;
66 int height = -1;
67 QPersistentModelIndex firstIndex;
68 // if we have n elements on this block, and we inserted an element at position i. The quarantine
69 // will start at index (i, column, parent). This means that for all elements j where i <= j <= n, the
70 // visual rect position of item j will have to be recomputed (cannot use the cached point). The quarantine
71 // will only affect the current block, since the rest of blocks can be affected only in the way
72 // that the whole block will have different offset, but items will keep the same relative position
73 // in terms of their parent blocks.
74 QPersistentModelIndex quarantineStart;
75 QList<Item> items;
76
77 // this affects the whole block, not items separately. items contain the topLeft point relative
78 // to the block. Because of insertions or removals a whole block can be moved, so the whole block
79 // will enter in quarantine, what is faster than moving all items in absolute terms.
80 bool outOfQuarantine = false;
81
82 // should we alternate its color ? is just a hint, could not be used
83 bool alternate = false;
84 bool collapsed = false;
85};
86
87KCategorizedViewPrivate::KCategorizedViewPrivate(KCategorizedView *qq)
88 : q(qq)
89 , hoveredBlock(new Block())
90 , hoveredIndex(QModelIndex())
91 , pressedPosition(QPoint())
92 , rubberBandRect(QRect())
93{
94}
95
96KCategorizedViewPrivate::~KCategorizedViewPrivate()
97{
98 delete hoveredBlock;
99}
100
101bool KCategorizedViewPrivate::isCategorized() const
102{
103 return proxyModel && categoryDrawer && proxyModel->isCategorizedModel();
104}
105
106QStyleOptionViewItem KCategorizedViewPrivate::viewOpts()
107{
108 QStyleOptionViewItem option;
109 q->initViewItemOption(option: &option);
110 return option;
111}
112
113QStyleOptionViewItem KCategorizedViewPrivate::blockRect(const QModelIndex &representative)
114{
115 QStyleOptionViewItem option = viewOpts();
116
117 const int height = categoryDrawer->categoryHeight(index: representative, option);
118 const QString categoryDisplay = representative.data(arole: KCategorizedSortFilterProxyModel::CategoryDisplayRole).toString();
119 QPoint pos = blockPosition(category: categoryDisplay);
120 pos.ry() -= height;
121 option.rect.setTopLeft(pos);
122 option.rect.setWidth(viewportWidth() + categoryDrawer->leftMargin() + categoryDrawer->rightMargin());
123 option.rect.setHeight(height + blockHeight(category: categoryDisplay));
124 option.rect = mapToViewport(rect: option.rect);
125
126 return option;
127}
128
129std::pair<QModelIndex, QModelIndex> KCategorizedViewPrivate::intersectingIndexesWithRect(const QRect &_rect) const
130{
131 const int rowCount = proxyModel->rowCount();
132
133 const QRect rect = _rect.normalized();
134
135 // binary search to find out the top border
136 int bottom = 0;
137 int top = rowCount - 1;
138 while (bottom <= top) {
139 const int middle = (bottom + top) / 2;
140 const QModelIndex index = proxyModel->index(row: middle, column: q->modelColumn(), parent: q->rootIndex());
141 const QRect itemRect = q->visualRect(index);
142 if (itemRect.bottomRight().y() <= rect.topLeft().y()) {
143 bottom = middle + 1;
144 } else {
145 top = middle - 1;
146 }
147 }
148
149 const QModelIndex bottomIndex = proxyModel->index(row: bottom, column: q->modelColumn(), parent: q->rootIndex());
150
151 // binary search to find out the bottom border
152 bottom = 0;
153 top = rowCount - 1;
154 while (bottom <= top) {
155 const int middle = (bottom + top) / 2;
156 const QModelIndex index = proxyModel->index(row: middle, column: q->modelColumn(), parent: q->rootIndex());
157 const QRect itemRect = q->visualRect(index);
158 if (itemRect.topLeft().y() <= rect.bottomRight().y()) {
159 bottom = middle + 1;
160 } else {
161 top = middle - 1;
162 }
163 }
164
165 const QModelIndex topIndex = proxyModel->index(row: top, column: q->modelColumn(), parent: q->rootIndex());
166
167 return {bottomIndex, topIndex};
168}
169
170QPoint KCategorizedViewPrivate::blockPosition(const QString &category)
171{
172 Block &block = blocks[category];
173
174 if (block.outOfQuarantine && !block.topLeft.isNull()) {
175 return block.topLeft;
176 }
177
178 QPoint res(categorySpacing, 0);
179
180 const QModelIndex index = block.firstIndex;
181
182 for (auto it = blocks.begin(); it != blocks.end(); ++it) {
183 Block &block = *it;
184 const QModelIndex categoryIndex = block.firstIndex;
185 if (index.row() < categoryIndex.row()) {
186 continue;
187 }
188
189 res.ry() += categoryDrawer->categoryHeight(index: categoryIndex, option: viewOpts()) + categorySpacing;
190 if (index.row() == categoryIndex.row()) {
191 continue;
192 }
193 res.ry() += blockHeight(category: it.key());
194 }
195
196 block.outOfQuarantine = true;
197 block.topLeft = res;
198
199 return res;
200}
201
202int KCategorizedViewPrivate::blockHeight(const QString &category)
203{
204 Block &block = blocks[category];
205
206 if (block.collapsed) {
207 return 0;
208 }
209
210 if (block.height > -1) {
211 return block.height;
212 }
213
214 const QModelIndex firstIndex = block.firstIndex;
215 const QModelIndex lastIndex = proxyModel->index(row: firstIndex.row() + block.items.count() - 1, column: q->modelColumn(), parent: q->rootIndex());
216 const QRect topLeft = q->visualRect(index: firstIndex);
217 QRect bottomRight = q->visualRect(index: lastIndex);
218
219 if (hasGrid()) {
220 bottomRight.setHeight(qMax(a: bottomRight.height(), b: q->gridSize().height()));
221 } else {
222 if (!q->uniformItemSizes()) {
223 bottomRight.setHeight(highestElementInLastRow(block) + q->spacing() * 2);
224 }
225 }
226
227 const int height = bottomRight.bottomRight().y() - topLeft.topLeft().y() + 1;
228 block.height = height;
229
230 return height;
231}
232
233int KCategorizedViewPrivate::viewportWidth() const
234{
235 return q->viewport()->width() - categorySpacing * 2 - categoryDrawer->leftMargin() - categoryDrawer->rightMargin();
236}
237
238void KCategorizedViewPrivate::regenerateAllElements()
239{
240 for (QHash<QString, Block>::Iterator it = blocks.begin(); it != blocks.end(); ++it) {
241 Block &block = *it;
242 block.outOfQuarantine = false;
243 block.quarantineStart = block.firstIndex;
244 block.height = -1;
245 }
246}
247
248void KCategorizedViewPrivate::rowsInserted(const QModelIndex &parent, int start, int end)
249{
250 if (!isCategorized()) {
251 return;
252 }
253
254 for (int i = start; i <= end; ++i) {
255 const QModelIndex index = proxyModel->index(row: i, column: q->modelColumn(), parent);
256
257 Q_ASSERT(index.isValid());
258
259 const QString category = categoryForIndex(index);
260
261 Block &block = blocks[category];
262
263 // BEGIN: update firstIndex
264 // save as firstIndex in block if
265 // - it forced the category creation (first element on this category)
266 // - it is before the first row on that category
267 const QModelIndex firstIndex = block.firstIndex;
268 if (!firstIndex.isValid() || index.row() < firstIndex.row()) {
269 block.firstIndex = index;
270 }
271 // END: update firstIndex
272
273 Q_ASSERT(block.firstIndex.isValid());
274
275 const int firstIndexRow = block.firstIndex.row();
276
277 block.items.insert(i: index.row() - firstIndexRow, t: KCategorizedViewPrivate::Item());
278 block.height = -1;
279
280 q->visualRect(index);
281 q->viewport()->update();
282 }
283
284 // BEGIN: update the items that are in quarantine in affected categories
285 {
286 const QModelIndex lastIndex = proxyModel->index(row: end, column: q->modelColumn(), parent);
287 const QString category = categoryForIndex(index: lastIndex);
288 KCategorizedViewPrivate::Block &block = blocks[category];
289 block.quarantineStart = block.firstIndex;
290 }
291 // END: update the items that are in quarantine in affected categories
292
293 // BEGIN: mark as in quarantine those categories that are under the affected ones
294 {
295 const QModelIndex firstIndex = proxyModel->index(row: start, column: q->modelColumn(), parent);
296 const QString category = categoryForIndex(index: firstIndex);
297 const QModelIndex firstAffectedCategory = blocks[category].firstIndex;
298 // BEGIN: order for marking as alternate those blocks that are alternate
299 QList<Block> blockList = blocks.values();
300 std::sort(first: blockList.begin(), last: blockList.end(), comp: Block::lessThan);
301 QList<int> firstIndexesRows;
302 for (const Block &block : std::as_const(t&: blockList)) {
303 firstIndexesRows << block.firstIndex.row();
304 }
305 // END: order for marking as alternate those blocks that are alternate
306 for (auto it = blocks.begin(); it != blocks.end(); ++it) {
307 KCategorizedViewPrivate::Block &block = *it;
308 if (block.firstIndex.row() > firstAffectedCategory.row()) {
309 block.outOfQuarantine = false;
310 block.alternate = firstIndexesRows.indexOf(t: block.firstIndex.row()) % 2;
311 } else if (block.firstIndex.row() == firstAffectedCategory.row()) {
312 block.alternate = firstIndexesRows.indexOf(t: block.firstIndex.row()) % 2;
313 }
314 }
315 }
316 // END: mark as in quarantine those categories that are under the affected ones
317}
318
319QRect KCategorizedViewPrivate::mapToViewport(const QRect &rect) const
320{
321 const int dx = -q->horizontalOffset();
322 const int dy = -q->verticalOffset();
323 return rect.adjusted(xp1: dx, yp1: dy, xp2: dx, yp2: dy);
324}
325
326QRect KCategorizedViewPrivate::mapFromViewport(const QRect &rect) const
327{
328 const int dx = q->horizontalOffset();
329 const int dy = q->verticalOffset();
330 return rect.adjusted(xp1: dx, yp1: dy, xp2: dx, yp2: dy);
331}
332
333int KCategorizedViewPrivate::highestElementInLastRow(const Block &block) const
334{
335 // Find the highest element in the last row
336 const QModelIndex lastIndex = proxyModel->index(row: block.firstIndex.row() + block.items.count() - 1, column: q->modelColumn(), parent: q->rootIndex());
337 const QRect prevRect = q->visualRect(index: lastIndex);
338 int res = prevRect.height();
339 QModelIndex prevIndex = proxyModel->index(row: lastIndex.row() - 1, column: q->modelColumn(), parent: q->rootIndex());
340 if (!prevIndex.isValid()) {
341 return res;
342 }
343 Q_FOREVER {
344 const QRect tempRect = q->visualRect(index: prevIndex);
345 if (tempRect.topLeft().y() < prevRect.topLeft().y()) {
346 break;
347 }
348 res = qMax(a: res, b: tempRect.height());
349 if (prevIndex == block.firstIndex) {
350 break;
351 }
352 prevIndex = proxyModel->index(row: prevIndex.row() - 1, column: q->modelColumn(), parent: q->rootIndex());
353 }
354
355 return res;
356}
357
358bool KCategorizedViewPrivate::hasGrid() const
359{
360 const QSize gridSize = q->gridSize();
361 return gridSize.isValid() && !gridSize.isNull();
362}
363
364QString KCategorizedViewPrivate::categoryForIndex(const QModelIndex &index) const
365{
366 const auto indexModel = index.model();
367 if (!indexModel || !proxyModel) {
368 qCWarning(KITEMVIEWS_LOG) << "Index or view doesn't contain model";
369 return QString();
370 }
371
372 const QModelIndex categoryIndex = indexModel->index(row: index.row(), column: proxyModel->sortColumn(), parent: index.parent());
373 return categoryIndex.data(arole: KCategorizedSortFilterProxyModel::CategoryDisplayRole).toString();
374}
375
376void KCategorizedViewPrivate::leftToRightVisualRect(const QModelIndex &index, Item &item, const Block &block, const QPoint &blockPos) const
377{
378 const int firstIndexRow = block.firstIndex.row();
379
380 if (hasGrid()) {
381 const int relativeRow = index.row() - firstIndexRow;
382 const int maxItemsPerRow = qMax(a: viewportWidth() / q->gridSize().width(), b: 1);
383 if (q->layoutDirection() == Qt::LeftToRight) {
384 item.topLeft.rx() = (relativeRow % maxItemsPerRow) * q->gridSize().width() + blockPos.x() + categoryDrawer->leftMargin();
385 } else {
386 item.topLeft.rx() = viewportWidth() - ((relativeRow % maxItemsPerRow) + 1) * q->gridSize().width() + categoryDrawer->leftMargin() + categorySpacing;
387 }
388 item.topLeft.ry() = (relativeRow / maxItemsPerRow) * q->gridSize().height();
389 } else {
390 if (q->uniformItemSizes()) {
391 const int relativeRow = index.row() - firstIndexRow;
392 const QSize itemSize = q->sizeHintForIndex(index);
393 const int maxItemsPerRow = qMax(a: (viewportWidth() - q->spacing()) / (itemSize.width() + q->spacing()), b: 1);
394 if (q->layoutDirection() == Qt::LeftToRight) {
395 item.topLeft.rx() = (relativeRow % maxItemsPerRow) * itemSize.width() + blockPos.x() + categoryDrawer->leftMargin();
396 } else {
397 item.topLeft.rx() = viewportWidth() - (relativeRow % maxItemsPerRow) * itemSize.width() + categoryDrawer->leftMargin() + categorySpacing;
398 }
399 item.topLeft.ry() = (relativeRow / maxItemsPerRow) * itemSize.height();
400 } else {
401 const QSize currSize = q->sizeHintForIndex(index);
402 if (index != block.firstIndex) {
403 const int viewportW = viewportWidth() - q->spacing();
404 QModelIndex prevIndex = proxyModel->index(row: index.row() - 1, column: q->modelColumn(), parent: q->rootIndex());
405 QRect prevRect = q->visualRect(index: prevIndex);
406 prevRect = mapFromViewport(rect: prevRect);
407 if ((prevRect.bottomRight().x() + 1) + currSize.width() - blockPos.x() + q->spacing() > viewportW) {
408 // we have to check the whole previous row, and see which one was the
409 // highest.
410 Q_FOREVER {
411 prevIndex = proxyModel->index(row: prevIndex.row() - 1, column: q->modelColumn(), parent: q->rootIndex());
412 const QRect tempRect = q->visualRect(index: prevIndex);
413 if (tempRect.topLeft().y() < prevRect.topLeft().y()) {
414 break;
415 }
416 if (tempRect.bottomRight().y() > prevRect.bottomRight().y()) {
417 prevRect = tempRect;
418 }
419 if (prevIndex == block.firstIndex) {
420 break;
421 }
422 }
423 if (q->layoutDirection() == Qt::LeftToRight) {
424 item.topLeft.rx() = categoryDrawer->leftMargin() + blockPos.x() + q->spacing();
425 } else {
426 item.topLeft.rx() = viewportWidth() - currSize.width() + categoryDrawer->leftMargin() + categorySpacing;
427 }
428 item.topLeft.ry() = (prevRect.bottomRight().y() + 1) + q->spacing() - blockPos.y();
429 } else {
430 if (q->layoutDirection() == Qt::LeftToRight) {
431 item.topLeft.rx() = (prevRect.bottomRight().x() + 1) + q->spacing();
432 } else {
433 item.topLeft.rx() = (prevRect.bottomLeft().x() - 1) - q->spacing() - item.size.width() + categoryDrawer->leftMargin() + categorySpacing;
434 }
435 item.topLeft.ry() = prevRect.topLeft().y() - blockPos.y();
436 }
437 } else {
438 if (q->layoutDirection() == Qt::LeftToRight) {
439 item.topLeft.rx() = blockPos.x() + categoryDrawer->leftMargin() + q->spacing();
440 } else {
441 item.topLeft.rx() = viewportWidth() - currSize.width() + categoryDrawer->leftMargin() + categorySpacing;
442 }
443 item.topLeft.ry() = q->spacing();
444 }
445 }
446 }
447 item.size = q->sizeHintForIndex(index);
448}
449
450void KCategorizedViewPrivate::topToBottomVisualRect(const QModelIndex &index, Item &item, const Block &block, const QPoint &blockPos) const
451{
452 const int firstIndexRow = block.firstIndex.row();
453
454 if (hasGrid()) {
455 const int relativeRow = index.row() - firstIndexRow;
456 item.topLeft.rx() = blockPos.x() + categoryDrawer->leftMargin();
457 item.topLeft.ry() = relativeRow * q->gridSize().height();
458 } else {
459 if (q->uniformItemSizes()) {
460 const int relativeRow = index.row() - firstIndexRow;
461 const QSize itemSize = q->sizeHintForIndex(index);
462 item.topLeft.rx() = blockPos.x() + categoryDrawer->leftMargin();
463 item.topLeft.ry() = relativeRow * itemSize.height();
464 } else {
465 if (index != block.firstIndex) {
466 QModelIndex prevIndex = proxyModel->index(row: index.row() - 1, column: q->modelColumn(), parent: q->rootIndex());
467 QRect prevRect = q->visualRect(index: prevIndex);
468 prevRect = mapFromViewport(rect: prevRect);
469 item.topLeft.rx() = blockPos.x() + categoryDrawer->leftMargin() + q->spacing();
470 item.topLeft.ry() = (prevRect.bottomRight().y() + 1) + q->spacing() - blockPos.y();
471 } else {
472 item.topLeft.rx() = blockPos.x() + categoryDrawer->leftMargin() + q->spacing();
473 item.topLeft.ry() = q->spacing();
474 }
475 }
476 }
477 item.size = q->sizeHintForIndex(index);
478 item.size.setWidth(viewportWidth());
479}
480
481void KCategorizedViewPrivate::_k_slotCollapseOrExpandClicked(QModelIndex)
482{
483}
484
485// END: Private part
486
487// BEGIN: Public part
488
489KCategorizedView::KCategorizedView(QWidget *parent)
490 : QListView(parent)
491 , d(new KCategorizedViewPrivate(this))
492{
493}
494
495KCategorizedView::~KCategorizedView() = default;
496
497void KCategorizedView::setModel(QAbstractItemModel *model)
498{
499 if (d->proxyModel == model) {
500 return;
501 }
502
503 d->blocks.clear();
504
505 if (d->proxyModel) {
506 disconnect(sender: d->proxyModel, SIGNAL(layoutChanged()), receiver: this, SLOT(slotLayoutChanged()));
507 }
508
509 d->proxyModel = dynamic_cast<KCategorizedSortFilterProxyModel *>(model);
510
511 if (d->proxyModel) {
512 connect(sender: d->proxyModel, SIGNAL(layoutChanged()), receiver: this, SLOT(slotLayoutChanged()));
513 }
514
515 QListView::setModel(model);
516
517 // if the model already had information inserted, update our data structures to it
518 if (model && model->rowCount()) {
519 slotLayoutChanged();
520 }
521}
522
523void KCategorizedView::setGridSize(const QSize &size)
524{
525 setGridSizeOwn(size);
526}
527
528void KCategorizedView::setGridSizeOwn(const QSize &size)
529{
530 d->regenerateAllElements();
531 QListView::setGridSize(size);
532}
533
534QRect KCategorizedView::visualRect(const QModelIndex &index) const
535{
536 if (!d->isCategorized()) {
537 return QListView::visualRect(index);
538 }
539
540 if (!index.isValid()) {
541 return QRect();
542 }
543
544 const QString category = d->categoryForIndex(index);
545
546 if (!d->blocks.contains(key: category)) {
547 return QRect();
548 }
549
550 KCategorizedViewPrivate::Block &block = d->blocks[category];
551 const int firstIndexRow = block.firstIndex.row();
552
553 Q_ASSERT(block.firstIndex.isValid());
554
555 if (index.row() - firstIndexRow < 0 || index.row() - firstIndexRow >= block.items.count()) {
556 return QRect();
557 }
558
559 const QPoint blockPos = d->blockPosition(category);
560
561 KCategorizedViewPrivate::Item &ritem = block.items[index.row() - firstIndexRow];
562
563 if (ritem.topLeft.isNull() //
564 || (block.quarantineStart.isValid() && index.row() >= block.quarantineStart.row())) {
565 if (flow() == LeftToRight) {
566 d->leftToRightVisualRect(index, item&: ritem, block, blockPos);
567 } else {
568 d->topToBottomVisualRect(index, item&: ritem, block, blockPos);
569 }
570
571 // BEGIN: update the quarantine start
572 const bool wasLastIndex = (index.row() == (block.firstIndex.row() + block.items.count() - 1));
573 if (index.row() == block.quarantineStart.row()) {
574 if (wasLastIndex) {
575 block.quarantineStart = QModelIndex();
576 } else {
577 const QModelIndex nextIndex = d->proxyModel->index(row: index.row() + 1, column: modelColumn(), parent: rootIndex());
578 block.quarantineStart = nextIndex;
579 }
580 }
581 // END: update the quarantine start
582 }
583
584 // we get now the absolute position through the relative position of the parent block. do not
585 // save this on ritem, since this would override the item relative position in block terms.
586 KCategorizedViewPrivate::Item item(ritem);
587 item.topLeft.ry() += blockPos.y();
588
589 const QSize sizeHint = item.size;
590
591 if (d->hasGrid()) {
592 const QSize sizeGrid = gridSize();
593 const QSize resultingSize = sizeHint.boundedTo(otherSize: sizeGrid);
594 QRect res(item.topLeft.x() + ((sizeGrid.width() - resultingSize.width()) / 2), item.topLeft.y(), resultingSize.width(), resultingSize.height());
595 if (block.collapsed) {
596 // we can still do binary search, while we "hide" items. We move those items in collapsed
597 // blocks to the left and set a 0 height.
598 res.setLeft(-resultingSize.width());
599 res.setHeight(0);
600 }
601 return d->mapToViewport(rect: res);
602 }
603
604 QRect res(item.topLeft.x(), item.topLeft.y(), sizeHint.width(), sizeHint.height());
605 if (block.collapsed) {
606 // we can still do binary search, while we "hide" items. We move those items in collapsed
607 // blocks to the left and set a 0 height.
608 res.setLeft(-sizeHint.width());
609 res.setHeight(0);
610 }
611 return d->mapToViewport(rect: res);
612}
613
614KCategoryDrawer *KCategorizedView::categoryDrawer() const
615{
616 return d->categoryDrawer;
617}
618
619void KCategorizedView::setCategoryDrawer(KCategoryDrawer *categoryDrawer)
620{
621 if (d->categoryDrawer) {
622 disconnect(sender: d->categoryDrawer, SIGNAL(collapseOrExpandClicked(QModelIndex)), receiver: this, SLOT(_k_slotCollapseOrExpandClicked(QModelIndex)));
623 }
624
625 d->categoryDrawer = categoryDrawer;
626
627 connect(sender: d->categoryDrawer, SIGNAL(collapseOrExpandClicked(QModelIndex)), receiver: this, SLOT(_k_slotCollapseOrExpandClicked(QModelIndex)));
628}
629
630int KCategorizedView::categorySpacing() const
631{
632 return d->categorySpacing;
633}
634
635void KCategorizedView::setCategorySpacing(int categorySpacing)
636{
637 if (d->categorySpacing == categorySpacing) {
638 return;
639 }
640
641 d->categorySpacing = categorySpacing;
642
643 for (auto it = d->blocks.begin(); it != d->blocks.end(); ++it) {
644 KCategorizedViewPrivate::Block &block = *it;
645 block.outOfQuarantine = false;
646 }
647 Q_EMIT categorySpacingChanged(spacing: d->categorySpacing);
648}
649
650bool KCategorizedView::alternatingBlockColors() const
651{
652 return d->alternatingBlockColors;
653}
654
655void KCategorizedView::setAlternatingBlockColors(bool enable)
656{
657 if (d->alternatingBlockColors == enable) {
658 return;
659 }
660
661 d->alternatingBlockColors = enable;
662 Q_EMIT alternatingBlockColorsChanged(enable: d->alternatingBlockColors);
663}
664
665bool KCategorizedView::collapsibleBlocks() const
666{
667 return d->collapsibleBlocks;
668}
669
670void KCategorizedView::setCollapsibleBlocks(bool enable)
671{
672 if (d->collapsibleBlocks == enable) {
673 return;
674 }
675
676 d->collapsibleBlocks = enable;
677 Q_EMIT collapsibleBlocksChanged(enable: d->collapsibleBlocks);
678}
679
680QModelIndexList KCategorizedView::block(const QString &category)
681{
682 QModelIndexList res;
683 const KCategorizedViewPrivate::Block &block = d->blocks[category];
684 if (block.height == -1) {
685 return res;
686 }
687 QModelIndex current = block.firstIndex;
688 const int first = current.row();
689 for (int i = 1; i <= block.items.count(); ++i) {
690 if (current.isValid()) {
691 res << current;
692 }
693 current = d->proxyModel->index(row: first + i, column: modelColumn(), parent: rootIndex());
694 }
695 return res;
696}
697
698QModelIndexList KCategorizedView::block(const QModelIndex &representative)
699{
700 return block(category: representative.data(arole: KCategorizedSortFilterProxyModel::CategoryDisplayRole).toString());
701}
702
703QModelIndex KCategorizedView::indexAt(const QPoint &point) const
704{
705 if (!d->isCategorized()) {
706 return QListView::indexAt(p: point);
707 }
708
709 const int rowCount = d->proxyModel->rowCount();
710 if (!rowCount) {
711 return QModelIndex();
712 }
713
714 // Binary search that will try to spot if there is an index under point
715 int bottom = 0;
716 int top = rowCount - 1;
717 while (bottom <= top) {
718 const int middle = (bottom + top) / 2;
719 const QModelIndex index = d->proxyModel->index(row: middle, column: modelColumn(), parent: rootIndex());
720 const QRect rect = visualRect(index);
721 if (rect.contains(p: point)) {
722 if (index.model()->flags(index) & Qt::ItemIsEnabled) {
723 return index;
724 }
725 return QModelIndex();
726 }
727 bool directionCondition;
728 if (layoutDirection() == Qt::LeftToRight) {
729 directionCondition = point.x() >= rect.bottomLeft().x();
730 } else {
731 directionCondition = point.x() <= rect.bottomRight().x();
732 }
733 if (point.y() < rect.topLeft().y()) {
734 top = middle - 1;
735 } else if (directionCondition) {
736 bottom = middle + 1;
737 } else if (point.y() <= rect.bottomRight().y()) {
738 top = middle - 1;
739 } else {
740 bool after = true;
741 for (int i = middle - 1; i >= bottom; i--) {
742 const QModelIndex newIndex = d->proxyModel->index(row: i, column: modelColumn(), parent: rootIndex());
743 const QRect newRect = visualRect(index: newIndex);
744 if (newRect.topLeft().y() < rect.topLeft().y()) {
745 break;
746 } else if (newRect.contains(p: point)) {
747 if (newIndex.model()->flags(index: newIndex) & Qt::ItemIsEnabled) {
748 return newIndex;
749 }
750 return QModelIndex();
751 // clang-format off
752 } else if ((layoutDirection() == Qt::LeftToRight) ?
753 (newRect.topLeft().x() <= point.x()) :
754 (newRect.topRight().x() >= point.x())) {
755 // clang-format on
756 break;
757 } else if (newRect.bottomRight().y() >= point.y()) {
758 after = false;
759 }
760 }
761 if (!after) {
762 return QModelIndex();
763 }
764 bottom = middle + 1;
765 }
766 }
767 return QModelIndex();
768}
769
770void KCategorizedView::reset()
771{
772 d->blocks.clear();
773 QListView::reset();
774}
775
776void KCategorizedView::paintEvent(QPaintEvent *event)
777{
778 if (!d->isCategorized()) {
779 QListView::paintEvent(e: event);
780 return;
781 }
782
783 const std::pair<QModelIndex, QModelIndex> intersecting = d->intersectingIndexesWithRect(rect: viewport()->rect().intersected(other: event->rect()));
784
785 QPainter p(viewport());
786 p.save();
787
788 Q_ASSERT(selectionModel()->model() == d->proxyModel);
789
790 // BEGIN: draw categories
791 auto it = d->blocks.constBegin();
792 while (it != d->blocks.constEnd()) {
793 const KCategorizedViewPrivate::Block &block = *it;
794 const QModelIndex categoryIndex = d->proxyModel->index(row: block.firstIndex.row(), column: d->proxyModel->sortColumn(), parent: rootIndex());
795
796 QStyleOptionViewItem option = d->viewOpts();
797 option.features |= d->alternatingBlockColors && block.alternate //
798 ? QStyleOptionViewItem::Alternate
799 : QStyleOptionViewItem::None;
800 option.state |= !d->collapsibleBlocks || !block.collapsed //
801 ? QStyle::State_Open
802 : QStyle::State_None;
803 const int height = d->categoryDrawer->categoryHeight(index: categoryIndex, option);
804 QPoint pos = d->blockPosition(category: it.key());
805 pos.ry() -= height;
806 option.rect.setTopLeft(pos);
807 option.rect.setWidth(d->viewportWidth() + d->categoryDrawer->leftMargin() + d->categoryDrawer->rightMargin());
808 option.rect.setHeight(height + d->blockHeight(category: it.key()));
809 option.rect = d->mapToViewport(rect: option.rect);
810 if (!option.rect.intersects(r: viewport()->rect())) {
811 ++it;
812 continue;
813 }
814 d->categoryDrawer->drawCategory(index: categoryIndex, sortRole: d->proxyModel->sortRole(), option, painter: &p);
815 ++it;
816 }
817 // END: draw categories
818
819 if (intersecting.first.isValid() && intersecting.second.isValid()) {
820 // BEGIN: draw items
821 int i = intersecting.first.row();
822 int indexToCheckIfBlockCollapsed = i;
823 QModelIndex categoryIndex;
824 QString category;
825 KCategorizedViewPrivate::Block *block = nullptr;
826 while (i <= intersecting.second.row()) {
827 // BEGIN: first check if the block is collapsed. if so, we have to skip the item painting
828 if (i == indexToCheckIfBlockCollapsed) {
829 categoryIndex = d->proxyModel->index(row: i, column: d->proxyModel->sortColumn(), parent: rootIndex());
830 category = categoryIndex.data(arole: KCategorizedSortFilterProxyModel::CategoryDisplayRole).toString();
831 block = &d->blocks[category];
832 indexToCheckIfBlockCollapsed = block->firstIndex.row() + block->items.count();
833 if (block->collapsed) {
834 i = indexToCheckIfBlockCollapsed;
835 continue;
836 }
837 }
838 // END: first check if the block is collapsed. if so, we have to skip the item painting
839
840 Q_ASSERT(block);
841
842 const bool alternateItem = (i - block->firstIndex.row()) % 2;
843
844 const QModelIndex index = d->proxyModel->index(row: i, column: modelColumn(), parent: rootIndex());
845 const Qt::ItemFlags flags = d->proxyModel->flags(index);
846 QStyleOptionViewItem option(d->viewOpts());
847 option.rect = visualRect(index);
848 option.widget = this;
849 option.features |= wordWrap() ? QStyleOptionViewItem::WrapText : QStyleOptionViewItem::None;
850 option.features |= alternatingRowColors() && alternateItem ? QStyleOptionViewItem::Alternate : QStyleOptionViewItem::None;
851 if (flags & Qt::ItemIsSelectable) {
852 option.state |= selectionModel()->isSelected(index) ? QStyle::State_Selected : QStyle::State_None;
853 } else {
854 option.state &= ~QStyle::State_Selected;
855 }
856 option.state |= (index == currentIndex()) ? QStyle::State_HasFocus : QStyle::State_None;
857 if (!(flags & Qt::ItemIsEnabled)) {
858 option.state &= ~QStyle::State_Enabled;
859 } else {
860 option.state |= (index == d->hoveredIndex) ? QStyle::State_MouseOver : QStyle::State_None;
861 }
862
863 itemDelegateForIndex(index)->paint(painter: &p, option, index);
864 ++i;
865 }
866 // END: draw items
867 }
868
869 // BEGIN: draw selection rect
870 if (isSelectionRectVisible() && d->rubberBandRect.isValid()) {
871 QStyleOptionRubberBand opt;
872 opt.initFrom(w: this);
873 opt.shape = QRubberBand::Rectangle;
874 opt.opaque = false;
875 opt.rect = d->mapToViewport(rect: d->rubberBandRect).intersected(other: viewport()->rect().adjusted(xp1: -16, yp1: -16, xp2: 16, yp2: 16));
876 p.save();
877 style()->drawControl(element: QStyle::CE_RubberBand, opt: &opt, p: &p);
878 p.restore();
879 }
880 // END: draw selection rect
881
882 p.restore();
883}
884
885void KCategorizedView::resizeEvent(QResizeEvent *event)
886{
887 d->regenerateAllElements();
888 QListView::resizeEvent(e: event);
889}
890
891void KCategorizedView::setSelection(const QRect &rect, QItemSelectionModel::SelectionFlags flags)
892{
893 if (!d->isCategorized()) {
894 QListView::setSelection(rect, command: flags);
895 return;
896 }
897
898 if (rect.topLeft() == rect.bottomRight()) {
899 const QModelIndex index = indexAt(point: rect.topLeft());
900 selectionModel()->select(index, command: flags);
901 return;
902 }
903
904 const std::pair<QModelIndex, QModelIndex> intersecting = d->intersectingIndexesWithRect(rect: rect);
905
906 QItemSelection selection;
907
908 // TODO: think of a faster implementation
909 QModelIndex firstIndex;
910 QModelIndex lastIndex;
911 for (int i = intersecting.first.row(); i <= intersecting.second.row(); ++i) {
912 const QModelIndex index = d->proxyModel->index(row: i, column: modelColumn(), parent: rootIndex());
913 const bool visualRectIntersects = visualRect(index).intersects(r: rect);
914 if (firstIndex.isValid()) {
915 if (visualRectIntersects) {
916 lastIndex = index;
917 } else {
918 selection << QItemSelectionRange(firstIndex, lastIndex);
919 firstIndex = QModelIndex();
920 }
921 } else if (visualRectIntersects) {
922 firstIndex = index;
923 lastIndex = index;
924 }
925 }
926
927 if (firstIndex.isValid()) {
928 selection << QItemSelectionRange(firstIndex, lastIndex);
929 }
930
931 selectionModel()->select(selection, command: flags);
932}
933
934void KCategorizedView::mouseMoveEvent(QMouseEvent *event)
935{
936 QListView::mouseMoveEvent(e: event);
937 d->hoveredIndex = indexAt(point: event->pos());
938 const SelectionMode itemViewSelectionMode = selectionMode();
939 if (state() == DragSelectingState //
940 && isSelectionRectVisible() //
941 && itemViewSelectionMode != SingleSelection //
942 && itemViewSelectionMode != NoSelection) {
943 QRect rect(d->pressedPosition, event->pos() + QPoint(horizontalOffset(), verticalOffset()));
944 rect = rect.normalized();
945 update(rect.united(r: d->rubberBandRect));
946 d->rubberBandRect = rect;
947 }
948 if (!d->categoryDrawer) {
949 return;
950 }
951 auto it = d->blocks.constBegin();
952 while (it != d->blocks.constEnd()) {
953 const KCategorizedViewPrivate::Block &block = *it;
954 const QModelIndex categoryIndex = d->proxyModel->index(row: block.firstIndex.row(), column: d->proxyModel->sortColumn(), parent: rootIndex());
955 QStyleOptionViewItem option(d->viewOpts());
956 const int height = d->categoryDrawer->categoryHeight(index: categoryIndex, option);
957 QPoint pos = d->blockPosition(category: it.key());
958 pos.ry() -= height;
959 option.rect.setTopLeft(pos);
960 option.rect.setWidth(d->viewportWidth() + d->categoryDrawer->leftMargin() + d->categoryDrawer->rightMargin());
961 option.rect.setHeight(height + d->blockHeight(category: it.key()));
962 option.rect = d->mapToViewport(rect: option.rect);
963 const QPoint mousePos = viewport()->mapFromGlobal(QCursor::pos());
964 if (option.rect.contains(p: mousePos)) {
965 if (d->hoveredBlock->height != -1 && *d->hoveredBlock != block) {
966 const QModelIndex categoryIndex = d->proxyModel->index(row: d->hoveredBlock->firstIndex.row(), column: d->proxyModel->sortColumn(), parent: rootIndex());
967 const QStyleOptionViewItem option = d->blockRect(representative: categoryIndex);
968 d->categoryDrawer->mouseLeft(index: categoryIndex, blockRect: option.rect);
969 *d->hoveredBlock = block;
970 d->hoveredCategory = it.key();
971 viewport()->update(option.rect);
972 } else if (d->hoveredBlock->height == -1) {
973 *d->hoveredBlock = block;
974 d->hoveredCategory = it.key();
975 } else {
976 d->categoryDrawer->mouseMoved(index: categoryIndex, blockRect: option.rect, event);
977 }
978 viewport()->update(option.rect);
979 return;
980 }
981 ++it;
982 }
983 if (d->hoveredBlock->height != -1) {
984 const QModelIndex categoryIndex = d->proxyModel->index(row: d->hoveredBlock->firstIndex.row(), column: d->proxyModel->sortColumn(), parent: rootIndex());
985 const QStyleOptionViewItem option = d->blockRect(representative: categoryIndex);
986 d->categoryDrawer->mouseLeft(index: categoryIndex, blockRect: option.rect);
987 *d->hoveredBlock = KCategorizedViewPrivate::Block();
988 d->hoveredCategory = QString();
989 viewport()->update(option.rect);
990 }
991}
992
993void KCategorizedView::mousePressEvent(QMouseEvent *event)
994{
995 if (event->button() == Qt::LeftButton) {
996 d->pressedPosition = event->pos();
997 d->pressedPosition.rx() += horizontalOffset();
998 d->pressedPosition.ry() += verticalOffset();
999 }
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(row: block.firstIndex.row(), column: d->proxyModel->sortColumn(), parent: rootIndex());
1008 const QStyleOptionViewItem option = d->blockRect(representative: categoryIndex);
1009 const QPoint mousePos = viewport()->mapFromGlobal(QCursor::pos());
1010 if (option.rect.contains(p: mousePos)) {
1011 d->categoryDrawer->mouseButtonPressed(index: categoryIndex, blockRect: 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
1023void KCategorizedView::mouseReleaseEvent(QMouseEvent *event)
1024{
1025 d->pressedPosition = QPoint();
1026 d->rubberBandRect = QRect();
1027 if (!d->categoryDrawer) {
1028 QListView::mouseReleaseEvent(e: 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(row: block.firstIndex.row(), column: d->proxyModel->sortColumn(), parent: rootIndex());
1035 const QStyleOptionViewItem option = d->blockRect(representative: categoryIndex);
1036 const QPoint mousePos = viewport()->mapFromGlobal(QCursor::pos());
1037 if (option.rect.contains(p: mousePos)) {
1038 d->categoryDrawer->mouseButtonReleased(index: categoryIndex, blockRect: option.rect, event);
1039 viewport()->update(option.rect);
1040 if (!event->isAccepted()) {
1041 QListView::mouseReleaseEvent(e: event);
1042 }
1043 return;
1044 }
1045 ++it;
1046 }
1047 QListView::mouseReleaseEvent(e: event);
1048}
1049
1050void KCategorizedView::leaveEvent(QEvent *event)
1051{
1052 QListView::leaveEvent(event);
1053 if (d->hoveredIndex.isValid()) {
1054 viewport()->update(visualRect(index: d->hoveredIndex));
1055 d->hoveredIndex = QModelIndex();
1056 }
1057 if (d->categoryDrawer && d->hoveredBlock->height != -1) {
1058 const QModelIndex categoryIndex = d->proxyModel->index(row: d->hoveredBlock->firstIndex.row(), column: d->proxyModel->sortColumn(), parent: rootIndex());
1059 const QStyleOptionViewItem option = d->blockRect(representative: categoryIndex);
1060 d->categoryDrawer->mouseLeft(index: categoryIndex, blockRect: option.rect);
1061 *d->hoveredBlock = KCategorizedViewPrivate::Block();
1062 d->hoveredCategory = QString();
1063 viewport()->update(option.rect);
1064 }
1065}
1066
1067void KCategorizedView::startDrag(Qt::DropActions supportedActions)
1068{
1069 QListView::startDrag(supportedActions);
1070}
1071
1072void KCategorizedView::dragMoveEvent(QDragMoveEvent *event)
1073{
1074 QListView::dragMoveEvent(e: event);
1075 d->hoveredIndex = indexAt(point: event->position().toPoint());
1076}
1077
1078void KCategorizedView::dragEnterEvent(QDragEnterEvent *event)
1079{
1080 QListView::dragEnterEvent(event);
1081}
1082
1083void KCategorizedView::dragLeaveEvent(QDragLeaveEvent *event)
1084{
1085 QListView::dragLeaveEvent(e: event);
1086}
1087
1088void KCategorizedView::dropEvent(QDropEvent *event)
1089{
1090 QListView::dropEvent(e: 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
1095QModelIndex 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(index: current);
1103 if (!current.isValid()) {
1104 const int rowCount = d->proxyModel->rowCount(parent: rootIndex());
1105 if (!rowCount) {
1106 return QModelIndex();
1107 }
1108 return d->proxyModel->index(row: 0, column: modelColumn(), parent: rootIndex());
1109 }
1110
1111 switch (cursorAction) {
1112 case MoveLeft: {
1113 if (!current.row()) {
1114 return QModelIndex();
1115 }
1116 const QModelIndex previous = d->proxyModel->index(row: current.row() - 1, column: modelColumn(), parent: rootIndex());
1117 const QRect previousRect = visualRect(index: 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(row: current.row() + 1, column: modelColumn(), parent: rootIndex());
1129 const QRect nextRect = visualRect(index: 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(index: current);
1140 const KCategorizedViewPrivate::Block &block = d->blocks[d->categoryForIndex(index: current)];
1141 const int maxItemsPerRow = qMax(a: d->viewportWidth() / itemSize.width(), b: 1);
1142 const bool canMove = current.row() + maxItemsPerRow < block.firstIndex.row() + block.items.count();
1143
1144 if (canMove) {
1145 return d->proxyModel->index(row: current.row() + maxItemsPerRow, column: modelColumn(), parent: rootIndex());
1146 }
1147
1148 const int currentRelativePos = (current.row() - block.firstIndex.row()) % maxItemsPerRow;
1149 const QModelIndex nextIndex = d->proxyModel->index(row: block.firstIndex.row() + block.items.count(), column: modelColumn(), parent: rootIndex());
1150
1151 if (!nextIndex.isValid()) {
1152 return QModelIndex();
1153 }
1154
1155 const KCategorizedViewPrivate::Block &nextBlock = d->blocks[d->categoryForIndex(index: 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(row: nextBlock.firstIndex.row() + currentRelativePos, column: modelColumn(), parent: 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(index: current);
1171 const KCategorizedViewPrivate::Block &block = d->blocks[d->categoryForIndex(index: current)];
1172 const int maxItemsPerRow = qMax(a: d->viewportWidth() / itemSize.width(), b: 1);
1173 const bool canMove = current.row() - maxItemsPerRow >= block.firstIndex.row();
1174
1175 if (canMove) {
1176 return d->proxyModel->index(row: current.row() - maxItemsPerRow, column: modelColumn(), parent: rootIndex());
1177 }
1178
1179 const int currentRelativePos = (current.row() - block.firstIndex.row()) % maxItemsPerRow;
1180 const QModelIndex prevIndex = d->proxyModel->index(row: block.firstIndex.row() - 1, column: modelColumn(), parent: rootIndex());
1181
1182 if (!prevIndex.isValid()) {
1183 return QModelIndex();
1184 }
1185
1186 const KCategorizedViewPrivate::Block &prevBlock = d->blocks[d->categoryForIndex(index: 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(row: prevBlock.firstIndex.row() + prevBlock.items.count() - remainder + currentRelativePos, column: modelColumn(), parent: rootIndex());
1195 }
1196
1197 return QModelIndex();
1198 }
1199 break;
1200 }
1201 default:
1202 break;
1203 }
1204
1205 return QModelIndex();
1206}
1207
1208void 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(row: i, column: 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: 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(row: end, column: modelColumn(), parent);
1290 const QString category = d->categoryForIndex(index: 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(row: end + 1, column: 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(t&: listOfCategoriesMarkedForRemoval)) {
1300 d->blocks.remove(key: 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(first: blockList.begin(), last: blockList.end(), comp: KCategorizedViewPrivate::Block::lessThan);
1308 QList<int> firstIndexesRows;
1309 for (const KCategorizedViewPrivate::Block &block : std::as_const(t&: 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(t: block.firstIndex.row()) % 2;
1318 } else if (block.firstIndex.row() == start) {
1319 block.alternate = firstIndexesRows.indexOf(t: 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
1328void 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(min: 0, max: 0);
1365 // unconditional, see function end todo
1366 // BEGIN bugs 213068, 287847 ------------------------------------------------------------
1367 // restoring values from above ...
1368 horizontalScrollBar()->setRange(min: 0, max: 0);
1369 setVerticalScrollBarPolicy(verticalP);
1370 setHorizontalScrollBarPolicy(horizontalP);
1371 // END bugs 213068, 287847 --------------------------------------------------------------
1372 return;
1373 }
1374
1375 const QModelIndex lastIndex = d->proxyModel->index(row: rowCount - 1, column: modelColumn(), parent: rootIndex());
1376 Q_ASSERT(lastIndex.isValid());
1377 QRect lastItemRect = visualRect(index: lastIndex);
1378
1379 if (d->hasGrid()) {
1380 lastItemRect.setSize(lastItemRect.size().expandedTo(otherSize: gridSize()));
1381 } else {
1382 if (uniformItemSizes()) {
1383 QSize itemSize = sizeHintForIndex(index: lastIndex);
1384 itemSize.setHeight(itemSize.height() + spacing());
1385 lastItemRect.setSize(itemSize);
1386 } else {
1387 QSize itemSize = sizeHintForIndex(index: lastIndex);
1388 const QString category = d->categoryForIndex(index: lastIndex);
1389 itemSize.setHeight(d->highestElementInLastRow(block: 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(a: viewport()->height() / lastItemRect.height(), b: 1);
1399 verticalScrollBar()->setPageStep(rowsPerPage * lastItemRect.height());
1400 }
1401
1402 verticalScrollBar()->setRange(min: 0, max: 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(min: 0, max: 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
1427void KCategorizedView::currentChanged(const QModelIndex &current, const QModelIndex &previous)
1428{
1429 QListView::currentChanged(current, previous);
1430}
1431
1432void 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(row: i, column: modelColumn(), parent: rootIndex());
1450 if (i == indexToCheck) {
1451 categoryIndex = d->proxyModel->index(row: i, column: d->proxyModel->sortColumn(), parent: rootIndex());
1452 category = categoryIndex.data(arole: KCategorizedSortFilterProxyModel::CategoryDisplayRole).toString();
1453 block = &d->blocks[category];
1454 block->quarantineStart = currIndex;
1455 indexToCheck = block->firstIndex.row() + block->items.count();
1456 }
1457 visualRect(index: currIndex);
1458 ++i;
1459 }
1460 // END: since the model changed data, we need to reconsider item sizes
1461}
1462
1463void 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
1475void 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(parent: rootIndex(), start: 0, end: d->proxyModel->rowCount() - 1);
1486 }
1487}
1488
1489// END: Public part
1490
1491#include "moc_kcategorizedview.cpp"
1492

source code of kitemviews/src/kcategorizedview.cpp