KItemViews

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

KDE's Doxygen guidelines are available online.