]> cloud.milkyroute.net Git - dolphin.git/blob - src/kitemviews/private/kitemlistviewlayouter.cpp
4c22b4dbcbac26714888e4fed1adb6c82acf0fb3
[dolphin.git] / src / kitemviews / private / kitemlistviewlayouter.cpp
1 /*
2 * SPDX-FileCopyrightText: 2011 Peter Penz <peter.penz19@gmail.com>
3 *
4 * SPDX-License-Identifier: GPL-2.0-or-later
5 */
6
7 #include "kitemlistviewlayouter.h"
8 #include "dolphindebug.h"
9 #include "kitemlistsizehintresolver.h"
10 #include "kitemviews/kitemmodelbase.h"
11
12 #include <QGuiApplication>
13 #include <QScopeGuard>
14
15 // #define KITEMLISTVIEWLAYOUTER_DEBUG
16
17 KItemListViewLayouter::KItemListViewLayouter(KItemListSizeHintResolver* sizeHintResolver, QObject* parent) :
18 QObject(parent),
19 m_dirty(true),
20 m_visibleIndexesDirty(true),
21 m_scrollOrientation(Qt::Vertical),
22 m_size(),
23 m_itemSize(128, 128),
24 m_itemMargin(),
25 m_headerHeight(0),
26 m_model(nullptr),
27 m_sizeHintResolver(sizeHintResolver),
28 m_scrollOffset(0),
29 m_maximumScrollOffset(0),
30 m_itemOffset(0),
31 m_maximumItemOffset(0),
32 m_firstVisibleIndex(-1),
33 m_lastVisibleIndex(-1),
34 m_columnWidth(0),
35 m_xPosInc(0),
36 m_columnCount(0),
37 m_rowOffsets(),
38 m_columnOffsets(),
39 m_groupItemIndexes(),
40 m_groupHeaderHeight(0),
41 m_groupHeaderMargin(0),
42 m_itemInfos()
43 {
44 Q_ASSERT(m_sizeHintResolver);
45 }
46
47 KItemListViewLayouter::~KItemListViewLayouter()
48 {
49 }
50
51 void KItemListViewLayouter::setScrollOrientation(Qt::Orientation orientation)
52 {
53 if (m_scrollOrientation != orientation) {
54 m_scrollOrientation = orientation;
55 m_dirty = true;
56 }
57 }
58
59 Qt::Orientation KItemListViewLayouter::scrollOrientation() const
60 {
61 return m_scrollOrientation;
62 }
63
64 void KItemListViewLayouter::setSize(const QSizeF& size)
65 {
66 if (m_size != size) {
67 if (m_scrollOrientation == Qt::Vertical) {
68 if (m_size.width() != size.width()) {
69 m_dirty = true;
70 }
71 } else if (m_size.height() != size.height()) {
72 m_dirty = true;
73 }
74
75 m_size = size;
76 m_visibleIndexesDirty = true;
77 }
78 }
79
80 QSizeF KItemListViewLayouter::size() const
81 {
82 return m_size;
83 }
84
85 void KItemListViewLayouter::setItemSize(const QSizeF& size)
86 {
87 if (m_itemSize != size) {
88 m_itemSize = size;
89 m_dirty = true;
90 }
91 }
92
93 QSizeF KItemListViewLayouter::itemSize() const
94 {
95 return m_itemSize;
96 }
97
98 void KItemListViewLayouter::setItemMargin(const QSizeF& margin)
99 {
100 if (m_itemMargin != margin) {
101 m_itemMargin = margin;
102 m_dirty = true;
103 }
104 }
105
106 QSizeF KItemListViewLayouter::itemMargin() const
107 {
108 return m_itemMargin;
109 }
110
111 void KItemListViewLayouter::setHeaderHeight(qreal height)
112 {
113 if (m_headerHeight != height) {
114 m_headerHeight = height;
115 m_dirty = true;
116 }
117 }
118
119 qreal KItemListViewLayouter::headerHeight() const
120 {
121 return m_headerHeight;
122 }
123
124 void KItemListViewLayouter::setGroupHeaderHeight(qreal height)
125 {
126 if (m_groupHeaderHeight != height) {
127 m_groupHeaderHeight = height;
128 m_dirty = true;
129 }
130 }
131
132 qreal KItemListViewLayouter::groupHeaderHeight() const
133 {
134 return m_groupHeaderHeight;
135 }
136
137 void KItemListViewLayouter::setGroupHeaderMargin(qreal margin)
138 {
139 if (m_groupHeaderMargin != margin) {
140 m_groupHeaderMargin = margin;
141 m_dirty = true;
142 }
143 }
144
145 qreal KItemListViewLayouter::groupHeaderMargin() const
146 {
147 return m_groupHeaderMargin;
148 }
149
150 void KItemListViewLayouter::setScrollOffset(qreal offset)
151 {
152 if (m_scrollOffset != offset) {
153 m_scrollOffset = offset;
154 m_visibleIndexesDirty = true;
155 }
156 }
157
158 qreal KItemListViewLayouter::scrollOffset() const
159 {
160 return m_scrollOffset;
161 }
162
163 qreal KItemListViewLayouter::maximumScrollOffset() const
164 {
165 const_cast<KItemListViewLayouter*>(this)->doLayout();
166 return m_maximumScrollOffset;
167 }
168
169 void KItemListViewLayouter::setItemOffset(qreal offset)
170 {
171 if (m_itemOffset != offset) {
172 m_itemOffset = offset;
173 m_visibleIndexesDirty = true;
174 }
175 }
176
177 qreal KItemListViewLayouter::itemOffset() const
178 {
179 return m_itemOffset;
180 }
181
182 qreal KItemListViewLayouter::maximumItemOffset() const
183 {
184 const_cast<KItemListViewLayouter*>(this)->doLayout();
185 return m_maximumItemOffset;
186 }
187
188 void KItemListViewLayouter::setModel(const KItemModelBase* model)
189 {
190 if (m_model != model) {
191 m_model = model;
192 m_dirty = true;
193 }
194 }
195
196 const KItemModelBase* KItemListViewLayouter::model() const
197 {
198 return m_model;
199 }
200
201 int KItemListViewLayouter::firstVisibleIndex() const
202 {
203 const_cast<KItemListViewLayouter*>(this)->doLayout();
204 return m_firstVisibleIndex;
205 }
206
207 int KItemListViewLayouter::lastVisibleIndex() const
208 {
209 const_cast<KItemListViewLayouter*>(this)->doLayout();
210 return m_lastVisibleIndex;
211 }
212
213 QRectF KItemListViewLayouter::itemRect(int index) const
214 {
215 const_cast<KItemListViewLayouter*>(this)->doLayout();
216 if (index < 0 || index >= m_itemInfos.count()) {
217 return QRectF();
218 }
219
220 QSizeF sizeHint = m_sizeHintResolver->sizeHint(index);
221
222 const qreal x = m_columnOffsets.at(m_itemInfos.at(index).column);
223 const qreal y = m_rowOffsets.at(m_itemInfos.at(index).row);
224
225 if (m_scrollOrientation == Qt::Horizontal) {
226 // Rotate the logical direction which is always vertical by 90°
227 // to get the physical horizontal direction
228 QPointF pos(y, x);
229 pos.rx() -= m_scrollOffset;
230 sizeHint.transpose();
231 return QRectF(pos, sizeHint);
232 }
233
234 if (sizeHint.width() <= 0) {
235 // In Details View, a size hint with negative width is used internally.
236 sizeHint.rwidth() = m_itemSize.width();
237 }
238
239 const QPointF pos(x - m_itemOffset, y - m_scrollOffset);
240 return QRectF(pos, sizeHint);
241 }
242
243 QRectF KItemListViewLayouter::groupHeaderRect(int index) const
244 {
245 const_cast<KItemListViewLayouter*>(this)->doLayout();
246
247 const QRectF firstItemRect = itemRect(index);
248 QPointF pos = firstItemRect.topLeft();
249 if (pos.isNull()) {
250 return QRectF();
251 }
252
253 QSizeF size;
254 if (m_scrollOrientation == Qt::Vertical) {
255 pos.rx() = 0;
256 pos.ry() -= m_groupHeaderHeight;
257 size = QSizeF(m_size.width(), m_groupHeaderHeight);
258 } else {
259 pos.rx() -= m_itemMargin.width();
260 pos.ry() = 0;
261
262 // Determine the maximum width used in the current column. As the
263 // scroll-direction is Qt::Horizontal and m_itemRects is accessed
264 // directly, the logical height represents the visual width, and
265 // the logical row represents the column.
266 qreal headerWidth = minimumGroupHeaderWidth();
267 const int row = m_itemInfos[index].row;
268 const int maxIndex = m_itemInfos.count() - 1;
269 while (index <= maxIndex) {
270 if (m_itemInfos[index].row != row) {
271 break;
272 }
273
274 const qreal itemWidth = (m_scrollOrientation == Qt::Vertical)
275 ? m_sizeHintResolver->sizeHint(index).width()
276 : m_sizeHintResolver->sizeHint(index).height();
277
278 if (itemWidth > headerWidth) {
279 headerWidth = itemWidth;
280 }
281
282 ++index;
283 }
284
285 size = QSizeF(headerWidth, m_size.height());
286 }
287 return QRectF(pos, size);
288 }
289
290 int KItemListViewLayouter::itemColumn(int index) const
291 {
292 const_cast<KItemListViewLayouter*>(this)->doLayout();
293 if (index < 0 || index >= m_itemInfos.count()) {
294 return -1;
295 }
296
297 return (m_scrollOrientation == Qt::Vertical)
298 ? m_itemInfos[index].column
299 : m_itemInfos[index].row;
300 }
301
302 int KItemListViewLayouter::itemRow(int index) const
303 {
304 const_cast<KItemListViewLayouter*>(this)->doLayout();
305 if (index < 0 || index >= m_itemInfos.count()) {
306 return -1;
307 }
308
309 return (m_scrollOrientation == Qt::Vertical)
310 ? m_itemInfos[index].row
311 : m_itemInfos[index].column;
312 }
313
314 int KItemListViewLayouter::maximumVisibleItems() const
315 {
316 const_cast<KItemListViewLayouter*>(this)->doLayout();
317
318 const int height = static_cast<int>(m_size.height());
319 const int rowHeight = static_cast<int>(m_itemSize.height());
320 int rows = height / rowHeight;
321 if (height % rowHeight != 0) {
322 ++rows;
323 }
324
325 return rows * m_columnCount;
326 }
327
328 bool KItemListViewLayouter::isFirstGroupItem(int itemIndex) const
329 {
330 const_cast<KItemListViewLayouter*>(this)->doLayout();
331 return m_groupItemIndexes.contains(itemIndex);
332 }
333
334 void KItemListViewLayouter::markAsDirty()
335 {
336 m_dirty = true;
337 }
338
339
340 #ifndef QT_NO_DEBUG
341 bool KItemListViewLayouter::isDirty()
342 {
343 return m_dirty;
344 }
345 #endif
346
347 void KItemListViewLayouter::doLayout()
348 {
349 // we always want to update visible indexes after performing a layout
350 auto qsg = qScopeGuard([this] { updateVisibleIndexes(); });
351
352 if (!m_dirty) {
353 return;
354 }
355
356 #ifdef KITEMLISTVIEWLAYOUTER_DEBUG
357 QElapsedTimer timer;
358 timer.start();
359 #endif
360 m_visibleIndexesDirty = true;
361
362 QSizeF itemSize = m_itemSize;
363 QSizeF itemMargin = m_itemMargin;
364 QSizeF size = m_size;
365
366 const bool grouped = createGroupHeaders();
367
368 const bool horizontalScrolling = (m_scrollOrientation == Qt::Horizontal);
369 if (horizontalScrolling) {
370 // Flip everything so that the layout logically can work like having
371 // a vertical scrolling
372 itemSize.transpose();
373 itemMargin.transpose();
374 size.transpose();
375
376 if (grouped) {
377 // In the horizontal scrolling case all groups are aligned
378 // at the top, which decreases the available height. For the
379 // flipped data this means that the width must be decreased.
380 size.rwidth() -= m_groupHeaderHeight;
381 }
382 }
383
384 m_columnWidth = itemSize.width() + itemMargin.width();
385 const qreal widthForColumns = size.width() - itemMargin.width();
386 m_columnCount = qMax(1, int(widthForColumns / m_columnWidth));
387 m_xPosInc = itemMargin.width();
388
389 const int itemCount = m_model->count();
390 if (itemCount > m_columnCount && m_columnWidth >= 32) {
391 // Apply the unused width equally to each column
392 const qreal unusedWidth = widthForColumns - m_columnCount * m_columnWidth;
393 if (unusedWidth > 0) {
394 const qreal columnInc = unusedWidth / (m_columnCount + 1);
395 m_columnWidth += columnInc;
396 m_xPosInc += columnInc;
397 }
398 }
399
400 m_itemInfos.resize(itemCount);
401
402 // Calculate the offset of each column, i.e., the x-coordinate where the column starts.
403 m_columnOffsets.resize(m_columnCount);
404 qreal currentOffset = QGuiApplication::isRightToLeft() ? widthForColumns : m_xPosInc;
405
406 if (grouped && horizontalScrolling) {
407 // All group headers will always be aligned on the top and not
408 // flipped like the other properties.
409 currentOffset += m_groupHeaderHeight;
410 }
411
412 if (QGuiApplication::isLeftToRight()) for (int column = 0; column < m_columnCount; ++column) {
413 m_columnOffsets[column] = currentOffset;
414 currentOffset += m_columnWidth;
415 }
416 else for (int column = 0; column < m_columnCount; ++column) {
417 m_columnOffsets[column] = currentOffset - m_columnWidth;
418 currentOffset -= m_columnWidth;
419 }
420
421 // Prepare the QVector which stores the y-coordinate for each new row.
422 int numberOfRows = (itemCount + m_columnCount - 1) / m_columnCount;
423 if (grouped && m_columnCount > 1) {
424 // In the worst case, a new row will be started for every group.
425 // We could calculate the exact number of rows now to prevent that we reserve
426 // too much memory, but the code required to do that might need much more
427 // memory than it would save in the average case.
428 numberOfRows += m_groupItemIndexes.count();
429 }
430 m_rowOffsets.resize(numberOfRows);
431
432 qreal y = m_headerHeight + itemMargin.height();
433 int row = 0;
434
435 int index = 0;
436 while (index < itemCount) {
437 qreal maxItemHeight = itemSize.height();
438
439 if (grouped) {
440 if (m_groupItemIndexes.contains(index)) {
441 // The item is the first item of a group.
442 // Increase the y-position to provide space
443 // for the group header.
444 if (index > 0) {
445 // Only add a margin if there has been added another
446 // group already before
447 y += m_groupHeaderMargin;
448 } else if (!horizontalScrolling) {
449 // The first group header should be aligned on top
450 y -= itemMargin.height();
451 }
452
453 if (!horizontalScrolling) {
454 y += m_groupHeaderHeight;
455 }
456 }
457 }
458
459 m_rowOffsets[row] = y;
460
461 int column = 0;
462 while (index < itemCount && column < m_columnCount) {
463 qreal requiredItemHeight = itemSize.height();
464 const QSizeF sizeHint = m_sizeHintResolver->sizeHint(index);
465 const qreal sizeHintHeight = sizeHint.height();
466 if (sizeHintHeight > requiredItemHeight) {
467 requiredItemHeight = sizeHintHeight;
468 }
469
470 ItemInfo& itemInfo = m_itemInfos[index];
471 itemInfo.column = column;
472 itemInfo.row = row;
473
474 if (grouped && horizontalScrolling) {
475 // When grouping is enabled in the horizontal mode, the header alignment
476 // looks like this:
477 // Header-1 Header-2 Header-3
478 // Item 1 Item 4 Item 7
479 // Item 2 Item 5 Item 8
480 // Item 3 Item 6 Item 9
481 // In this case 'requiredItemHeight' represents the column-width. We don't
482 // check the content of the header in the layouter to determine the required
483 // width, hence assure that at least a minimal width of 15 characters is given
484 // (in average a character requires the halve width of the font height).
485 //
486 // TODO: Let the group headers provide a minimum width and respect this width here
487 const qreal headerWidth = minimumGroupHeaderWidth();
488 if (requiredItemHeight < headerWidth) {
489 requiredItemHeight = headerWidth;
490 }
491 }
492
493 maxItemHeight = qMax(maxItemHeight, requiredItemHeight);
494 ++index;
495 ++column;
496
497 if (grouped && m_groupItemIndexes.contains(index)) {
498 // The item represents the first index of a group
499 // and must aligned in the first column
500 break;
501 }
502 }
503
504 y += maxItemHeight + itemMargin.height();
505 ++row;
506 }
507
508 if (itemCount > 0) {
509 m_maximumScrollOffset = y;
510 m_maximumItemOffset = m_columnCount * m_columnWidth;
511 } else {
512 m_maximumScrollOffset = 0;
513 m_maximumItemOffset = 0;
514 }
515
516 #ifdef KITEMLISTVIEWLAYOUTER_DEBUG
517 qCDebug(DolphinDebug) << "[TIME] doLayout() for " << m_model->count() << "items:" << timer.elapsed();
518 #endif
519 m_dirty = false;
520 }
521
522 void KItemListViewLayouter::updateVisibleIndexes()
523 {
524 if (!m_visibleIndexesDirty) {
525 return;
526 }
527
528 Q_ASSERT(!m_dirty);
529
530 if (m_model->count() <= 0) {
531 m_firstVisibleIndex = -1;
532 m_lastVisibleIndex = -1;
533 m_visibleIndexesDirty = false;
534 return;
535 }
536
537 const int maxIndex = m_model->count() - 1;
538
539 // Calculate the first visible index that is fully visible
540 int min = 0;
541 int max = maxIndex;
542 int mid = 0;
543 do {
544 mid = (min + max) / 2;
545 if (m_rowOffsets.at(m_itemInfos[mid].row) < m_scrollOffset) {
546 min = mid + 1;
547 } else {
548 max = mid - 1;
549 }
550 } while (min <= max);
551
552 if (mid > 0) {
553 // Include the row before the first fully visible index, as it might
554 // be partly visible
555 if (m_rowOffsets.at(m_itemInfos[mid].row) >= m_scrollOffset) {
556 --mid;
557 Q_ASSERT(m_rowOffsets.at(m_itemInfos[mid].row) < m_scrollOffset);
558 }
559
560 const int firstVisibleRow = m_itemInfos[mid].row;
561 while (mid > 0 && m_itemInfos[mid - 1].row == firstVisibleRow) {
562 --mid;
563 }
564 }
565 m_firstVisibleIndex = mid;
566
567 // Calculate the last visible index that is (at least partly) visible
568 const int visibleHeight = (m_scrollOrientation == Qt::Horizontal) ? m_size.width() : m_size.height();
569 qreal bottom = m_scrollOffset + visibleHeight;
570 if (m_model->groupedSorting()) {
571 bottom += m_groupHeaderHeight;
572 }
573
574 min = m_firstVisibleIndex;
575 max = maxIndex;
576 do {
577 mid = (min + max) / 2;
578 if (m_rowOffsets.at(m_itemInfos[mid].row) <= bottom) {
579 min = mid + 1;
580 } else {
581 max = mid - 1;
582 }
583 } while (min <= max);
584
585 while (mid > 0 && m_rowOffsets.at(m_itemInfos[mid].row) > bottom) {
586 --mid;
587 }
588 m_lastVisibleIndex = mid;
589
590 m_visibleIndexesDirty = false;
591 }
592
593 bool KItemListViewLayouter::createGroupHeaders()
594 {
595 if (!m_model->groupedSorting()) {
596 return false;
597 }
598
599 m_groupItemIndexes.clear();
600
601 const QList<QPair<int, QVariant> > groups = m_model->groups();
602 if (groups.isEmpty()) {
603 return false;
604 }
605
606 for (int i = 0; i < groups.count(); ++i) {
607 const int firstItemIndex = groups.at(i).first;
608 m_groupItemIndexes.insert(firstItemIndex);
609 }
610
611 return true;
612 }
613
614 qreal KItemListViewLayouter::minimumGroupHeaderWidth() const
615 {
616 return 100;
617 }
618