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