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