2 * SPDX-FileCopyrightText: 2011 Peter Penz <peter.penz19@gmail.com>
4 * SPDX-License-Identifier: GPL-2.0-or-later
7 #include "kitemlistviewlayouter.h"
8 #include "dolphindebug.h"
9 #include "kitemlistsizehintresolver.h"
10 #include "kitemviews/kitemmodelbase.h"
12 #include <QGuiApplication>
13 #include <QScopeGuard>
15 // #define KITEMLISTVIEWLAYOUTER_DEBUG
17 KItemListViewLayouter::KItemListViewLayouter(KItemListSizeHintResolver
*sizeHintResolver
, QObject
*parent
)
20 , m_visibleIndexesDirty(true)
21 , m_scrollOrientation(Qt::Vertical
)
23 , m_itemSize(128, 128)
27 , m_sizeHintResolver(sizeHintResolver
)
29 , m_maximumScrollOffset(0)
31 , m_maximumItemOffset(0)
32 , m_firstVisibleIndex(-1)
33 , m_lastVisibleIndex(-1)
39 , m_groupItemIndexes()
40 , m_groupHeaderHeight(0)
41 , m_groupHeaderMargin(0)
43 , m_statusBarOffset(0)
45 Q_ASSERT(m_sizeHintResolver
);
48 KItemListViewLayouter::~KItemListViewLayouter()
52 void KItemListViewLayouter::setScrollOrientation(Qt::Orientation orientation
)
54 if (m_scrollOrientation
!= orientation
) {
55 m_scrollOrientation
= orientation
;
60 Qt::Orientation
KItemListViewLayouter::scrollOrientation() const
62 return m_scrollOrientation
;
65 void KItemListViewLayouter::setSize(const QSizeF
&size
)
68 if (m_scrollOrientation
== Qt::Vertical
) {
69 if (m_size
.width() != size
.width()) {
72 } else if (m_size
.height() != size
.height()) {
77 m_visibleIndexesDirty
= true;
81 QSizeF
KItemListViewLayouter::size() const
86 void KItemListViewLayouter::setItemSize(const QSizeF
&size
)
88 if (m_itemSize
!= size
) {
94 QSizeF
KItemListViewLayouter::itemSize() const
99 void KItemListViewLayouter::setItemMargin(const QSizeF
&margin
)
101 if (m_itemMargin
!= margin
) {
102 m_itemMargin
= margin
;
107 QSizeF
KItemListViewLayouter::itemMargin() const
112 void KItemListViewLayouter::setHeaderHeight(qreal height
)
114 if (m_headerHeight
!= height
) {
115 m_headerHeight
= height
;
120 qreal
KItemListViewLayouter::headerHeight() const
122 return m_headerHeight
;
125 void KItemListViewLayouter::setGroupHeaderHeight(qreal height
)
127 if (m_groupHeaderHeight
!= height
) {
128 m_groupHeaderHeight
= height
;
133 qreal
KItemListViewLayouter::groupHeaderHeight() const
135 return m_groupHeaderHeight
;
138 void KItemListViewLayouter::setGroupHeaderMargin(qreal margin
)
140 if (m_groupHeaderMargin
!= margin
) {
141 m_groupHeaderMargin
= margin
;
146 qreal
KItemListViewLayouter::groupHeaderMargin() const
148 return m_groupHeaderMargin
;
151 void KItemListViewLayouter::setScrollOffset(qreal offset
)
153 if (m_scrollOffset
!= offset
) {
154 m_scrollOffset
= offset
;
155 m_visibleIndexesDirty
= true;
159 qreal
KItemListViewLayouter::scrollOffset() const
161 return m_scrollOffset
;
164 qreal
KItemListViewLayouter::maximumScrollOffset() const
166 const_cast<KItemListViewLayouter
*>(this)->doLayout();
167 return m_maximumScrollOffset
;
170 void KItemListViewLayouter::setItemOffset(qreal offset
)
172 if (m_itemOffset
!= offset
) {
173 m_itemOffset
= offset
;
174 m_visibleIndexesDirty
= true;
178 qreal
KItemListViewLayouter::itemOffset() const
183 qreal
KItemListViewLayouter::maximumItemOffset() const
185 const_cast<KItemListViewLayouter
*>(this)->doLayout();
186 return m_maximumItemOffset
;
189 void KItemListViewLayouter::setModel(const KItemModelBase
*model
)
191 if (m_model
!= model
) {
197 const KItemModelBase
*KItemListViewLayouter::model() const
202 int KItemListViewLayouter::firstVisibleIndex() const
204 const_cast<KItemListViewLayouter
*>(this)->doLayout();
205 return m_firstVisibleIndex
;
208 int KItemListViewLayouter::lastVisibleIndex() const
210 const_cast<KItemListViewLayouter
*>(this)->doLayout();
211 return m_lastVisibleIndex
;
214 QRectF
KItemListViewLayouter::itemRect(int index
) const
216 const_cast<KItemListViewLayouter
*>(this)->doLayout();
217 if (index
< 0 || index
>= m_itemInfos
.count()) {
221 QSizeF sizeHint
= m_sizeHintResolver
->sizeHint(index
);
223 const qreal x
= m_columnOffsets
.at(m_itemInfos
.at(index
).column
);
224 const qreal y
= m_rowOffsets
.at(m_itemInfos
.at(index
).row
);
226 if (m_scrollOrientation
== Qt::Horizontal
) {
227 // Rotate the logical direction which is always vertical by 90°
228 // to get the physical horizontal direction
230 sizeHint
.transpose();
231 if (QGuiApplication::isRightToLeft()) {
232 pos
.rx() = m_size
.width() - 1 + m_scrollOffset
- pos
.x() - sizeHint
.width();
234 pos
.rx() -= m_scrollOffset
;
236 return QRectF(pos
, sizeHint
);
239 if (sizeHint
.width() <= 0) {
240 // In Details View, a size hint with negative width is used internally.
241 sizeHint
.rwidth() = m_itemSize
.width();
244 const QPointF
pos(x
- m_itemOffset
, y
- m_scrollOffset
);
245 return QRectF(pos
, sizeHint
);
248 QRectF
KItemListViewLayouter::groupHeaderRect(int index
) const
250 const_cast<KItemListViewLayouter
*>(this)->doLayout();
252 const QRectF firstItemRect
= itemRect(index
);
253 QPointF pos
= firstItemRect
.topLeft();
259 if (m_scrollOrientation
== Qt::Vertical
) {
261 pos
.ry() -= m_groupHeaderHeight
;
262 size
= QSizeF(m_size
.width(), m_groupHeaderHeight
);
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
) {
278 const qreal itemWidth
=
279 (m_scrollOrientation
== Qt::Vertical
) ? m_sizeHintResolver
->sizeHint(index
).width() : m_sizeHintResolver
->sizeHint(index
).height();
281 if (itemWidth
> headerWidth
) {
282 headerWidth
= itemWidth
;
288 size
= QSizeF(headerWidth
, m_size
.height());
290 if (QGuiApplication::isRightToLeft()) {
291 pos
.setX(firstItemRect
.right() + m_itemMargin
.width() - size
.width());
293 pos
.rx() -= m_itemMargin
.width();
297 return QRectF(pos
, size
);
300 int KItemListViewLayouter::itemColumn(int index
) const
302 const_cast<KItemListViewLayouter
*>(this)->doLayout();
303 if (index
< 0 || index
>= m_itemInfos
.count()) {
307 return (m_scrollOrientation
== Qt::Vertical
) ? m_itemInfos
[index
].column
: m_itemInfos
[index
].row
;
310 int KItemListViewLayouter::itemRow(int index
) const
312 const_cast<KItemListViewLayouter
*>(this)->doLayout();
313 if (index
< 0 || index
>= m_itemInfos
.count()) {
317 return (m_scrollOrientation
== Qt::Vertical
) ? m_itemInfos
[index
].row
: m_itemInfos
[index
].column
;
320 int KItemListViewLayouter::maximumVisibleItems() const
322 const_cast<KItemListViewLayouter
*>(this)->doLayout();
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) {
331 return rows
* m_columnCount
;
334 bool KItemListViewLayouter::isFirstGroupItem(int itemIndex
) const
336 const_cast<KItemListViewLayouter
*>(this)->doLayout();
337 return m_groupItemIndexes
.contains(itemIndex
);
340 void KItemListViewLayouter::markAsDirty()
345 void KItemListViewLayouter::setStatusBarOffset(int offset
)
347 if (m_statusBarOffset
!= offset
) {
348 m_statusBarOffset
= offset
;
353 bool KItemListViewLayouter::isDirty()
359 void KItemListViewLayouter::doLayout()
361 // we always want to update visible indexes after performing a layout
362 auto qsg
= qScopeGuard([this] {
363 updateVisibleIndexes();
370 #ifdef KITEMLISTVIEWLAYOUTER_DEBUG
374 m_visibleIndexesDirty
= true;
376 QSizeF itemSize
= m_itemSize
;
377 QSizeF itemMargin
= m_itemMargin
;
378 QSizeF size
= m_size
;
380 const bool grouped
= createGroupHeaders();
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();
389 size
.rwidth() -= m_statusBarOffset
;
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
;
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();
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
;
416 m_itemInfos
.resize(itemCount
);
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
;
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
;
429 for (int column
= 0; column
< m_columnCount
; ++column
) {
430 if (horizontalScrolling
) {
431 m_columnOffsets
[column
] = currentOffset
+ column
* m_columnWidth
;
433 currentOffset
-= m_columnWidth
;
434 m_columnOffsets
[column
] = currentOffset
;
438 for (int column
= 0; column
< m_columnCount
; ++column
) {
439 m_columnOffsets
[column
] = currentOffset
;
440 currentOffset
+= m_columnWidth
;
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();
453 m_rowOffsets
.resize(numberOfRows
);
455 qreal y
= m_headerHeight
+ itemMargin
.height();
459 while (index
< itemCount
) {
460 qreal maxItemHeight
= itemSize
.height();
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.
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();
476 if (!horizontalScrolling
) {
477 y
+= m_groupHeaderHeight
;
482 m_rowOffsets
[row
] = y
;
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
;
493 ItemInfo
&itemInfo
= m_itemInfos
[index
];
494 itemInfo
.column
= column
;
497 if (grouped
&& horizontalScrolling
) {
498 // When grouping is enabled in the horizontal mode, the header alignment
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).
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
;
516 maxItemHeight
= qMax(maxItemHeight
, requiredItemHeight
);
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
527 y
+= maxItemHeight
+ itemMargin
.height();
532 m_maximumScrollOffset
= y
;
533 m_maximumItemOffset
= m_columnCount
* m_columnWidth
;
535 m_maximumScrollOffset
= 0;
536 m_maximumItemOffset
= 0;
539 #ifdef KITEMLISTVIEWLAYOUTER_DEBUG
540 qCDebug(DolphinDebug
) << "[TIME] doLayout() for " << m_model
->count() << "items:" << timer
.elapsed();
545 void KItemListViewLayouter::updateVisibleIndexes()
547 if (!m_visibleIndexesDirty
) {
553 if (m_model
->count() <= 0) {
554 m_firstVisibleIndex
= -1;
555 m_lastVisibleIndex
= -1;
556 m_visibleIndexesDirty
= false;
560 const int maxIndex
= m_model
->count() - 1;
562 // Calculate the first visible index that is fully visible
567 mid
= (min
+ max
) / 2;
568 if (m_rowOffsets
.at(m_itemInfos
[mid
].row
) < m_scrollOffset
) {
573 } while (min
<= max
);
576 // Include the row before the first fully visible index, as it might
578 if (m_rowOffsets
.at(m_itemInfos
[mid
].row
) >= m_scrollOffset
) {
580 Q_ASSERT(m_rowOffsets
.at(m_itemInfos
[mid
].row
) < m_scrollOffset
);
583 const int firstVisibleRow
= m_itemInfos
[mid
].row
;
584 while (mid
> 0 && m_itemInfos
[mid
- 1].row
== firstVisibleRow
) {
588 m_firstVisibleIndex
= mid
;
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
;
597 min
= m_firstVisibleIndex
;
600 mid
= (min
+ max
) / 2;
601 if (m_rowOffsets
.at(m_itemInfos
[mid
].row
) <= bottom
) {
606 } while (min
<= max
);
608 while (mid
> 0 && m_rowOffsets
.at(m_itemInfos
[mid
].row
) > bottom
) {
611 m_lastVisibleIndex
= mid
;
613 m_visibleIndexesDirty
= false;
616 bool KItemListViewLayouter::createGroupHeaders()
618 if (!m_model
->groupedSorting()) {
622 m_groupItemIndexes
.clear();
624 const QList
<QPair
<int, QVariant
>> groups
= m_model
->groups();
625 if (groups
.isEmpty()) {
629 for (int i
= 0; i
< groups
.count(); ++i
) {
630 const int firstItemIndex
= groups
.at(i
).first
;
631 m_groupItemIndexes
.insert(firstItemIndex
);
637 qreal
KItemListViewLayouter::minimumGroupHeaderWidth() const
642 #include "moc_kitemlistviewlayouter.cpp"