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