1 /**
2   * This file is part of the KDE project
3   * Copyright (C) 2007, 2009 Rafael Fernández López <ereslibre@kde.org>
4   *
5   * This library is free software; you can redistribute it and/or
6   * modify it under the terms of the GNU Library General Public
7   * License as published by the Free Software Foundation; either
8   * version 2 of the License, or (at your option) any later version.
9   *
10   * This library is distributed in the hope that it will be useful,
11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13   * Library General Public License for more details.
14   *
15   * You should have received a copy of the GNU Library General Public License
16   * along with this library; see the file COPYING.LIB.  If not, write to
17   * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
18   * Boston, MA 02110-1301, USA.
19   */
20 
21 /**
22   * IMPLEMENTATION NOTES:
23   *
24   * QListView::setRowHidden() and QListView::isRowHidden() are not taken into
25   * account. This methods should actually not exist. This effect should be handled
26   * by an hypothetical QSortFilterProxyModel which filters out the desired rows.
27   *
28   * In case this needs to be implemented, contact me, but I consider this a faulty
29   * design.
30   */
31 
32 #include "kcategorizedview.h"
33 #include "kcategorizedview_p.h"
34 
35 #include <QPainter>
36 #include <QScrollBar>
37 #include <QPaintEvent>
38 #include <algorithm>
39 
40 #include "kcategorydrawer.h"
41 #include "kcategorizedsortfilterproxymodel.h"
42 
43 //BEGIN: Private part
44 
45 struct KCategorizedView::Private::Item {
ItemKCategorizedView::Private::Item46     Item()
47         : topLeft(QPoint())
48         , size(QSize())
49     {
50     }
51 
52     QPoint topLeft;
53     QSize size;
54 };
55 
56 struct KCategorizedView::Private::Block {
BlockKCategorizedView::Private::Block57     Block()
58         : topLeft(QPoint())
59         , height(-1)
60         , firstIndex(QModelIndex())
61         , quarantineStart(QModelIndex())
62         , items(QList<Item>())
63         , outOfQuarantine(false)
64         , alternate(false)
65         , collapsed(false)
66     {
67     }
68 
operator !=KCategorizedView::Private::Block69     bool operator!=(const Block &rhs) const
70     {
71         return firstIndex != rhs.firstIndex;
72     }
73 
lessThanKCategorizedView::Private::Block74     static bool lessThan(const Block &left, const Block &right)
75     {
76         Q_ASSERT(left.firstIndex.isValid());
77         Q_ASSERT(right.firstIndex.isValid());
78         return left.firstIndex.row() < right.firstIndex.row();
79     }
80 
81     QPoint topLeft;
82     int height;
83     QPersistentModelIndex firstIndex;
84     // if we have n elements on this block, and we inserted an element at position i. The quarantine
85     // will start at index (i, column, parent). This means that for all elements j where i <= j <= n, the
86     // visual rect position of item j will have to be recomputed (cannot use the cached point). The quarantine
87     // will only affect the current block, since the rest of blocks can be affected only in the way
88     // that the whole block will have different offset, but items will keep the same relative position
89     // in terms of their parent blocks.
90     QPersistentModelIndex quarantineStart;
91     QList<Item> items;
92 
93     // this affects the whole block, not items separately. items contain the topLeft point relative
94     // to the block. Because of insertions or removals a whole block can be moved, so the whole block
95     // will enter in quarantine, what is faster than moving all items in absolute terms.
96     bool outOfQuarantine;
97 
98     // should we alternate its color ? is just a hint, could not be used
99     bool alternate;
100     bool collapsed;
101 };
102 
Private(KCategorizedView * q)103 KCategorizedView::Private::Private(KCategorizedView *q)
104     : q(q)
105     , proxyModel(nullptr)
106     , categoryDrawer(nullptr)
107     , categorySpacing(5)
108     , alternatingBlockColors(false)
109     , collapsibleBlocks(false)
110     , hoveredBlock(new Block())
111     , hoveredIndex(QModelIndex())
112     , pressedPosition(QPoint())
113     , rubberBandRect(QRect())
114 {
115 }
116 
~Private()117 KCategorizedView::Private::~Private()
118 {
119     delete hoveredBlock;
120 }
121 
isCategorized() const122 bool KCategorizedView::Private::isCategorized() const
123 {
124     return proxyModel && categoryDrawer && proxyModel->isCategorizedModel();
125 }
126 
blockRect(const QModelIndex & representative)127 QStyleOptionViewItem KCategorizedView::Private::blockRect(const QModelIndex &representative)
128 {
129     QStyleOptionViewItem option(q->viewOptions());
130     const int height = categoryDrawer->categoryHeight(representative, option);
131     const QString categoryDisplay = representative.data(KCategorizedSortFilterProxyModel::CategoryDisplayRole).toString();
132     QPoint pos = blockPosition(categoryDisplay);
133     pos.ry() -= height;
134     option.rect.setTopLeft(pos);
135     option.rect.setWidth(viewportWidth() + categoryDrawer->leftMargin() + categoryDrawer->rightMargin());
136     option.rect.setHeight(height + blockHeight(categoryDisplay));
137     option.rect = mapToViewport(option.rect);
138 
139     return option;
140 }
141 
intersectingIndexesWithRect(const QRect & _rect) const142 QPair<QModelIndex, QModelIndex> KCategorizedView::Private::intersectingIndexesWithRect(const QRect &_rect) const
143 {
144     const int rowCount = proxyModel->rowCount();
145 
146     const QRect rect = _rect.normalized();
147 
148     // binary search to find out the top border
149     int bottom = 0;
150     int top = rowCount - 1;
151     while (bottom <= top) {
152         const int middle = (bottom + top) / 2;
153         const QModelIndex index = proxyModel->index(middle, q->modelColumn(), q->rootIndex());
154         const QRect itemRect = q->visualRect(index);
155         if (itemRect.bottomRight().y() <= rect.topLeft().y()) {
156             bottom = middle + 1;
157         } else {
158             top = middle - 1;
159         }
160     }
161 
162     const QModelIndex bottomIndex = proxyModel->index(bottom, q->modelColumn(), q->rootIndex());
163 
164     // binary search to find out the bottom border
165     bottom = 0;
166     top = rowCount - 1;
167     while (bottom <= top) {
168         const int middle = (bottom + top) / 2;
169         const QModelIndex index = proxyModel->index(middle, q->modelColumn(), q->rootIndex());
170         const QRect itemRect = q->visualRect(index);
171         if (itemRect.topLeft().y() <= rect.bottomRight().y()) {
172             bottom = middle + 1;
173         } else {
174             top = middle - 1;
175         }
176     }
177 
178     const QModelIndex topIndex = proxyModel->index(top, q->modelColumn(), q->rootIndex());
179 
180     return qMakePair(bottomIndex, topIndex);
181 }
182 
blockPosition(const QString & category)183 QPoint KCategorizedView::Private::blockPosition(const QString &category)
184 {
185     Block &block = blocks[category];
186 
187     if (block.outOfQuarantine && !block.topLeft.isNull()) {
188         return block.topLeft;
189     }
190 
191     QPoint res(categorySpacing, 0);
192 
193     const QModelIndex index = block.firstIndex;
194 
195     for (QHash<QString, Private::Block>::Iterator it = blocks.begin(); it != blocks.end(); ++it) {
196         Block &block = *it;
197         const QModelIndex categoryIndex = block.firstIndex;
198         if (index.row() < categoryIndex.row()) {
199             continue;
200         }
201         res.ry() += categoryDrawer->categoryHeight(categoryIndex, q->viewOptions()) + categorySpacing;
202         if (index.row() == categoryIndex.row()) {
203             continue;
204         }
205         res.ry() += blockHeight(it.key());
206     }
207 
208     block.outOfQuarantine = true;
209     block.topLeft = res;
210 
211     return res;
212 }
213 
blockHeight(const QString & category)214 int KCategorizedView::Private::blockHeight(const QString &category)
215 {
216     Block &block = blocks[category];
217 
218     if (block.collapsed) {
219         return 0;
220     }
221 
222     if (block.height > -1) {
223         return block.height;
224     }
225 
226     const QModelIndex firstIndex = block.firstIndex;
227     const QModelIndex lastIndex = proxyModel->index(firstIndex.row() + block.items.count() - 1, q->modelColumn(), q->rootIndex());
228     const QRect topLeft = q->visualRect(firstIndex);
229     QRect bottomRight = q->visualRect(lastIndex);
230 
231     if (hasGrid()) {
232         bottomRight.setHeight(qMax(bottomRight.height(), q->gridSize().height()));
233     } else {
234         if (!q->uniformItemSizes()) {
235             bottomRight.setHeight(highestElementInLastRow(block) + q->spacing() * 2);
236         }
237     }
238 
239     const int height = bottomRight.bottomRight().y() - topLeft.topLeft().y() + 1;
240     block.height = height;
241 
242     return height;
243 }
244 
viewportWidth() const245 int KCategorizedView::Private::viewportWidth() const
246 {
247     return q->viewport()->width() - categorySpacing * 2 - categoryDrawer->leftMargin() - categoryDrawer->rightMargin();
248 }
249 
regenerateAllElements()250 void KCategorizedView::Private::regenerateAllElements()
251 {
252     for (QHash<QString, Block>::Iterator it = blocks.begin(); it != blocks.end(); ++it) {
253         Block &block = *it;
254         block.outOfQuarantine = false;
255         block.quarantineStart = block.firstIndex;
256         block.height = -1;
257     }
258 }
259 
rowsInserted(const QModelIndex & parent,int start,int end)260 void KCategorizedView::Private::rowsInserted(const QModelIndex &parent, int start, int end)
261 {
262     if (!isCategorized()) {
263         return;
264     }
265 
266     for (int i = start; i <= end; ++i) {
267         const QModelIndex index = proxyModel->index(i, q->modelColumn(), parent);
268 
269         Q_ASSERT(index.isValid());
270 
271         const QString category = categoryForIndex(index);
272 
273         Block &block = blocks[category];
274 
275         //BEGIN: update firstIndex
276         // save as firstIndex in block if
277         //     - it forced the category creation (first element on this category)
278         //     - it is before the first row on that category
279         const QModelIndex firstIndex = block.firstIndex;
280         if (!firstIndex.isValid() || index.row() < firstIndex.row()) {
281             block.firstIndex = index;
282         }
283         //END: update firstIndex
284 
285         Q_ASSERT(block.firstIndex.isValid());
286 
287         const int firstIndexRow = block.firstIndex.row();
288 
289         block.items.insert(index.row() - firstIndexRow, Private::Item());
290         block.height = -1;
291 
292         q->visualRect(index);
293         q->viewport()->update();
294     }
295 
296     //BEGIN: update the items that are in quarantine in affected categories
297     {
298         const QModelIndex lastIndex = proxyModel->index(end, q->modelColumn(), parent);
299         const QString category = categoryForIndex(lastIndex);
300         Private::Block &block = blocks[category];
301         block.quarantineStart = block.firstIndex;
302     }
303     //END: update the items that are in quarantine in affected categories
304 
305     //BEGIN: mark as in quarantine those categories that are under the affected ones
306     {
307         const QModelIndex firstIndex = proxyModel->index(start, q->modelColumn(), parent);
308         const QString category = categoryForIndex(firstIndex);
309         const QModelIndex firstAffectedCategory = blocks[category].firstIndex;
310         //BEGIN: order for marking as alternate those blocks that are alternate
311         QList<Block> blockList = blocks.values();
312         std::sort(blockList.begin(), blockList.end(), Block::lessThan);
313         QList<int> firstIndexesRows;
314         foreach (const Block &block, blockList) {
315             firstIndexesRows << block.firstIndex.row();
316         }
317         //END: order for marking as alternate those blocks that are alternate
318         for (QHash<QString, Private::Block>::Iterator it = blocks.begin(); it != blocks.end(); ++it) {
319             Private::Block &block = *it;
320             if (block.firstIndex.row() > firstAffectedCategory.row()) {
321                 block.outOfQuarantine = false;
322                 block.alternate = firstIndexesRows.indexOf(block.firstIndex.row()) % 2;
323             } else if (block.firstIndex.row() == firstAffectedCategory.row()) {
324                 block.alternate = firstIndexesRows.indexOf(block.firstIndex.row()) % 2;
325             }
326         }
327     }
328     //END: mark as in quarantine those categories that are under the affected ones
329 }
330 
mapToViewport(const QRect & rect) const331 QRect KCategorizedView::Private::mapToViewport(const QRect &rect) const
332 {
333     const int dx = -q->horizontalOffset();
334     const int dy = -q->verticalOffset();
335     return rect.adjusted(dx, dy, dx, dy);
336 }
337 
mapFromViewport(const QRect & rect) const338 QRect KCategorizedView::Private::mapFromViewport(const QRect &rect) const
339 {
340     const int dx = q->horizontalOffset();
341     const int dy = q->verticalOffset();
342     return rect.adjusted(dx, dy, dx, dy);
343 }
344 
highestElementInLastRow(const Block & block) const345 int KCategorizedView::Private::highestElementInLastRow(const Block &block) const
346 {
347     //Find the highest element in the last row
348     const QModelIndex lastIndex = proxyModel->index(block.firstIndex.row() + block.items.count() - 1, q->modelColumn(), q->rootIndex());
349     const QRect prevRect = q->visualRect(lastIndex);
350     int res = prevRect.height();
351     QModelIndex prevIndex = proxyModel->index(lastIndex.row() - 1, q->modelColumn(), q->rootIndex());
352     if (!prevIndex.isValid()) {
353         return res;
354     }
355     Q_FOREVER {
356     const QRect tempRect = q->visualRect(prevIndex);
357         if (tempRect.topLeft().y() < prevRect.topLeft().y())
358         {
359             break;
360         }
361         res = qMax(res, tempRect.height());
362         if (prevIndex == block.firstIndex)
363         {
364             break;
365         }
366         prevIndex = proxyModel->index(prevIndex.row() - 1, q->modelColumn(), q->rootIndex());
367     }
368 
369     return res;
370 }
371 
hasGrid() const372 bool KCategorizedView::Private::hasGrid() const
373 {
374     const QSize gridSize = q->gridSize();
375     return gridSize.isValid() && !gridSize.isNull();
376 }
377 
categoryForIndex(const QModelIndex & index) const378 QString KCategorizedView::Private::categoryForIndex(const QModelIndex &index) const
379 {
380     const QModelIndex categoryIndex = index.model()->index(index.row(), proxyModel->sortColumn(), index.parent());
381     return categoryIndex.data(KCategorizedSortFilterProxyModel::CategoryDisplayRole).toString();
382 }
383 
leftToRightVisualRect(const QModelIndex & index,Item & item,const Block & block,const QPoint & blockPos) const384 void KCategorizedView::Private::leftToRightVisualRect(const QModelIndex &index, Item &item,
385         const Block &block, const QPoint &blockPos) const
386 {
387     const int firstIndexRow = block.firstIndex.row();
388 
389     if (hasGrid()) {
390         const int relativeRow = index.row() - firstIndexRow;
391         const int maxItemsPerRow = qMax(viewportWidth() / q->gridSize().width(), 1);
392         if (q->layoutDirection() == Qt::LeftToRight) {
393             item.topLeft.rx() = (relativeRow % maxItemsPerRow) * q->gridSize().width() + blockPos.x() + categoryDrawer->leftMargin();
394         } else {
395             item.topLeft.rx() = viewportWidth() - ((relativeRow % maxItemsPerRow) + 1) * q->gridSize().width() + categoryDrawer->leftMargin() + categorySpacing;
396         }
397         item.topLeft.ry() = (relativeRow / maxItemsPerRow) * q->gridSize().height();
398     } else {
399         if (q->uniformItemSizes()) {
400             const int relativeRow = index.row() - firstIndexRow;
401             const QSize itemSize = q->sizeHintForIndex(index);
402             const int maxItemsPerRow = qMax((viewportWidth() - q->spacing()) / (itemSize.width() + q->spacing()), 1);
403             if (q->layoutDirection() == Qt::LeftToRight) {
404                 item.topLeft.rx() = (relativeRow % maxItemsPerRow) * itemSize.width() + blockPos.x() + categoryDrawer->leftMargin();
405             } else {
406                 item.topLeft.rx() = viewportWidth() - (relativeRow % maxItemsPerRow) * itemSize.width() + categoryDrawer->leftMargin() + categorySpacing;
407             }
408             item.topLeft.ry() = (relativeRow / maxItemsPerRow) * itemSize.height();
409         } else {
410             const QSize currSize = q->sizeHintForIndex(index);
411             if (index != block.firstIndex) {
412                 const int viewportW = viewportWidth() - q->spacing();
413                 QModelIndex prevIndex = proxyModel->index(index.row() - 1, q->modelColumn(), q->rootIndex());
414                 QRect prevRect = q->visualRect(prevIndex);
415                 prevRect = mapFromViewport(prevRect);
416                 if ((prevRect.bottomRight().x() + 1) + currSize.width() - blockPos.x() + q->spacing()  > viewportW) {
417                     // we have to check the whole previous row, and see which one was the
418                     // highest.
419                     Q_FOREVER {
420                     prevIndex = proxyModel->index(prevIndex.row() - 1, q->modelColumn(), q->rootIndex());
421                         const QRect tempRect = q->visualRect(prevIndex);
422                         if (tempRect.topLeft().y() < prevRect.topLeft().y())
423                         {
424                             break;
425                         }
426                         if (tempRect.bottomRight().y() > prevRect.bottomRight().y())
427                         {
428                             prevRect = tempRect;
429                         }
430                         if (prevIndex == block.firstIndex)
431                         {
432                             break;
433                         }
434                     }
435                     if (q->layoutDirection() == Qt::LeftToRight) {
436                         item.topLeft.rx() = categoryDrawer->leftMargin() + blockPos.x() + q->spacing();
437                     } else {
438                         item.topLeft.rx() = viewportWidth() - currSize.width() + categoryDrawer->leftMargin() + categorySpacing;
439                     }
440                     item.topLeft.ry() = (prevRect.bottomRight().y() + 1) + q->spacing() - blockPos.y();
441                 } else {
442                     if (q->layoutDirection() == Qt::LeftToRight) {
443                         item.topLeft.rx() = (prevRect.bottomRight().x() + 1) + q->spacing();
444                     } else {
445                         item.topLeft.rx() = (prevRect.bottomLeft().x() - 1) - q->spacing() - item.size.width() + categoryDrawer->leftMargin() + categorySpacing;
446                     }
447                     item.topLeft.ry() = prevRect.topLeft().y() - blockPos.y();
448                 }
449             } else {
450                 if (q->layoutDirection() == Qt::LeftToRight) {
451                     item.topLeft.rx() = blockPos.x() + categoryDrawer->leftMargin() + q->spacing();
452                 } else {
453                     item.topLeft.rx() = viewportWidth() - currSize.width() + categoryDrawer->leftMargin() + categorySpacing;
454                 }
455                 item.topLeft.ry() = q->spacing();
456             }
457         }
458     }
459     item.size = q->sizeHintForIndex(index);
460 }
461 
topToBottomVisualRect(const QModelIndex & index,Item & item,const Block & block,const QPoint & blockPos) const462 void KCategorizedView::Private::topToBottomVisualRect(const QModelIndex &index, Item &item,
463         const Block &block, const QPoint &blockPos) const
464 {
465     const int firstIndexRow = block.firstIndex.row();
466 
467     if (hasGrid()) {
468         const int relativeRow = index.row() - firstIndexRow;
469         item.topLeft.rx() = blockPos.x() + categoryDrawer->leftMargin();
470         item.topLeft.ry() = relativeRow * q->gridSize().height();
471     } else {
472         if (q->uniformItemSizes()) {
473             const int relativeRow = index.row() - firstIndexRow;
474             const QSize itemSize = q->sizeHintForIndex(index);
475             item.topLeft.rx() = blockPos.x() + categoryDrawer->leftMargin();
476             item.topLeft.ry() = relativeRow * itemSize.height();
477         } else {
478             if (index != block.firstIndex) {
479                 QModelIndex prevIndex = proxyModel->index(index.row() - 1, q->modelColumn(), q->rootIndex());
480                 QRect prevRect = q->visualRect(prevIndex);
481                 prevRect = mapFromViewport(prevRect);
482                 item.topLeft.rx() = blockPos.x() + categoryDrawer->leftMargin() + q->spacing();
483                 item.topLeft.ry() = (prevRect.bottomRight().y() + 1) + q->spacing() - blockPos.y();
484             } else {
485                 item.topLeft.rx() = blockPos.x() + categoryDrawer->leftMargin() + q->spacing();
486                 item.topLeft.ry() = q->spacing();
487             }
488         }
489     }
490     item.size = q->sizeHintForIndex(index);
491     item.size.setWidth(viewportWidth());
492 }
493 
_k_slotCollapseOrExpandClicked(QModelIndex)494 void KCategorizedView::Private::_k_slotCollapseOrExpandClicked(QModelIndex)
495 {
496 }
497 
498 //END: Private part
499 
500 //BEGIN: Public part
501 
KCategorizedView(QWidget * parent)502 KCategorizedView::KCategorizedView(QWidget *parent)
503     : QListView(parent)
504     , d(new Private(this))
505 {
506 }
507 
~KCategorizedView()508 KCategorizedView::~KCategorizedView()
509 {
510     delete d;
511 }
512 
setModel(QAbstractItemModel * model)513 void KCategorizedView::setModel(QAbstractItemModel *model)
514 {
515     if (d->proxyModel == model) {
516         return;
517     }
518 
519     d->blocks.clear();
520 
521     if (d->proxyModel) {
522         disconnect(d->proxyModel, SIGNAL(layoutChanged()), this, SLOT(slotLayoutChanged()));
523     }
524 
525     d->proxyModel = dynamic_cast<KCategorizedSortFilterProxyModel *>(model);
526 
527     if (d->proxyModel) {
528         connect(d->proxyModel, SIGNAL(layoutChanged()), this, SLOT(slotLayoutChanged()));
529     }
530 
531     QListView::setModel(model);
532 
533     // if the model already had information inserted, update our data structures to it
534     if (model && model->rowCount()) {
535         slotLayoutChanged();
536     }
537 }
538 
setGridSize(const QSize & size)539 void KCategorizedView::setGridSize(const QSize &size)
540 {
541     setGridSizeOwn(size);
542 }
543 
setGridSizeOwn(const QSize & size)544 void KCategorizedView::setGridSizeOwn(const QSize &size)
545 {
546     d->regenerateAllElements();
547     QListView::setGridSize(size);
548 }
549 
visualRect(const QModelIndex & index) const550 QRect KCategorizedView::visualRect(const QModelIndex &index) const
551 {
552     if (!d->isCategorized()) {
553         return QListView::visualRect(index);
554     }
555 
556     if (!index.isValid()) {
557         return QRect();
558     }
559 
560     const QString category = d->categoryForIndex(index);
561 
562     if (!d->blocks.contains(category)) {
563         return QRect();
564     }
565 
566     Private::Block &block = d->blocks[category];
567     const int firstIndexRow = block.firstIndex.row();
568 
569     Q_ASSERT(block.firstIndex.isValid());
570 
571     if (index.row() - firstIndexRow < 0 || index.row() - firstIndexRow >= block.items.count()) {
572         return QRect();
573     }
574 
575     const QPoint blockPos = d->blockPosition(category);
576 
577     Private::Item &ritem = block.items[index.row() - firstIndexRow];
578 
579     if (ritem.topLeft.isNull() || (block.quarantineStart.isValid() &&
580                                    index.row() >= block.quarantineStart.row())) {
581         if (flow() == LeftToRight) {
582             d->leftToRightVisualRect(index, ritem, block, blockPos);
583         } else {
584             d->topToBottomVisualRect(index, ritem, block, blockPos);
585         }
586 
587         //BEGIN: update the quarantine start
588         const bool wasLastIndex = (index.row() == (block.firstIndex.row() + block.items.count() - 1));
589         if (index.row() == block.quarantineStart.row()) {
590             if (wasLastIndex) {
591                 block.quarantineStart = QModelIndex();
592             } else {
593                 const QModelIndex nextIndex = d->proxyModel->index(index.row() + 1, modelColumn(), rootIndex());
594                 block.quarantineStart = nextIndex;
595             }
596         }
597         //END: update the quarantine start
598     }
599 
600     // we get now the absolute position through the relative position of the parent block. do not
601     // save this on ritem, since this would override the item relative position in block terms.
602     Private::Item item(ritem);
603     item.topLeft.ry() += blockPos.y();
604 
605     const QSize sizeHint = item.size;
606 
607     if (d->hasGrid()) {
608         const QSize sizeGrid = gridSize();
609         const QSize resultingSize = sizeHint.boundedTo(sizeGrid);
610         QRect res(item.topLeft.x() + ((sizeGrid.width() - resultingSize.width()) / 2),
611                   item.topLeft.y(), resultingSize.width(), resultingSize.height());
612         if (block.collapsed) {
613             // we can still do binary search, while we "hide" items. We move those items in collapsed
614             // blocks to the left and set a 0 height.
615             res.setLeft(-resultingSize.width());
616             res.setHeight(0);
617         }
618         return d->mapToViewport(res);
619     }
620 
621     QRect res(item.topLeft.x(), item.topLeft.y(), sizeHint.width(), sizeHint.height());
622     if (block.collapsed) {
623         // we can still do binary search, while we "hide" items. We move those items in collapsed
624         // blocks to the left and set a 0 height.
625         res.setLeft(-sizeHint.width());
626         res.setHeight(0);
627     }
628     return d->mapToViewport(res);
629 }
630 
categoryDrawer() const631 KCategoryDrawer *KCategorizedView::categoryDrawer() const
632 {
633     return d->categoryDrawer;
634 }
635 
setCategoryDrawer(KCategoryDrawer * categoryDrawer)636 void KCategorizedView::setCategoryDrawer(KCategoryDrawer *categoryDrawer)
637 {
638     if (d->categoryDrawer) {
639         disconnect(d->categoryDrawer, SIGNAL(collapseOrExpandClicked(QModelIndex)),
640                    this, SLOT(_k_slotCollapseOrExpandClicked(QModelIndex)));
641     }
642 
643     d->categoryDrawer = categoryDrawer;
644 
645     connect(d->categoryDrawer, SIGNAL(collapseOrExpandClicked(QModelIndex)),
646             this, SLOT(_k_slotCollapseOrExpandClicked(QModelIndex)));
647 }
648 
categorySpacing() const649 int KCategorizedView::categorySpacing() const
650 {
651     return d->categorySpacing;
652 }
653 
setCategorySpacing(int categorySpacing)654 void KCategorizedView::setCategorySpacing(int categorySpacing)
655 {
656     if (d->categorySpacing == categorySpacing) {
657         return;
658     }
659 
660     d->categorySpacing = categorySpacing;
661 
662     for (QHash<QString, Private::Block>::Iterator it = d->blocks.begin(); it != d->blocks.end(); ++it) {
663         Private::Block &block = *it;
664         block.outOfQuarantine = false;
665     }
666 }
667 
alternatingBlockColors() const668 bool KCategorizedView::alternatingBlockColors() const
669 {
670     return d->alternatingBlockColors;
671 }
672 
setAlternatingBlockColors(bool enable)673 void KCategorizedView::setAlternatingBlockColors(bool enable)
674 {
675     d->alternatingBlockColors = enable;
676 }
677 
collapsibleBlocks() const678 bool KCategorizedView::collapsibleBlocks() const
679 {
680     return d->collapsibleBlocks;
681 }
682 
setCollapsibleBlocks(bool enable)683 void KCategorizedView::setCollapsibleBlocks(bool enable)
684 {
685     d->collapsibleBlocks = enable;
686 }
687 
block(const QString & category)688 QModelIndexList KCategorizedView::block(const QString &category)
689 {
690     QModelIndexList res;
691     const Private::Block &block = d->blocks[category];
692     if (block.height == -1) {
693         return res;
694     }
695     QModelIndex current = block.firstIndex;
696     const int first = current.row();
697     for (int i = 1; i <= block.items.count(); ++i) {
698         if (current.isValid()) {
699             res << current;
700         }
701         current = d->proxyModel->index(first + i, modelColumn(), rootIndex());
702     }
703     return res;
704 }
705 
block(const QModelIndex & representative)706 QModelIndexList KCategorizedView::block(const QModelIndex &representative)
707 {
708     return block(representative.data(KCategorizedSortFilterProxyModel::CategoryDisplayRole).toString());
709 }
710 
indexAt(const QPoint & point) const711 QModelIndex KCategorizedView::indexAt(const QPoint &point) const
712 {
713     if (!d->isCategorized()) {
714         return QListView::indexAt(point);
715     }
716 
717     const int rowCount = d->proxyModel->rowCount();
718     if (!rowCount) {
719         return QModelIndex();
720     }
721 
722     // Binary search that will try to spot if there is an index under point
723     int bottom = 0;
724     int top = rowCount - 1;
725     while (bottom <= top) {
726         const int middle = (bottom + top) / 2;
727         const QModelIndex index = d->proxyModel->index(middle, modelColumn(), rootIndex());
728         const QRect rect = visualRect(index);
729         if (rect.contains(point)) {
730             if (index.model()->flags(index) & Qt::ItemIsEnabled) {
731                 return index;
732             }
733             return QModelIndex();
734         }
735         bool directionCondition;
736         if (layoutDirection() == Qt::LeftToRight) {
737             directionCondition = point.x() >= rect.bottomLeft().x();
738         } else {
739             directionCondition = point.x() <= rect.bottomRight().x();
740         }
741         if (point.y() < rect.topLeft().y()) {
742             top = middle - 1;
743         } else if (directionCondition) {
744             bottom = middle + 1;
745         } else if (point.y() <= rect.bottomRight().y()) {
746             top = middle - 1;
747         } else {
748             bool after = true;
749             for (int i = middle - 1;i >= bottom;i--) {
750                 const QModelIndex newIndex =
751                     d->proxyModel->index(i, modelColumn(), rootIndex());
752                 const QRect newRect = visualRect(newIndex);
753                 if (newRect.topLeft().y() < rect.topLeft().y()) {
754                     break;
755                 } else if (newRect.contains(point)) {
756                     if (newIndex.model()->flags(newIndex) & Qt::ItemIsEnabled) {
757                         return newIndex;
758                     }
759                     return QModelIndex();
760                 } else if ((layoutDirection() == Qt::LeftToRight) ?
761                            (newRect.topLeft().x() <= point.x()) :
762                            (newRect.topRight().x() >= point.x())) {
763                     break;
764                 } else if (newRect.bottomRight().y() >= point.y()) {
765                     after = false;
766                 }
767             }
768             if (!after) {
769                 return QModelIndex();
770             }
771             bottom = middle + 1;
772         }
773     }
774     return QModelIndex();
775 }
776 
reset()777 void KCategorizedView::reset()
778 {
779     d->blocks.clear();
780     QListView::reset();
781 }
782 
paintEvent(QPaintEvent * event)783 void KCategorizedView::paintEvent(QPaintEvent *event)
784 {
785     if (!d->isCategorized()) {
786         QListView::paintEvent(event);
787         return;
788     }
789 
790     const QPair<QModelIndex, QModelIndex> intersecting = d->intersectingIndexesWithRect(viewport()->rect().intersected(event->rect()));
791 
792     QPainter p(viewport());
793     p.save();
794 
795     Q_ASSERT(selectionModel()->model() == d->proxyModel);
796 
797     //BEGIN: draw categories
798     QHash<QString, Private::Block>::ConstIterator it(d->blocks.constBegin());
799     while (it != d->blocks.constEnd()) {
800         const Private::Block &block = *it;
801         const QModelIndex categoryIndex = d->proxyModel->index(block.firstIndex.row(), d->proxyModel->sortColumn(), rootIndex());
802         QStyleOptionViewItem option(viewOptions());
803         option.features |= d->alternatingBlockColors && block.alternate ? QStyleOptionViewItem::Alternate
804                            : QStyleOptionViewItem::None;
805         option.state |= !d->collapsibleBlocks || !block.collapsed ? QStyle::State_Open
806                         : QStyle::State_None;
807         const int height = d->categoryDrawer->categoryHeight(categoryIndex, option);
808         QPoint pos = d->blockPosition(it.key());
809         pos.ry() -= height;
810         option.rect.setTopLeft(pos);
811         option.rect.setWidth(d->viewportWidth() + d->categoryDrawer->leftMargin() + d->categoryDrawer->rightMargin());
812         option.rect.setHeight(height + d->blockHeight(it.key()));
813         option.rect = d->mapToViewport(option.rect);
814         if (!option.rect.intersects(viewport()->rect())) {
815             ++it;
816             continue;
817         }
818         d->categoryDrawer->drawCategory(categoryIndex, d->proxyModel->sortRole(), option, &p);
819         ++it;
820     }
821     //END: draw categories
822 
823     if (intersecting.first.isValid() && intersecting.second.isValid()) {
824         //BEGIN: draw items
825         int i = intersecting.first.row();
826         int indexToCheckIfBlockCollapsed = i;
827         QModelIndex categoryIndex;
828         QString category;
829         Private::Block *block = nullptr;
830         while (i <= intersecting.second.row()) {
831             //BEGIN: first check if the block is collapsed. if so, we have to skip the item painting
832             if (i == indexToCheckIfBlockCollapsed) {
833                 categoryIndex = d->proxyModel->index(i, d->proxyModel->sortColumn(), rootIndex());
834                 category = categoryIndex.data(KCategorizedSortFilterProxyModel::CategoryDisplayRole).toString();
835                 block = &d->blocks[category];
836                 indexToCheckIfBlockCollapsed = block->firstIndex.row() + block->items.count();
837                 if (block->collapsed) {
838                     i = indexToCheckIfBlockCollapsed;
839                     continue;
840                 }
841             }
842             //END: first check if the block is collapsed. if so, we have to skip the item painting
843 
844             Q_ASSERT(block);
845 
846             const bool alternateItem = (i - block->firstIndex.row()) % 2;
847 
848             const QModelIndex index = d->proxyModel->index(i, modelColumn(), rootIndex());
849             const Qt::ItemFlags flags = d->proxyModel->flags(index);
850             QStyleOptionViewItem option(viewOptions());
851             option.rect = visualRect(index);
852             option.widget = this;
853             option.features |= wordWrap() ? QStyleOptionViewItem::WrapText
854                                : QStyleOptionViewItem::None;
855             option.features |= alternatingRowColors() && alternateItem ? QStyleOptionViewItem::Alternate
856                                : QStyleOptionViewItem::None;
857             if (flags & Qt::ItemIsSelectable) {
858                 option.state |= selectionModel()->isSelected(index) ? QStyle::State_Selected
859                                 : QStyle::State_None;
860             } else {
861                 option.state &= ~QStyle::State_Selected;
862             }
863             option.state |= (index == currentIndex()) ? QStyle::State_HasFocus
864                             : QStyle::State_None;
865             if (!(flags & Qt::ItemIsEnabled)) {
866                 option.state &= ~QStyle::State_Enabled;
867             } else {
868                 option.state |= (index == d->hoveredIndex) ? QStyle::State_MouseOver
869                                 : QStyle::State_None;
870             }
871 
872             itemDelegate(index)->paint(&p, option, index);
873             ++i;
874         }
875         //END: draw items
876     }
877 
878     //BEGIN: draw selection rect
879     if (isSelectionRectVisible() && d->rubberBandRect.isValid()) {
880         QStyleOptionRubberBand opt;
881         opt.initFrom(this);
882         opt.shape = QRubberBand::Rectangle;
883         opt.opaque = false;
884         opt.rect = d->mapToViewport(d->rubberBandRect).intersected(viewport()->rect().adjusted(-16, -16, 16, 16));
885         p.save();
886         style()->drawControl(QStyle::CE_RubberBand, &opt, &p);
887         p.restore();
888     }
889     //END: draw selection rect
890 
891     p.restore();
892 }
893 
resizeEvent(QResizeEvent * event)894 void KCategorizedView::resizeEvent(QResizeEvent *event)
895 {
896     d->regenerateAllElements();
897     QListView::resizeEvent(event);
898 }
899 
setSelection(const QRect & rect,QItemSelectionModel::SelectionFlags flags)900 void KCategorizedView::setSelection(const QRect &rect,
901                                     QItemSelectionModel::SelectionFlags flags)
902 {
903     if (!d->isCategorized()) {
904         QListView::setSelection(rect, flags);
905         return;
906     }
907 
908     if (rect.topLeft() == rect.bottomRight()) {
909         const QModelIndex index = indexAt(rect.topLeft());
910         selectionModel()->select(index, flags);
911         return;
912     }
913 
914     const QPair<QModelIndex, QModelIndex> intersecting = d->intersectingIndexesWithRect(rect);
915 
916     QItemSelection selection;
917 
918     //TODO: think of a faster implementation
919     QModelIndex firstIndex;
920     QModelIndex lastIndex;
921     for (int i = intersecting.first.row(); i <= intersecting.second.row(); ++i) {
922         const QModelIndex index = d->proxyModel->index(i, modelColumn(), rootIndex());
923         const bool visualRectIntersects = visualRect(index).intersects(rect);
924         if (firstIndex.isValid()) {
925             if (visualRectIntersects) {
926                 lastIndex = index;
927             } else {
928                 selection << QItemSelectionRange(firstIndex, lastIndex);
929                 firstIndex = QModelIndex();
930             }
931         } else if (visualRectIntersects) {
932             firstIndex = index;
933             lastIndex = index;
934         }
935     }
936 
937     if (firstIndex.isValid()) {
938         selection << QItemSelectionRange(firstIndex, lastIndex);
939     }
940 
941     selectionModel()->select(selection, flags);
942 }
943 
mouseMoveEvent(QMouseEvent * event)944 void KCategorizedView::mouseMoveEvent(QMouseEvent *event)
945 {
946     QListView::mouseMoveEvent(event);
947     d->hoveredIndex = indexAt(event->pos());
948     const SelectionMode itemViewSelectionMode = selectionMode();
949     if (state() == DragSelectingState && isSelectionRectVisible() && itemViewSelectionMode != SingleSelection
950             && itemViewSelectionMode != NoSelection) {
951         QRect rect(d->pressedPosition, event->pos() + QPoint(horizontalOffset(), verticalOffset()));
952         rect = rect.normalized();
953         update(rect.united(d->rubberBandRect));
954         d->rubberBandRect = rect;
955     }
956     if (!d->categoryDrawer) {
957         return;
958     }
959     QHash<QString, Private::Block>::ConstIterator it(d->blocks.constBegin());
960     while (it != d->blocks.constEnd()) {
961         const Private::Block &block = *it;
962         const QModelIndex categoryIndex = d->proxyModel->index(block.firstIndex.row(), d->proxyModel->sortColumn(), rootIndex());
963         QStyleOptionViewItem option(viewOptions());
964         const int height = d->categoryDrawer->categoryHeight(categoryIndex, option);
965         QPoint pos = d->blockPosition(it.key());
966         pos.ry() -= height;
967         option.rect.setTopLeft(pos);
968         option.rect.setWidth(d->viewportWidth() + d->categoryDrawer->leftMargin() + d->categoryDrawer->rightMargin());
969         option.rect.setHeight(height + d->blockHeight(it.key()));
970         option.rect = d->mapToViewport(option.rect);
971         const QPoint mousePos = viewport()->mapFromGlobal(QCursor::pos());
972         if (option.rect.contains(mousePos)) {
973             if (d->hoveredBlock->height != -1 && *d->hoveredBlock != block) {
974                 const QModelIndex categoryIndex = d->proxyModel->index(d->hoveredBlock->firstIndex.row(), d->proxyModel->sortColumn(), rootIndex());
975                 const QStyleOptionViewItem option = d->blockRect(categoryIndex);
976                 d->categoryDrawer->mouseLeft(categoryIndex, option.rect);
977                 *d->hoveredBlock = block;
978                 d->hoveredCategory = it.key();
979                 viewport()->update(option.rect);
980             } else if (d->hoveredBlock->height == -1) {
981                 *d->hoveredBlock = block;
982                 d->hoveredCategory = it.key();
983             } else {
984                 d->categoryDrawer->mouseMoved(categoryIndex, option.rect, event);
985             }
986             viewport()->update(option.rect);
987             return;
988         }
989         ++it;
990     }
991     if (d->hoveredBlock->height != -1) {
992         const QModelIndex categoryIndex = d->proxyModel->index(d->hoveredBlock->firstIndex.row(), d->proxyModel->sortColumn(), rootIndex());
993         const QStyleOptionViewItem option = d->blockRect(categoryIndex);
994         d->categoryDrawer->mouseLeft(categoryIndex, option.rect);
995         *d->hoveredBlock = Private::Block();
996         d->hoveredCategory = QString();
997         viewport()->update(option.rect);
998     }
999 }
1000 
mousePressEvent(QMouseEvent * event)1001 void KCategorizedView::mousePressEvent(QMouseEvent *event)
1002 {
1003     if (event->button() == Qt::LeftButton) {
1004         d->pressedPosition = event->pos();
1005         d->pressedPosition.rx() += horizontalOffset();
1006         d->pressedPosition.ry() += verticalOffset();
1007     }
1008     if (!d->categoryDrawer) {
1009         QListView::mousePressEvent(event);
1010         return;
1011     }
1012     QHash<QString, Private::Block>::ConstIterator it(d->blocks.constBegin());
1013     while (it != d->blocks.constEnd()) {
1014         const Private::Block &block = *it;
1015         const QModelIndex categoryIndex = d->proxyModel->index(block.firstIndex.row(), d->proxyModel->sortColumn(), rootIndex());
1016         const QStyleOptionViewItem option = d->blockRect(categoryIndex);
1017         const QPoint mousePos = viewport()->mapFromGlobal(QCursor::pos());
1018         if (option.rect.contains(mousePos)) {
1019             d->categoryDrawer->mouseButtonPressed(categoryIndex, option.rect, event);
1020             viewport()->update(option.rect);
1021             if (!event->isAccepted()) {
1022                 QListView::mousePressEvent(event);
1023             }
1024             return;
1025         }
1026         ++it;
1027     }
1028     QListView::mousePressEvent(event);
1029 }
1030 
mouseReleaseEvent(QMouseEvent * event)1031 void KCategorizedView::mouseReleaseEvent(QMouseEvent *event)
1032 {
1033     d->pressedPosition = QPoint();
1034     d->rubberBandRect = QRect();
1035     if (!d->categoryDrawer) {
1036         QListView::mouseReleaseEvent(event);
1037         return;
1038     }
1039     QHash<QString, Private::Block>::ConstIterator it(d->blocks.constBegin());
1040     while (it != d->blocks.constEnd()) {
1041         const Private::Block &block = *it;
1042         const QModelIndex categoryIndex = d->proxyModel->index(block.firstIndex.row(), d->proxyModel->sortColumn(), rootIndex());
1043         const QStyleOptionViewItem option = d->blockRect(categoryIndex);
1044         const QPoint mousePos = viewport()->mapFromGlobal(QCursor::pos());
1045         if (option.rect.contains(mousePos)) {
1046             d->categoryDrawer->mouseButtonReleased(categoryIndex, option.rect, event);
1047             viewport()->update(option.rect);
1048             if (!event->isAccepted()) {
1049                 QListView::mouseReleaseEvent(event);
1050             }
1051             return;
1052         }
1053         ++it;
1054     }
1055     QListView::mouseReleaseEvent(event);
1056 }
1057 
leaveEvent(QEvent * event)1058 void KCategorizedView::leaveEvent(QEvent *event)
1059 {
1060     QListView::leaveEvent(event);
1061     if (d->hoveredIndex.isValid()) {
1062         viewport()->update(visualRect(d->hoveredIndex));
1063         d->hoveredIndex = QModelIndex();
1064     }
1065     if (d->categoryDrawer && d->hoveredBlock->height != -1) {
1066         const QModelIndex categoryIndex = d->proxyModel->index(d->hoveredBlock->firstIndex.row(), d->proxyModel->sortColumn(), rootIndex());
1067         const QStyleOptionViewItem option = d->blockRect(categoryIndex);
1068         d->categoryDrawer->mouseLeft(categoryIndex, option.rect);
1069         *d->hoveredBlock = Private::Block();
1070         d->hoveredCategory = QString();
1071         viewport()->update(option.rect);
1072     }
1073 }
1074 
startDrag(Qt::DropActions supportedActions)1075 void KCategorizedView::startDrag(Qt::DropActions supportedActions)
1076 {
1077     QListView::startDrag(supportedActions);
1078 }
1079 
dragMoveEvent(QDragMoveEvent * event)1080 void KCategorizedView::dragMoveEvent(QDragMoveEvent *event)
1081 {
1082     QListView::dragMoveEvent(event);
1083     d->hoveredIndex = indexAt(event->pos());
1084 }
1085 
dragEnterEvent(QDragEnterEvent * event)1086 void KCategorizedView::dragEnterEvent(QDragEnterEvent *event)
1087 {
1088     QListView::dragEnterEvent(event);
1089 }
1090 
dragLeaveEvent(QDragLeaveEvent * event)1091 void KCategorizedView::dragLeaveEvent(QDragLeaveEvent *event)
1092 {
1093     QListView::dragLeaveEvent(event);
1094 }
1095 
dropEvent(QDropEvent * event)1096 void KCategorizedView::dropEvent(QDropEvent *event)
1097 {
1098     QListView::dropEvent(event);
1099 }
1100 
1101 //TODO: improve se we take into account collapsed blocks
1102 //TODO: take into account when there is no grid and no uniformItemSizes
moveCursor(CursorAction cursorAction,Qt::KeyboardModifiers modifiers)1103 QModelIndex KCategorizedView::moveCursor(CursorAction cursorAction,
1104         Qt::KeyboardModifiers modifiers)
1105 {
1106     if (!d->isCategorized() || viewMode() == QListView::ListMode) {
1107         return QListView::moveCursor(cursorAction, modifiers);
1108     }
1109 
1110     const QModelIndex current = currentIndex();
1111     const QRect currentRect = visualRect(current);
1112     if (!current.isValid()) {
1113         const int rowCount = d->proxyModel->rowCount(rootIndex());
1114         if (!rowCount) {
1115             return QModelIndex();
1116         }
1117         return d->proxyModel->index(0, modelColumn(), rootIndex());
1118     }
1119 
1120     switch (cursorAction) {
1121     case MoveLeft: {
1122         if (!current.row()) {
1123             return QModelIndex();
1124         }
1125         const QModelIndex previous = d->proxyModel->index(current.row() - 1, modelColumn(), rootIndex());
1126         const QRect previousRect = visualRect(previous);
1127         if (previousRect.top() == currentRect.top()) {
1128             return previous;
1129         }
1130 
1131         return QModelIndex();
1132     }
1133     case MoveRight: {
1134         if (current.row() == d->proxyModel->rowCount() - 1) {
1135             return QModelIndex();
1136         }
1137         const QModelIndex next = d->proxyModel->index(current.row() + 1, modelColumn(), rootIndex());
1138         const QRect nextRect = visualRect(next);
1139         if (nextRect.top() == currentRect.top()) {
1140             return next;
1141         }
1142 
1143         return QModelIndex();
1144     }
1145     case MoveDown: {
1146         if (d->hasGrid() || uniformItemSizes()) {
1147             const QModelIndex current = currentIndex();
1148             const QSize itemSize = d->hasGrid() ? gridSize()
1149                                    : sizeHintForIndex(current);
1150             const Private::Block &block = d->blocks[d->categoryForIndex(current)];
1151             const int maxItemsPerRow = qMax(d->viewportWidth() / itemSize.width(), 1);
1152             const bool canMove = current.row() + maxItemsPerRow < block.firstIndex.row() +
1153                                  block.items.count();
1154 
1155             if (canMove) {
1156                 return d->proxyModel->index(current.row() + maxItemsPerRow, modelColumn(), rootIndex());
1157             }
1158 
1159             const int currentRelativePos = (current.row() - block.firstIndex.row()) % maxItemsPerRow;
1160             const QModelIndex nextIndex = d->proxyModel->index(block.firstIndex.row() + block.items.count(), modelColumn(), rootIndex());
1161 
1162             if (!nextIndex.isValid()) {
1163                 return QModelIndex();
1164             }
1165 
1166             const Private::Block &nextBlock = d->blocks[d->categoryForIndex(nextIndex)];
1167 
1168             if (nextBlock.items.count() <= currentRelativePos) {
1169                 return QModelIndex();
1170             }
1171 
1172             if (currentRelativePos < (block.items.count() % maxItemsPerRow)) {
1173                 return d->proxyModel->index(nextBlock.firstIndex.row() + currentRelativePos, modelColumn(), rootIndex());
1174             }
1175 
1176         }
1177         return QModelIndex();
1178     }
1179     case MoveUp: {
1180         if (d->hasGrid() || uniformItemSizes()) {
1181             const QModelIndex current = currentIndex();
1182             const QSize itemSize = d->hasGrid() ? gridSize()
1183                                    : sizeHintForIndex(current);
1184             const Private::Block &block = d->blocks[d->categoryForIndex(current)];
1185             const int maxItemsPerRow = qMax(d->viewportWidth() / itemSize.width(), 1);
1186             const bool canMove = current.row() - maxItemsPerRow >= block.firstIndex.row();
1187 
1188             if (canMove) {
1189                 return d->proxyModel->index(current.row() - maxItemsPerRow, modelColumn(), rootIndex());
1190             }
1191 
1192             const int currentRelativePos = (current.row() - block.firstIndex.row()) % maxItemsPerRow;
1193             const QModelIndex prevIndex = d->proxyModel->index(block.firstIndex.row() - 1, modelColumn(), rootIndex());
1194 
1195             if (!prevIndex.isValid()) {
1196                 return QModelIndex();
1197             }
1198 
1199             const Private::Block &prevBlock = d->blocks[d->categoryForIndex(prevIndex)];
1200 
1201             if (prevBlock.items.count() <= currentRelativePos) {
1202                 return QModelIndex();
1203             }
1204 
1205             const int remainder = prevBlock.items.count() % maxItemsPerRow;
1206             if (currentRelativePos < remainder) {
1207                 return d->proxyModel->index(prevBlock.firstIndex.row() + prevBlock.items.count() - remainder + currentRelativePos, modelColumn(), rootIndex());
1208             }
1209 
1210             return QModelIndex();
1211         }
1212     }
1213     default:
1214         break;
1215     }
1216 
1217     return QModelIndex();
1218 }
1219 
rowsAboutToBeRemoved(const QModelIndex & parent,int start,int end)1220 void KCategorizedView::rowsAboutToBeRemoved(const QModelIndex &parent,
1221         int start,
1222         int end)
1223 {
1224     if (!d->isCategorized()) {
1225         QListView::rowsAboutToBeRemoved(parent, start, end);
1226         return;
1227     }
1228 
1229     *d->hoveredBlock = Private::Block();
1230     d->hoveredCategory = QString();
1231 
1232     if (end - start + 1 == d->proxyModel->rowCount()) {
1233         d->blocks.clear();
1234         QListView::rowsAboutToBeRemoved(parent, start, end);
1235         return;
1236     }
1237 
1238     // Removal feels a bit more complicated than insertion. Basically we can consider there are
1239     // 3 different cases when going to remove items. (*) represents an item, Items between ([) and
1240     // (]) are the ones which are marked for removal.
1241     //
1242     // - 1st case:
1243     //              ... * * * * * * [ * * * ...
1244     //
1245     //   The items marked for removal are the last part of this category. No need to mark any item
1246     //   of this category as in quarantine, because no special offset will be pushed to items at
1247     //   the right because of any changes (since the removed items are those on the right most part
1248     //   of the category).
1249     //
1250     // - 2nd case:
1251     //              ... * * * * * * ] * * * ...
1252     //
1253     //   The items marked for removal are the first part of this category. We have to mark as in
1254     //   quarantine all items in this category. Absolutely all. All items will have to be moved to
1255     //   the left (or moving up, because rows got a different offset).
1256     //
1257     // - 3rd case:
1258     //              ... * * [ * * * * ] * * ...
1259     //
1260     //   The items marked for removal are in between of this category. We have to mark as in
1261     //   quarantine only those items that are at the right of the end of the removal interval,
1262     //   (starting on "]").
1263     //
1264     // It hasn't been explicitly said, but when we remove, we have to mark all blocks that are
1265     // located under the top most affected category as in quarantine (the block itself, as a whole),
1266     // because such a change can force it to have a different offset (note that items themselves
1267     // contain relative positions to the block, so marking the block as in quarantine is enough).
1268     //
1269     // Also note that removal implicitly means that we have to update correctly firstIndex of each
1270     // block, and in general keep updated the internal information of elements.
1271 
1272     QStringList listOfCategoriesMarkedForRemoval;
1273 
1274     QString lastCategory;
1275     int alreadyRemoved = 0;
1276     for (int i = start; i <= end; ++i) {
1277         const QModelIndex index = d->proxyModel->index(i, modelColumn(), parent);
1278 
1279         Q_ASSERT(index.isValid());
1280 
1281         const QString category = d->categoryForIndex(index);
1282 
1283         if (lastCategory != category) {
1284             lastCategory = category;
1285             alreadyRemoved = 0;
1286         }
1287 
1288         Private::Block &block = d->blocks[category];
1289         block.items.removeAt(i - block.firstIndex.row() - alreadyRemoved);
1290         ++alreadyRemoved;
1291 
1292         if (block.items.isEmpty()) {
1293             listOfCategoriesMarkedForRemoval << category;
1294         }
1295 
1296         block.height = -1;
1297 
1298         viewport()->update();
1299     }
1300 
1301     //BEGIN: update the items that are in quarantine in affected categories
1302     {
1303         const QModelIndex lastIndex = d->proxyModel->index(end, modelColumn(), parent);
1304         const QString category = d->categoryForIndex(lastIndex);
1305         Private::Block &block = d->blocks[category];
1306         if (!block.items.isEmpty() && start <= block.firstIndex.row() && end >= block.firstIndex.row()) {
1307             block.firstIndex = d->proxyModel->index(end + 1, modelColumn(), parent);
1308         }
1309         block.quarantineStart = block.firstIndex;
1310     }
1311     //END: update the items that are in quarantine in affected categories
1312 
1313     Q_FOREACH (const QString &category, listOfCategoriesMarkedForRemoval) {
1314         d->blocks.remove(category);
1315     }
1316 
1317     //BEGIN: mark as in quarantine those categories that are under the affected ones
1318     {
1319         //BEGIN: order for marking as alternate those blocks that are alternate
1320         QList<Private::Block> blockList = d->blocks.values();
1321         std::sort(blockList.begin(), blockList.end(), Private::Block::lessThan);
1322         QList<int> firstIndexesRows;
1323         foreach (const Private::Block &block, blockList) {
1324             firstIndexesRows << block.firstIndex.row();
1325         }
1326         //END: order for marking as alternate those blocks that are alternate
1327         for (QHash<QString, Private::Block>::Iterator it = d->blocks.begin(); it != d->blocks.end(); ++it) {
1328             Private::Block &block = *it;
1329             if (block.firstIndex.row() > start) {
1330                 block.outOfQuarantine = false;
1331                 block.alternate = firstIndexesRows.indexOf(block.firstIndex.row()) % 2;
1332             } else if (block.firstIndex.row() == start) {
1333                 block.alternate = firstIndexesRows.indexOf(block.firstIndex.row()) % 2;
1334             }
1335         }
1336     }
1337     //END: mark as in quarantine those categories that are under the affected ones
1338 
1339     QListView::rowsAboutToBeRemoved(parent, start, end);
1340 }
1341 
updateGeometries()1342 void KCategorizedView::updateGeometries()
1343 {
1344     const int oldVerticalOffset = verticalOffset();
1345     const Qt::ScrollBarPolicy verticalP = verticalScrollBarPolicy(), horizontalP = horizontalScrollBarPolicy();
1346 
1347     //BEGIN bugs 213068, 287847 ------------------------------------------------------------
1348     /*
1349      * QListView::updateGeometries() has it's own opinion on whether the scrollbars should be visible (valid range) or not
1350      * and triggers a (sometimes additionally timered) resize through ::layoutChildren()
1351      * http://qt.gitorious.org/qt/qt/blobs/4.7/src/gui/itemviews/qlistview.cpp#line1499
1352      * (the comment above the main block isn't all accurate, layoutChldren is called regardless of the policy)
1353      *
1354      * As a result QListView and KCategorizedView occasionally started a race on the scrollbar visibility, effectively blocking the UI
1355      * So we prevent QListView from having an own opinion on the scrollbar visibility by
1356      * fixing it before calling the baseclass QListView::updateGeometries()
1357      *
1358      * Since the implicit show/hide by the followin range setting will cause further resizes if the policy is Qt::ScrollBarAsNeeded
1359      * we keep it static until we're done, then restore the original value and ultimately change the scrollbar visibility ourself.
1360      */
1361     if (d->isCategorized()) { // important! - otherwise we'd pollute the setting if the view is initially not categorized
1362         setVerticalScrollBarPolicy((verticalP == Qt::ScrollBarAlwaysOn || verticalScrollBar()->isVisibleTo(this)) ?
1363                                    Qt::ScrollBarAlwaysOn : Qt::ScrollBarAlwaysOff);
1364         setHorizontalScrollBarPolicy((horizontalP == Qt::ScrollBarAlwaysOn || horizontalScrollBar()->isVisibleTo(this)) ?
1365                                      Qt::ScrollBarAlwaysOn : Qt::ScrollBarAlwaysOff);
1366     }
1367     //END bugs 213068, 287847 --------------------------------------------------------------
1368 
1369     QListView::updateGeometries();
1370 
1371     if (!d->isCategorized()) {
1372         return;
1373     }
1374 
1375     const int rowCount = d->proxyModel->rowCount();
1376     if (!rowCount) {
1377         verticalScrollBar()->setRange(0, 0);
1378         // unconditional, see function end todo
1379         //BEGIN bugs 213068, 287847 ------------------------------------------------------------
1380         // restoring values from above ...
1381         horizontalScrollBar()->setRange(0, 0);
1382         setVerticalScrollBarPolicy(verticalP);
1383         setHorizontalScrollBarPolicy(horizontalP);
1384         //END bugs 213068, 287847 --------------------------------------------------------------
1385         return;
1386     }
1387 
1388     const QModelIndex lastIndex = d->proxyModel->index(rowCount - 1, modelColumn(), rootIndex());
1389     Q_ASSERT(lastIndex.isValid());
1390     QRect lastItemRect = visualRect(lastIndex);
1391 
1392     if (d->hasGrid()) {
1393         lastItemRect.setSize(lastItemRect.size().expandedTo(gridSize()));
1394     } else {
1395         if (uniformItemSizes()) {
1396             QSize itemSize = sizeHintForIndex(lastIndex);
1397             itemSize.setHeight(itemSize.height() + spacing());
1398             lastItemRect.setSize(itemSize);
1399         } else {
1400             QSize itemSize = sizeHintForIndex(lastIndex);
1401             const QString category = d->categoryForIndex(lastIndex);
1402             itemSize.setHeight(d->highestElementInLastRow(d->blocks[category]) + spacing());
1403             lastItemRect.setSize(itemSize);
1404         }
1405     }
1406 
1407     const int bottomRange = lastItemRect.bottomRight().y() + verticalOffset() - viewport()->height();
1408 
1409     if (verticalScrollMode() == ScrollPerItem) {
1410         verticalScrollBar()->setSingleStep(lastItemRect.height());
1411         const int rowsPerPage = qMax(viewport()->height() / lastItemRect.height(), 1);
1412         verticalScrollBar()->setPageStep(rowsPerPage * lastItemRect.height());
1413     }
1414 
1415     verticalScrollBar()->setRange(0, bottomRange);
1416     verticalScrollBar()->setValue(oldVerticalOffset);
1417 
1418     //TODO: also consider working with the horizontal scroll bar. since at this level I am not still
1419     //      supporting "top to bottom" flow, there is no real problem. If I support that someday
1420     //      (think how to draw categories), we would have to take care of the horizontal scroll bar too.
1421     //      In theory, as KCategorizedView has been designed, there is no need of horizontal scroll bar.
1422     horizontalScrollBar()->setRange(0, 0);
1423 
1424     //BEGIN bugs 213068, 287847 ------------------------------------------------------------
1425     // restoring values from above ...
1426     setVerticalScrollBarPolicy(verticalP);
1427     setHorizontalScrollBarPolicy(horizontalP);
1428     // ... and correct the visibility
1429     bool validRange = verticalScrollBar()->maximum() != verticalScrollBar()->minimum();
1430     if (verticalP == Qt::ScrollBarAsNeeded && (verticalScrollBar()->isVisibleTo(this) != validRange)) {
1431         verticalScrollBar()->setVisible(validRange);
1432     }
1433     validRange = horizontalScrollBar()->maximum() > horizontalScrollBar()->minimum();
1434     if (horizontalP == Qt::ScrollBarAsNeeded && (horizontalScrollBar()->isVisibleTo(this) != validRange)) {
1435         horizontalScrollBar()->setVisible(validRange);
1436     }
1437     //END bugs 213068, 287847 --------------------------------------------------------------
1438 }
1439 
currentChanged(const QModelIndex & current,const QModelIndex & previous)1440 void KCategorizedView::currentChanged(const QModelIndex &current,
1441                                       const QModelIndex &previous)
1442 {
1443     QListView::currentChanged(current, previous);
1444 }
1445 
dataChanged(const QModelIndex & topLeft,const QModelIndex & bottomRight,const QVector<int> & roles)1446 void KCategorizedView::dataChanged(const QModelIndex &topLeft,
1447                                    const QModelIndex &bottomRight,
1448                                    const QVector<int> &roles)
1449 {
1450     QListView::dataChanged(topLeft, bottomRight, roles);
1451     if (!d->isCategorized()) {
1452         return;
1453     }
1454 
1455     *d->hoveredBlock = Private::Block();
1456     d->hoveredCategory = QString();
1457 
1458     //BEGIN: since the model changed data, we need to reconsider item sizes
1459     int i = topLeft.row();
1460     int indexToCheck = i;
1461     QModelIndex categoryIndex;
1462     QString category;
1463     Private::Block *block;
1464     while (i <= bottomRight.row()) {
1465         const QModelIndex currIndex = d->proxyModel->index(i, modelColumn(), rootIndex());
1466         if (i == indexToCheck) {
1467             categoryIndex = d->proxyModel->index(i, d->proxyModel->sortColumn(), rootIndex());
1468             category = categoryIndex.data(KCategorizedSortFilterProxyModel::CategoryDisplayRole).toString();
1469             block = &d->blocks[category];
1470             block->quarantineStart = currIndex;
1471             indexToCheck = block->firstIndex.row() + block->items.count();
1472         }
1473         visualRect(currIndex);
1474         ++i;
1475     }
1476     //END: since the model changed data, we need to reconsider item sizes
1477 }
1478 
rowsInserted(const QModelIndex & parent,int start,int end)1479 void KCategorizedView::rowsInserted(const QModelIndex &parent,
1480                                     int start,
1481                                     int end)
1482 {
1483     QListView::rowsInserted(parent, start, end);
1484     if (!d->isCategorized()) {
1485         return;
1486     }
1487 
1488     *d->hoveredBlock = Private::Block();
1489     d->hoveredCategory = QString();
1490     d->rowsInserted(parent, start, end);
1491 }
1492 
1493 /*
1494 #ifndef KITEMVIEWS_NO_DEPRECATED
1495 void KCategorizedView::rowsInsertedArtifficial(const QModelIndex &parent,
1496         int start,
1497         int end)
1498 {
1499     Q_UNUSED(parent);
1500     Q_UNUSED(start);
1501     Q_UNUSED(end);
1502 }
1503 #endif
1504 
1505 #ifndef KITEMVIEWS_NO_DEPRECATED
1506 void KCategorizedView::rowsRemoved(const QModelIndex &parent,
1507                                    int start,
1508                                    int end)
1509 {
1510     Q_UNUSED(parent);
1511     Q_UNUSED(start);
1512     Q_UNUSED(end);
1513 }
1514 #endif
1515 */
1516 
slotLayoutChanged()1517 void KCategorizedView::slotLayoutChanged()
1518 {
1519     if (!d->isCategorized()) {
1520         return;
1521     }
1522 
1523     d->blocks.clear();
1524     *d->hoveredBlock = Private::Block();
1525     d->hoveredCategory = QString();
1526     if (d->proxyModel->rowCount()) {
1527         d->rowsInserted(rootIndex(), 0, d->proxyModel->rowCount() - 1);
1528     }
1529 }
1530 
1531 //END: Public part
1532 
1533 #include "moc_kcategorizedview.cpp"
1534