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