KItemViews

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

KDE's Doxygen guidelines are available online.