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),
50 m_groupHeaderHeight(0),
51 m_groupHeaderMargin(0),
56 KItemListViewLayouter::~KItemListViewLayouter()
60 void KItemListViewLayouter::setScrollOrientation(Qt::Orientation orientation
)
62 if (m_scrollOrientation
!= orientation
) {
63 m_scrollOrientation
= orientation
;
68 Qt::Orientation
KItemListViewLayouter::scrollOrientation() const
70 return m_scrollOrientation
;
73 void KItemListViewLayouter::setSize(const QSizeF
& size
)
76 if (m_scrollOrientation
== Qt::Vertical
) {
77 if (m_size
.width() != size
.width()) {
80 } else if (m_size
.height() != size
.height()) {
85 m_visibleIndexesDirty
= true;
89 QSizeF
KItemListViewLayouter::size() const
94 void KItemListViewLayouter::setItemSize(const QSizeF
& size
)
96 if (m_itemSize
!= size
) {
102 QSizeF
KItemListViewLayouter::itemSize() const
107 void KItemListViewLayouter::setItemMargin(const QSizeF
& margin
)
109 if (m_itemMargin
!= margin
) {
110 m_itemMargin
= margin
;
115 QSizeF
KItemListViewLayouter::itemMargin() const
120 void KItemListViewLayouter::setHeaderHeight(qreal height
)
122 if (m_headerHeight
!= height
) {
123 m_headerHeight
= height
;
128 qreal
KItemListViewLayouter::headerHeight() const
130 return m_headerHeight
;
133 void KItemListViewLayouter::setGroupHeaderHeight(qreal height
)
135 if (m_groupHeaderHeight
!= height
) {
136 m_groupHeaderHeight
= height
;
141 qreal
KItemListViewLayouter::groupHeaderHeight() const
143 return m_groupHeaderHeight
;
146 void KItemListViewLayouter::setGroupHeaderMargin(qreal margin
)
148 if (m_groupHeaderMargin
!= margin
) {
149 m_groupHeaderMargin
= margin
;
154 qreal
KItemListViewLayouter::groupHeaderMargin() const
156 return m_groupHeaderMargin
;
159 void KItemListViewLayouter::setScrollOffset(qreal offset
)
161 if (m_scrollOffset
!= offset
) {
162 m_scrollOffset
= offset
;
163 m_visibleIndexesDirty
= true;
167 qreal
KItemListViewLayouter::scrollOffset() const
169 return m_scrollOffset
;
172 qreal
KItemListViewLayouter::maximumScrollOffset() const
174 const_cast<KItemListViewLayouter
*>(this)->doLayout();
175 return m_maximumScrollOffset
;
178 void KItemListViewLayouter::setItemOffset(qreal offset
)
180 if (m_itemOffset
!= offset
) {
181 m_itemOffset
= offset
;
182 m_visibleIndexesDirty
= true;
186 qreal
KItemListViewLayouter::itemOffset() const
191 qreal
KItemListViewLayouter::maximumItemOffset() const
193 const_cast<KItemListViewLayouter
*>(this)->doLayout();
194 return m_maximumItemOffset
;
197 void KItemListViewLayouter::setModel(const KItemModelBase
* model
)
199 if (m_model
!= model
) {
205 const KItemModelBase
* KItemListViewLayouter::model() const
210 void KItemListViewLayouter::setSizeHintResolver(const KItemListSizeHintResolver
* sizeHintResolver
)
212 if (m_sizeHintResolver
!= sizeHintResolver
) {
213 m_sizeHintResolver
= sizeHintResolver
;
218 const KItemListSizeHintResolver
* KItemListViewLayouter::sizeHintResolver() const
220 return m_sizeHintResolver
;
223 int KItemListViewLayouter::firstVisibleIndex() const
225 const_cast<KItemListViewLayouter
*>(this)->doLayout();
226 return m_firstVisibleIndex
;
229 int KItemListViewLayouter::lastVisibleIndex() const
231 const_cast<KItemListViewLayouter
*>(this)->doLayout();
232 return m_lastVisibleIndex
;
235 QRectF
KItemListViewLayouter::itemRect(int index
) const
237 const_cast<KItemListViewLayouter
*>(this)->doLayout();
238 if (index
< 0 || index
>= m_itemInfos
.count()) {
243 if (m_sizeHintResolver
) {
244 sizeHint
= m_sizeHintResolver
->sizeHint(index
);
246 sizeHint
= m_itemSize
;
249 if (m_scrollOrientation
== Qt::Horizontal
) {
250 // Rotate the logical direction which is always vertical by 90°
251 // to get the physical horizontal direction
252 const QPointF logicalPos
= m_itemInfos
[index
].pos
;
253 QPointF
pos(logicalPos
.y(), logicalPos
.x());
254 pos
.rx() -= m_scrollOffset
;
255 return QRectF(pos
, sizeHint
);
258 if (sizeHint
.width() <= 0) {
259 // In Details View, a size hint with negative width is used internally.
260 sizeHint
.rwidth() = m_itemSize
.width();
263 QPointF pos
= m_itemInfos
[index
].pos
;
264 pos
-= QPointF(m_itemOffset
, m_scrollOffset
);
265 return QRectF(pos
, sizeHint
);
268 QRectF
KItemListViewLayouter::groupHeaderRect(int index
) const
270 const_cast<KItemListViewLayouter
*>(this)->doLayout();
272 const QRectF firstItemRect
= itemRect(index
);
273 QPointF pos
= firstItemRect
.topLeft();
279 if (m_scrollOrientation
== Qt::Vertical
) {
281 pos
.ry() -= m_groupHeaderHeight
;
282 size
= QSizeF(m_size
.width(), m_groupHeaderHeight
);
284 pos
.rx() -= m_itemMargin
.width();
287 // Determine the maximum width used in the
288 // current column. As the scroll-direction is
289 // Qt::Horizontal and m_itemRects is accessed directly,
290 // the logical height represents the visual width.
291 qreal headerWidth
= minimumGroupHeaderWidth();
292 const qreal y
= m_itemInfos
[index
].pos
.y();
293 const int maxIndex
= m_itemInfos
.count() - 1;
294 while (index
<= maxIndex
) {
295 const QPointF pos
= m_itemInfos
[index
].pos
;
301 if (m_sizeHintResolver
) {
302 itemWidth
= m_sizeHintResolver
->sizeHint(index
).width();
304 itemWidth
= m_itemSize
.width();
307 if (itemWidth
> headerWidth
) {
308 headerWidth
= itemWidth
;
314 size
= QSizeF(headerWidth
, m_size
.height());
316 return QRectF(pos
, size
);
319 int KItemListViewLayouter::itemColumn(int index
) const
321 const_cast<KItemListViewLayouter
*>(this)->doLayout();
322 if (index
< 0 || index
>= m_itemInfos
.count()) {
326 return (m_scrollOrientation
== Qt::Vertical
)
327 ? m_itemInfos
[index
].column
328 : m_itemInfos
[index
].row
;
331 int KItemListViewLayouter::itemRow(int index
) const
333 const_cast<KItemListViewLayouter
*>(this)->doLayout();
334 if (index
< 0 || index
>= m_itemInfos
.count()) {
338 return (m_scrollOrientation
== Qt::Vertical
)
339 ? m_itemInfos
[index
].row
340 : m_itemInfos
[index
].column
;
343 int KItemListViewLayouter::maximumVisibleItems() const
345 const_cast<KItemListViewLayouter
*>(this)->doLayout();
347 const int height
= static_cast<int>(m_size
.height());
348 const int rowHeight
= static_cast<int>(m_itemSize
.height());
349 int rows
= height
/ rowHeight
;
350 if (height
% rowHeight
!= 0) {
354 return rows
* m_columnCount
;
357 bool KItemListViewLayouter::isFirstGroupItem(int itemIndex
) const
359 const_cast<KItemListViewLayouter
*>(this)->doLayout();
360 return m_groupItemIndexes
.contains(itemIndex
);
363 void KItemListViewLayouter::markAsDirty()
370 bool KItemListViewLayouter::isDirty()
376 void KItemListViewLayouter::doLayout()
379 #ifdef KITEMLISTVIEWLAYOUTER_DEBUG
383 m_visibleIndexesDirty
= true;
385 QSizeF itemSize
= m_itemSize
;
386 QSizeF itemMargin
= m_itemMargin
;
387 QSizeF size
= m_size
;
389 const bool grouped
= createGroupHeaders();
391 const bool horizontalScrolling
= (m_scrollOrientation
== Qt::Horizontal
);
392 if (horizontalScrolling
) {
393 // Flip everything so that the layout logically can work like having
394 // a vertical scrolling
395 itemSize
.transpose();
396 itemMargin
.transpose();
400 // In the horizontal scrolling case all groups are aligned
401 // at the top, which decreases the available height. For the
402 // flipped data this means that the width must be decreased.
403 size
.rwidth() -= m_groupHeaderHeight
;
407 m_columnWidth
= itemSize
.width() + itemMargin
.width();
408 const qreal widthForColumns
= size
.width() - itemMargin
.width();
409 m_columnCount
= qMax(1, int(widthForColumns
/ m_columnWidth
));
410 m_xPosInc
= itemMargin
.width();
412 const int itemCount
= m_model
->count();
413 if (itemCount
> m_columnCount
&& m_columnWidth
>= 32) {
414 // Apply the unused width equally to each column
415 const qreal unusedWidth
= widthForColumns
- m_columnCount
* m_columnWidth
;
416 if (unusedWidth
> 0) {
417 const qreal columnInc
= unusedWidth
/ (m_columnCount
+ 1);
418 m_columnWidth
+= columnInc
;
419 m_xPosInc
+= columnInc
;
423 m_itemInfos
.resize(itemCount
);
425 qreal y
= m_headerHeight
+ itemMargin
.height();
429 while (index
< itemCount
) {
431 qreal maxItemHeight
= itemSize
.height();
434 if (horizontalScrolling
) {
435 // All group headers will always be aligned on the top and not
436 // flipped like the other properties
437 x
+= m_groupHeaderHeight
;
440 if (m_groupItemIndexes
.contains(index
)) {
441 // The item is the first item of a group.
442 // Increase the y-position to provide space
443 // for the group header.
445 // Only add a margin if there has been added another
446 // group already before
447 y
+= m_groupHeaderMargin
;
448 } else if (!horizontalScrolling
) {
449 // The first group header should be aligned on top
450 y
-= itemMargin
.height();
453 if (!horizontalScrolling
) {
454 y
+= m_groupHeaderHeight
;
460 while (index
< itemCount
&& column
< m_columnCount
) {
461 qreal requiredItemHeight
= itemSize
.height();
462 if (m_sizeHintResolver
) {
463 const QSizeF sizeHint
= m_sizeHintResolver
->sizeHint(index
);
464 const qreal sizeHintHeight
= horizontalScrolling
? sizeHint
.width() : sizeHint
.height();
465 if (sizeHintHeight
> requiredItemHeight
) {
466 requiredItemHeight
= sizeHintHeight
;
470 ItemInfo
& itemInfo
= m_itemInfos
[index
];
471 itemInfo
.pos
= QPointF(x
, y
);
472 itemInfo
.column
= column
;
475 if (grouped
&& horizontalScrolling
) {
476 // When grouping is enabled in the horizontal mode, the header alignment
478 // Header-1 Header-2 Header-3
479 // Item 1 Item 4 Item 7
480 // Item 2 Item 5 Item 8
481 // Item 3 Item 6 Item 9
482 // In this case 'requiredItemHeight' represents the column-width. We don't
483 // check the content of the header in the layouter to determine the required
484 // width, hence assure that at least a minimal width of 15 characters is given
485 // (in average a character requires the halve width of the font height).
487 // TODO: Let the group headers provide a minimum width and respect this width here
488 const qreal headerWidth
= minimumGroupHeaderWidth();
489 if (requiredItemHeight
< headerWidth
) {
490 requiredItemHeight
= headerWidth
;
494 maxItemHeight
= qMax(maxItemHeight
, requiredItemHeight
);
499 if (grouped
&& m_groupItemIndexes
.contains(index
)) {
500 // The item represents the first index of a group
501 // and must aligned in the first column
506 y
+= maxItemHeight
+ itemMargin
.height();
511 m_maximumScrollOffset
= y
;
512 m_maximumItemOffset
= m_columnCount
* m_columnWidth
;
514 m_maximumScrollOffset
= 0;
515 m_maximumItemOffset
= 0;
518 #ifdef KITEMLISTVIEWLAYOUTER_DEBUG
519 kDebug() << "[TIME] doLayout() for " << m_model
->count() << "items:" << timer
.elapsed();
524 updateVisibleIndexes();
527 void KItemListViewLayouter::updateVisibleIndexes()
529 if (!m_visibleIndexesDirty
) {
535 if (m_model
->count() <= 0) {
536 m_firstVisibleIndex
= -1;
537 m_lastVisibleIndex
= -1;
538 m_visibleIndexesDirty
= false;
542 const int maxIndex
= m_model
->count() - 1;
544 // Calculate the first visible index that is fully visible
549 mid
= (min
+ max
) / 2;
550 if (m_itemInfos
[mid
].pos
.y() < m_scrollOffset
) {
555 } while (min
<= max
);
558 // Include the row before the first fully visible index, as it might
560 if (m_itemInfos
[mid
].pos
.y() >= m_scrollOffset
) {
562 Q_ASSERT(m_itemInfos
[mid
].pos
.y() < m_scrollOffset
);
565 const qreal rowTop
= m_itemInfos
[mid
].pos
.y();
566 while (mid
> 0 && m_itemInfos
[mid
- 1].pos
.y() == rowTop
) {
570 m_firstVisibleIndex
= mid
;
572 // Calculate the last visible index that is (at least partly) visible
573 const int visibleHeight
= (m_scrollOrientation
== Qt::Horizontal
) ? m_size
.width() : m_size
.height();
574 qreal bottom
= m_scrollOffset
+ visibleHeight
;
575 if (m_model
->groupedSorting()) {
576 bottom
+= m_groupHeaderHeight
;
579 min
= m_firstVisibleIndex
;
582 mid
= (min
+ max
) / 2;
583 if (m_itemInfos
[mid
].pos
.y() <= bottom
) {
588 } while (min
<= max
);
590 while (mid
> 0 && m_itemInfos
[mid
].pos
.y() > bottom
) {
593 m_lastVisibleIndex
= mid
;
595 m_visibleIndexesDirty
= false;
598 bool KItemListViewLayouter::createGroupHeaders()
600 if (!m_model
->groupedSorting()) {
604 m_groupItemIndexes
.clear();
606 const QList
<QPair
<int, QVariant
> > groups
= m_model
->groups();
607 if (groups
.isEmpty()) {
611 for (int i
= 0; i
< groups
.count(); ++i
) {
612 const int firstItemIndex
= groups
.at(i
).first
;
613 m_groupItemIndexes
.insert(firstItemIndex
);
619 qreal
KItemListViewLayouter::minimumGroupHeaderWidth() const
624 #include "kitemlistviewlayouter.moc"