1 /***************************************************************************
2 * Copyright (C) 2011 by Peter Penz <peter.penz19@gmail.com> *
4 * This program is free software; you can redistribute it and/or modify *
5 * it under the terms of the GNU General Public License as published by *
6 * the Free Software Foundation; either version 2 of the License, or *
7 * (at your option) any later version. *
9 * This program is distributed in the hope that it will be useful, *
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of *
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
12 * GNU General Public License for more details. *
14 * You should have received a copy of the GNU General Public License *
15 * along with this program; if not, write to the *
16 * Free Software Foundation, Inc., *
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *
18 ***************************************************************************/
20 #include "kitemlistviewlayouter.h"
22 #include <kitemviews/kitemmodelbase.h>
23 #include "kitemlistsizehintresolver.h"
27 // #define KITEMLISTVIEWLAYOUTER_DEBUG
29 KItemListViewLayouter::KItemListViewLayouter(QObject
* parent
) :
32 m_visibleIndexesDirty(true),
33 m_scrollOrientation(Qt::Vertical
),
39 m_sizeHintResolver(0),
41 m_maximumScrollOffset(0),
43 m_maximumItemOffset(0),
44 m_firstVisibleIndex(-1),
45 m_lastVisibleIndex(-1),
52 m_groupHeaderHeight(0),
53 m_groupHeaderMargin(0),
58 KItemListViewLayouter::~KItemListViewLayouter()
62 void KItemListViewLayouter::setScrollOrientation(Qt::Orientation orientation
)
64 if (m_scrollOrientation
!= orientation
) {
65 m_scrollOrientation
= orientation
;
70 Qt::Orientation
KItemListViewLayouter::scrollOrientation() const
72 return m_scrollOrientation
;
75 void KItemListViewLayouter::setSize(const QSizeF
& size
)
78 if (m_scrollOrientation
== Qt::Vertical
) {
79 if (m_size
.width() != size
.width()) {
82 } else if (m_size
.height() != size
.height()) {
87 m_visibleIndexesDirty
= true;
91 QSizeF
KItemListViewLayouter::size() const
96 void KItemListViewLayouter::setItemSize(const QSizeF
& size
)
98 if (m_itemSize
!= size
) {
104 QSizeF
KItemListViewLayouter::itemSize() const
109 void KItemListViewLayouter::setItemMargin(const QSizeF
& margin
)
111 if (m_itemMargin
!= margin
) {
112 m_itemMargin
= margin
;
117 QSizeF
KItemListViewLayouter::itemMargin() const
122 void KItemListViewLayouter::setHeaderHeight(qreal height
)
124 if (m_headerHeight
!= height
) {
125 m_headerHeight
= height
;
130 qreal
KItemListViewLayouter::headerHeight() const
132 return m_headerHeight
;
135 void KItemListViewLayouter::setGroupHeaderHeight(qreal height
)
137 if (m_groupHeaderHeight
!= height
) {
138 m_groupHeaderHeight
= height
;
143 qreal
KItemListViewLayouter::groupHeaderHeight() const
145 return m_groupHeaderHeight
;
148 void KItemListViewLayouter::setGroupHeaderMargin(qreal margin
)
150 if (m_groupHeaderMargin
!= margin
) {
151 m_groupHeaderMargin
= margin
;
156 qreal
KItemListViewLayouter::groupHeaderMargin() const
158 return m_groupHeaderMargin
;
161 void KItemListViewLayouter::setScrollOffset(qreal offset
)
163 if (m_scrollOffset
!= offset
) {
164 m_scrollOffset
= offset
;
165 m_visibleIndexesDirty
= true;
169 qreal
KItemListViewLayouter::scrollOffset() const
171 return m_scrollOffset
;
174 qreal
KItemListViewLayouter::maximumScrollOffset() const
176 const_cast<KItemListViewLayouter
*>(this)->doLayout();
177 return m_maximumScrollOffset
;
180 void KItemListViewLayouter::setItemOffset(qreal offset
)
182 if (m_itemOffset
!= offset
) {
183 m_itemOffset
= offset
;
184 m_visibleIndexesDirty
= true;
188 qreal
KItemListViewLayouter::itemOffset() const
193 qreal
KItemListViewLayouter::maximumItemOffset() const
195 const_cast<KItemListViewLayouter
*>(this)->doLayout();
196 return m_maximumItemOffset
;
199 void KItemListViewLayouter::setModel(const KItemModelBase
* model
)
201 if (m_model
!= model
) {
207 const KItemModelBase
* KItemListViewLayouter::model() const
212 void KItemListViewLayouter::setSizeHintResolver(KItemListSizeHintResolver
* sizeHintResolver
)
214 if (m_sizeHintResolver
!= sizeHintResolver
) {
215 m_sizeHintResolver
= sizeHintResolver
;
220 const KItemListSizeHintResolver
* KItemListViewLayouter::sizeHintResolver() const
222 return m_sizeHintResolver
;
225 int KItemListViewLayouter::firstVisibleIndex() const
227 const_cast<KItemListViewLayouter
*>(this)->doLayout();
228 return m_firstVisibleIndex
;
231 int KItemListViewLayouter::lastVisibleIndex() const
233 const_cast<KItemListViewLayouter
*>(this)->doLayout();
234 return m_lastVisibleIndex
;
237 QRectF
KItemListViewLayouter::itemRect(int index
) const
239 const_cast<KItemListViewLayouter
*>(this)->doLayout();
240 if (index
< 0 || index
>= m_itemInfos
.count()) {
245 if (m_sizeHintResolver
) {
246 sizeHint
= m_sizeHintResolver
->sizeHint(index
);
248 sizeHint
= m_itemSize
;
251 const qreal x
= m_columnOffsets
.at(m_itemInfos
.at(index
).column
);
252 const qreal y
= m_rowOffsets
.at(m_itemInfos
.at(index
).row
);
254 if (m_scrollOrientation
== Qt::Horizontal
) {
255 // Rotate the logical direction which is always vertical by 90°
256 // to get the physical horizontal direction
258 pos
.rx() -= m_scrollOffset
;
259 return QRectF(pos
, sizeHint
);
262 if (sizeHint
.width() <= 0) {
263 // In Details View, a size hint with negative width is used internally.
264 sizeHint
.rwidth() = m_itemSize
.width();
267 const QPointF
pos(x
- m_itemOffset
, y
- m_scrollOffset
);
268 return QRectF(pos
, sizeHint
);
271 QRectF
KItemListViewLayouter::groupHeaderRect(int index
) const
273 const_cast<KItemListViewLayouter
*>(this)->doLayout();
275 const QRectF firstItemRect
= itemRect(index
);
276 QPointF pos
= firstItemRect
.topLeft();
282 if (m_scrollOrientation
== Qt::Vertical
) {
284 pos
.ry() -= m_groupHeaderHeight
;
285 size
= QSizeF(m_size
.width(), m_groupHeaderHeight
);
287 pos
.rx() -= m_itemMargin
.width();
290 // Determine the maximum width used in the current column. As the
291 // scroll-direction is Qt::Horizontal and m_itemRects is accessed
292 // directly, the logical height represents the visual width, and
293 // the logical row represents the column.
294 qreal headerWidth
= minimumGroupHeaderWidth();
295 const int row
= m_itemInfos
[index
].row
;
296 const int maxIndex
= m_itemInfos
.count() - 1;
297 while (index
<= maxIndex
) {
298 if (m_itemInfos
[index
].row
!= row
) {
303 if (m_sizeHintResolver
) {
304 itemWidth
= m_sizeHintResolver
->sizeHint(index
).width();
306 itemWidth
= m_itemSize
.width();
309 if (itemWidth
> headerWidth
) {
310 headerWidth
= itemWidth
;
316 size
= QSizeF(headerWidth
, m_size
.height());
318 return QRectF(pos
, size
);
321 int KItemListViewLayouter::itemColumn(int index
) const
323 const_cast<KItemListViewLayouter
*>(this)->doLayout();
324 if (index
< 0 || index
>= m_itemInfos
.count()) {
328 return (m_scrollOrientation
== Qt::Vertical
)
329 ? m_itemInfos
[index
].column
330 : m_itemInfos
[index
].row
;
333 int KItemListViewLayouter::itemRow(int index
) const
335 const_cast<KItemListViewLayouter
*>(this)->doLayout();
336 if (index
< 0 || index
>= m_itemInfos
.count()) {
340 return (m_scrollOrientation
== Qt::Vertical
)
341 ? m_itemInfos
[index
].row
342 : m_itemInfos
[index
].column
;
345 int KItemListViewLayouter::maximumVisibleItems() const
347 const_cast<KItemListViewLayouter
*>(this)->doLayout();
349 const int height
= static_cast<int>(m_size
.height());
350 const int rowHeight
= static_cast<int>(m_itemSize
.height());
351 int rows
= height
/ rowHeight
;
352 if (height
% rowHeight
!= 0) {
356 return rows
* m_columnCount
;
359 bool KItemListViewLayouter::isFirstGroupItem(int itemIndex
) const
361 const_cast<KItemListViewLayouter
*>(this)->doLayout();
362 return m_groupItemIndexes
.contains(itemIndex
);
365 void KItemListViewLayouter::markAsDirty()
372 bool KItemListViewLayouter::isDirty()
378 void KItemListViewLayouter::doLayout()
381 #ifdef KITEMLISTVIEWLAYOUTER_DEBUG
385 m_visibleIndexesDirty
= true;
387 QSizeF itemSize
= m_itemSize
;
388 QSizeF itemMargin
= m_itemMargin
;
389 QSizeF size
= m_size
;
391 const bool grouped
= createGroupHeaders();
393 const bool horizontalScrolling
= (m_scrollOrientation
== Qt::Horizontal
);
394 if (horizontalScrolling
) {
395 // Flip everything so that the layout logically can work like having
396 // a vertical scrolling
397 itemSize
.transpose();
398 itemMargin
.transpose();
402 // In the horizontal scrolling case all groups are aligned
403 // at the top, which decreases the available height. For the
404 // flipped data this means that the width must be decreased.
405 size
.rwidth() -= m_groupHeaderHeight
;
409 m_columnWidth
= itemSize
.width() + itemMargin
.width();
410 const qreal widthForColumns
= size
.width() - itemMargin
.width();
411 m_columnCount
= qMax(1, int(widthForColumns
/ m_columnWidth
));
412 m_xPosInc
= itemMargin
.width();
414 const int itemCount
= m_model
->count();
415 if (itemCount
> m_columnCount
&& m_columnWidth
>= 32) {
416 // Apply the unused width equally to each column
417 const qreal unusedWidth
= widthForColumns
- m_columnCount
* m_columnWidth
;
418 if (unusedWidth
> 0) {
419 const qreal columnInc
= unusedWidth
/ (m_columnCount
+ 1);
420 m_columnWidth
+= columnInc
;
421 m_xPosInc
+= columnInc
;
425 m_itemInfos
.resize(itemCount
);
427 // Calculate the offset of each column, i.e., the x-coordinate where the column starts.
428 m_columnOffsets
.resize(m_columnCount
);
429 qreal currentOffset
= m_xPosInc
;
431 if (grouped
&& horizontalScrolling
) {
432 // All group headers will always be aligned on the top and not
433 // flipped like the other properties.
434 currentOffset
+= m_groupHeaderHeight
;
437 for (int column
= 0; column
< m_columnCount
; ++column
) {
438 m_columnOffsets
[column
] = currentOffset
;
439 currentOffset
+= m_columnWidth
;
442 // Prepare the QVector which stores the y-coordinate for each new row.
443 int numberOfRows
= (itemCount
+ m_columnCount
- 1) / m_columnCount
;
444 if (grouped
&& m_columnCount
> 1) {
445 // In the worst case, a new row will be started for every group.
446 // We could calculate the exact number of rows now to prevent that we reserve
447 // too much memory, but the code required to do that might need much more
448 // memory than it would save in the average case.
449 numberOfRows
+= m_groupItemIndexes
.count();
451 m_rowOffsets
.resize(numberOfRows
);
453 qreal y
= m_headerHeight
+ itemMargin
.height();
457 while (index
< itemCount
) {
458 qreal maxItemHeight
= itemSize
.height();
461 if (m_groupItemIndexes
.contains(index
)) {
462 // The item is the first item of a group.
463 // Increase the y-position to provide space
464 // for the group header.
466 // Only add a margin if there has been added another
467 // group already before
468 y
+= m_groupHeaderMargin
;
469 } else if (!horizontalScrolling
) {
470 // The first group header should be aligned on top
471 y
-= itemMargin
.height();
474 if (!horizontalScrolling
) {
475 y
+= m_groupHeaderHeight
;
480 m_rowOffsets
[row
] = y
;
483 while (index
< itemCount
&& column
< m_columnCount
) {
484 qreal requiredItemHeight
= itemSize
.height();
485 if (m_sizeHintResolver
) {
486 const QSizeF sizeHint
= m_sizeHintResolver
->sizeHint(index
);
487 const qreal sizeHintHeight
= horizontalScrolling
? sizeHint
.width() : sizeHint
.height();
488 if (sizeHintHeight
> requiredItemHeight
) {
489 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 kDebug() << "[TIME] doLayout() for " << m_model
->count() << "items:" << timer
.elapsed();
545 updateVisibleIndexes();
548 void KItemListViewLayouter::updateVisibleIndexes()
550 if (!m_visibleIndexesDirty
) {
556 if (m_model
->count() <= 0) {
557 m_firstVisibleIndex
= -1;
558 m_lastVisibleIndex
= -1;
559 m_visibleIndexesDirty
= false;
563 const int maxIndex
= m_model
->count() - 1;
565 // Calculate the first visible index that is fully visible
570 mid
= (min
+ max
) / 2;
571 if (m_rowOffsets
.at(m_itemInfos
[mid
].row
) < m_scrollOffset
) {
576 } while (min
<= max
);
579 // Include the row before the first fully visible index, as it might
581 if (m_rowOffsets
.at(m_itemInfos
[mid
].row
) >= m_scrollOffset
) {
583 Q_ASSERT(m_rowOffsets
.at(m_itemInfos
[mid
].row
) < m_scrollOffset
);
586 const int firstVisibleRow
= m_itemInfos
[mid
].row
;
587 while (mid
> 0 && m_itemInfos
[mid
- 1].row
== firstVisibleRow
) {
591 m_firstVisibleIndex
= mid
;
593 // Calculate the last visible index that is (at least partly) visible
594 const int visibleHeight
= (m_scrollOrientation
== Qt::Horizontal
) ? m_size
.width() : m_size
.height();
595 qreal bottom
= m_scrollOffset
+ visibleHeight
;
596 if (m_model
->groupedSorting()) {
597 bottom
+= m_groupHeaderHeight
;
600 min
= m_firstVisibleIndex
;
603 mid
= (min
+ max
) / 2;
604 if (m_rowOffsets
.at(m_itemInfos
[mid
].row
) <= bottom
) {
609 } while (min
<= max
);
611 while (mid
> 0 && m_rowOffsets
.at(m_itemInfos
[mid
].row
) > bottom
) {
614 m_lastVisibleIndex
= mid
;
616 m_visibleIndexesDirty
= false;
619 bool KItemListViewLayouter::createGroupHeaders()
621 if (!m_model
->groupedSorting()) {
625 m_groupItemIndexes
.clear();
627 const QList
<QPair
<int, QVariant
> > groups
= m_model
->groups();
628 if (groups
.isEmpty()) {
632 for (int i
= 0; i
< groups
.count(); ++i
) {
633 const int firstItemIndex
= groups
.at(i
).first
;
634 m_groupItemIndexes
.insert(firstItemIndex
);
640 qreal
KItemListViewLayouter::minimumGroupHeaderWidth() const
645 #include "kitemlistviewlayouter.moc"