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 // #define KITEMLISTVIEWLAYOUTER_DEBUG
14 KItemListViewLayouter::KItemListViewLayouter(KItemListSizeHintResolver
* sizeHintResolver
, QObject
* parent
) :
17 m_visibleIndexesDirty(true),
18 m_scrollOrientation(Qt::Vertical
),
24 m_sizeHintResolver(sizeHintResolver
),
26 m_maximumScrollOffset(0),
28 m_maximumItemOffset(0),
29 m_firstVisibleIndex(-1),
30 m_lastVisibleIndex(-1),
37 m_groupHeaderHeight(0),
38 m_groupHeaderMargin(0),
41 Q_ASSERT(m_sizeHintResolver
);
44 KItemListViewLayouter::~KItemListViewLayouter()
48 void KItemListViewLayouter::setScrollOrientation(Qt::Orientation orientation
)
50 if (m_scrollOrientation
!= orientation
) {
51 m_scrollOrientation
= orientation
;
56 Qt::Orientation
KItemListViewLayouter::scrollOrientation() const
58 return m_scrollOrientation
;
61 void KItemListViewLayouter::setSize(const QSizeF
& size
)
64 if (m_scrollOrientation
== Qt::Vertical
) {
65 if (m_size
.width() != size
.width()) {
68 } else if (m_size
.height() != size
.height()) {
73 m_visibleIndexesDirty
= true;
77 QSizeF
KItemListViewLayouter::size() const
82 void KItemListViewLayouter::setItemSize(const QSizeF
& size
)
84 if (m_itemSize
!= size
) {
90 QSizeF
KItemListViewLayouter::itemSize() const
95 void KItemListViewLayouter::setItemMargin(const QSizeF
& margin
)
97 if (m_itemMargin
!= margin
) {
98 m_itemMargin
= margin
;
103 QSizeF
KItemListViewLayouter::itemMargin() const
108 void KItemListViewLayouter::setHeaderHeight(qreal height
)
110 if (m_headerHeight
!= height
) {
111 m_headerHeight
= height
;
116 qreal
KItemListViewLayouter::headerHeight() const
118 return m_headerHeight
;
121 void KItemListViewLayouter::setGroupHeaderHeight(qreal height
)
123 if (m_groupHeaderHeight
!= height
) {
124 m_groupHeaderHeight
= height
;
129 qreal
KItemListViewLayouter::groupHeaderHeight() const
131 return m_groupHeaderHeight
;
134 void KItemListViewLayouter::setGroupHeaderMargin(qreal margin
)
136 if (m_groupHeaderMargin
!= margin
) {
137 m_groupHeaderMargin
= margin
;
142 qreal
KItemListViewLayouter::groupHeaderMargin() const
144 return m_groupHeaderMargin
;
147 void KItemListViewLayouter::setScrollOffset(qreal offset
)
149 if (m_scrollOffset
!= offset
) {
150 m_scrollOffset
= offset
;
151 m_visibleIndexesDirty
= true;
155 qreal
KItemListViewLayouter::scrollOffset() const
157 return m_scrollOffset
;
160 qreal
KItemListViewLayouter::maximumScrollOffset() const
162 const_cast<KItemListViewLayouter
*>(this)->doLayout();
163 return m_maximumScrollOffset
;
166 void KItemListViewLayouter::setItemOffset(qreal offset
)
168 if (m_itemOffset
!= offset
) {
169 m_itemOffset
= offset
;
170 m_visibleIndexesDirty
= true;
174 qreal
KItemListViewLayouter::itemOffset() const
179 qreal
KItemListViewLayouter::maximumItemOffset() const
181 const_cast<KItemListViewLayouter
*>(this)->doLayout();
182 return m_maximumItemOffset
;
185 void KItemListViewLayouter::setModel(const KItemModelBase
* model
)
187 if (m_model
!= model
) {
193 const KItemModelBase
* KItemListViewLayouter::model() const
198 int KItemListViewLayouter::firstVisibleIndex() const
200 const_cast<KItemListViewLayouter
*>(this)->doLayout();
201 return m_firstVisibleIndex
;
204 int KItemListViewLayouter::lastVisibleIndex() const
206 const_cast<KItemListViewLayouter
*>(this)->doLayout();
207 return m_lastVisibleIndex
;
210 QRectF
KItemListViewLayouter::itemRect(int index
) const
212 const_cast<KItemListViewLayouter
*>(this)->doLayout();
213 if (index
< 0 || index
>= m_itemInfos
.count()) {
217 QSizeF sizeHint
= m_sizeHintResolver
->sizeHint(index
);
219 const qreal x
= m_columnOffsets
.at(m_itemInfos
.at(index
).column
);
220 const qreal y
= m_rowOffsets
.at(m_itemInfos
.at(index
).row
);
222 if (m_scrollOrientation
== Qt::Horizontal
) {
223 // Rotate the logical direction which is always vertical by 90°
224 // to get the physical horizontal direction
226 pos
.rx() -= m_scrollOffset
;
227 sizeHint
.transpose();
228 return QRectF(pos
, sizeHint
);
231 if (sizeHint
.width() <= 0) {
232 // In Details View, a size hint with negative width is used internally.
233 sizeHint
.rwidth() = m_itemSize
.width();
236 const QPointF
pos(x
- m_itemOffset
, y
- m_scrollOffset
);
237 return QRectF(pos
, sizeHint
);
240 QRectF
KItemListViewLayouter::groupHeaderRect(int index
) const
242 const_cast<KItemListViewLayouter
*>(this)->doLayout();
244 const QRectF firstItemRect
= itemRect(index
);
245 QPointF pos
= firstItemRect
.topLeft();
251 if (m_scrollOrientation
== Qt::Vertical
) {
253 pos
.ry() -= m_groupHeaderHeight
;
254 size
= QSizeF(m_size
.width(), m_groupHeaderHeight
);
256 pos
.rx() -= m_itemMargin
.width();
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
) {
271 const qreal itemWidth
= (m_scrollOrientation
== Qt::Vertical
)
272 ? m_sizeHintResolver
->sizeHint(index
).width()
273 : m_sizeHintResolver
->sizeHint(index
).height();
275 if (itemWidth
> headerWidth
) {
276 headerWidth
= itemWidth
;
282 size
= QSizeF(headerWidth
, m_size
.height());
284 return QRectF(pos
, size
);
287 int KItemListViewLayouter::itemColumn(int index
) const
289 const_cast<KItemListViewLayouter
*>(this)->doLayout();
290 if (index
< 0 || index
>= m_itemInfos
.count()) {
294 return (m_scrollOrientation
== Qt::Vertical
)
295 ? m_itemInfos
[index
].column
296 : m_itemInfos
[index
].row
;
299 int KItemListViewLayouter::itemRow(int index
) const
301 const_cast<KItemListViewLayouter
*>(this)->doLayout();
302 if (index
< 0 || index
>= m_itemInfos
.count()) {
306 return (m_scrollOrientation
== Qt::Vertical
)
307 ? m_itemInfos
[index
].row
308 : m_itemInfos
[index
].column
;
311 int KItemListViewLayouter::maximumVisibleItems() const
313 const_cast<KItemListViewLayouter
*>(this)->doLayout();
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) {
322 return rows
* m_columnCount
;
325 bool KItemListViewLayouter::isFirstGroupItem(int itemIndex
) const
327 const_cast<KItemListViewLayouter
*>(this)->doLayout();
328 return m_groupItemIndexes
.contains(itemIndex
);
331 void KItemListViewLayouter::markAsDirty()
338 bool KItemListViewLayouter::isDirty()
344 void KItemListViewLayouter::doLayout()
347 #ifdef KITEMLISTVIEWLAYOUTER_DEBUG
351 m_visibleIndexesDirty
= true;
353 QSizeF itemSize
= m_itemSize
;
354 QSizeF itemMargin
= m_itemMargin
;
355 QSizeF size
= m_size
;
357 const bool grouped
= createGroupHeaders();
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();
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
;
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();
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
;
391 m_itemInfos
.resize(itemCount
);
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
;
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
;
403 for (int column
= 0; column
< m_columnCount
; ++column
) {
404 m_columnOffsets
[column
] = currentOffset
;
405 currentOffset
+= m_columnWidth
;
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();
417 m_rowOffsets
.resize(numberOfRows
);
419 qreal y
= m_headerHeight
+ itemMargin
.height();
423 while (index
< itemCount
) {
424 qreal maxItemHeight
= itemSize
.height();
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.
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();
440 if (!horizontalScrolling
) {
441 y
+= m_groupHeaderHeight
;
446 m_rowOffsets
[row
] = y
;
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
;
457 ItemInfo
& itemInfo
= m_itemInfos
[index
];
458 itemInfo
.column
= column
;
461 if (grouped
&& horizontalScrolling
) {
462 // When grouping is enabled in the horizontal mode, the header alignment
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).
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
;
480 maxItemHeight
= qMax(maxItemHeight
, requiredItemHeight
);
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
491 y
+= maxItemHeight
+ itemMargin
.height();
496 m_maximumScrollOffset
= y
;
497 m_maximumItemOffset
= m_columnCount
* m_columnWidth
;
499 m_maximumScrollOffset
= 0;
500 m_maximumItemOffset
= 0;
503 #ifdef KITEMLISTVIEWLAYOUTER_DEBUG
504 qCDebug(DolphinDebug
) << "[TIME] doLayout() for " << m_model
->count() << "items:" << timer
.elapsed();
509 updateVisibleIndexes();
512 void KItemListViewLayouter::updateVisibleIndexes()
514 if (!m_visibleIndexesDirty
) {
520 if (m_model
->count() <= 0) {
521 m_firstVisibleIndex
= -1;
522 m_lastVisibleIndex
= -1;
523 m_visibleIndexesDirty
= false;
527 const int maxIndex
= m_model
->count() - 1;
529 // Calculate the first visible index that is fully visible
534 mid
= (min
+ max
) / 2;
535 if (m_rowOffsets
.at(m_itemInfos
[mid
].row
) < m_scrollOffset
) {
540 } while (min
<= max
);
543 // Include the row before the first fully visible index, as it might
545 if (m_rowOffsets
.at(m_itemInfos
[mid
].row
) >= m_scrollOffset
) {
547 Q_ASSERT(m_rowOffsets
.at(m_itemInfos
[mid
].row
) < m_scrollOffset
);
550 const int firstVisibleRow
= m_itemInfos
[mid
].row
;
551 while (mid
> 0 && m_itemInfos
[mid
- 1].row
== firstVisibleRow
) {
555 m_firstVisibleIndex
= mid
;
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
;
564 min
= m_firstVisibleIndex
;
567 mid
= (min
+ max
) / 2;
568 if (m_rowOffsets
.at(m_itemInfos
[mid
].row
) <= bottom
) {
573 } while (min
<= max
);
575 while (mid
> 0 && m_rowOffsets
.at(m_itemInfos
[mid
].row
) > bottom
) {
578 m_lastVisibleIndex
= mid
;
580 m_visibleIndexesDirty
= false;
583 bool KItemListViewLayouter::createGroupHeaders()
585 if (!m_model
->groupedSorting()) {
589 m_groupItemIndexes
.clear();
591 const QList
<QPair
<int, QVariant
> > groups
= m_model
->groups();
592 if (groups
.isEmpty()) {
596 for (int i
= 0; i
< groups
.count(); ++i
) {
597 const int firstItemIndex
= groups
.at(i
).first
;
598 m_groupItemIndexes
.insert(firstItemIndex
);
604 qreal
KItemListViewLayouter::minimumGroupHeaderWidth() const