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_p.h"
22 #include "kitemmodelbase.h"
23 #include "kitemlistsizehintresolver_p.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
)
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 void KItemListViewLayouter::setSizeHintResolver(const KItemListSizeHintResolver
* sizeHintResolver
)
204 if (m_sizeHintResolver
!= sizeHintResolver
) {
205 m_sizeHintResolver
= sizeHintResolver
;
210 const KItemListSizeHintResolver
* KItemListViewLayouter::sizeHintResolver() const
212 return m_sizeHintResolver
;
215 int KItemListViewLayouter::firstVisibleIndex() const
217 const_cast<KItemListViewLayouter
*>(this)->doLayout();
218 return m_firstVisibleIndex
;
221 int KItemListViewLayouter::lastVisibleIndex() const
223 const_cast<KItemListViewLayouter
*>(this)->doLayout();
224 return m_lastVisibleIndex
;
227 QRectF
KItemListViewLayouter::itemRect(int index
) const
229 const_cast<KItemListViewLayouter
*>(this)->doLayout();
230 if (index
< 0 || index
>= m_itemInfos
.count()) {
234 if (m_scrollOrientation
== Qt::Horizontal
) {
235 // Rotate the logical direction which is always vertical by 90°
236 // to get the physical horizontal direction
237 const QRectF
& b
= m_itemInfos
[index
].rect
;
238 QRectF
bounds(b
.y(), b
.x(), b
.height(), b
.width());
239 QPointF pos
= bounds
.topLeft();
240 pos
.rx() -= m_scrollOffset
;
245 QRectF bounds
= m_itemInfos
[index
].rect
;
246 bounds
.moveTo(bounds
.topLeft() - QPointF(m_itemOffset
, m_scrollOffset
));
250 QRectF
KItemListViewLayouter::groupHeaderRect(int index
) const
252 const_cast<KItemListViewLayouter
*>(this)->doLayout();
254 const QRectF firstItemRect
= itemRect(index
);
255 QPointF pos
= firstItemRect
.topLeft();
261 if (m_scrollOrientation
== Qt::Vertical
) {
263 pos
.ry() -= m_groupHeaderHeight
;
264 size
= QSizeF(m_size
.width(), m_groupHeaderHeight
);
266 pos
.rx() -= m_itemMargin
.width();
269 // Determine the maximum width used in the
270 // current column. As the scroll-direction is
271 // Qt::Horizontal and m_itemRects is accessed directly,
272 // the logical height represents the visual width.
273 qreal width
= minimumGroupHeaderWidth();
274 const qreal y
= m_itemInfos
[index
].rect
.y();
275 const int maxIndex
= m_itemInfos
.count() - 1;
276 while (index
<= maxIndex
) {
277 QRectF bounds
= m_itemInfos
[index
].rect
;
278 if (bounds
.y() != y
) {
282 if (bounds
.height() > width
) {
283 width
= bounds
.height();
289 size
= QSizeF(width
, m_size
.height());
291 return QRectF(pos
, size
);
294 int KItemListViewLayouter::itemColumn(int index
) const
296 const_cast<KItemListViewLayouter
*>(this)->doLayout();
297 if (index
< 0 || index
>= m_itemInfos
.count()) {
301 return (m_scrollOrientation
== Qt::Vertical
)
302 ? m_itemInfos
[index
].column
303 : m_itemInfos
[index
].row
;
306 int KItemListViewLayouter::itemRow(int index
) const
308 const_cast<KItemListViewLayouter
*>(this)->doLayout();
309 if (index
< 0 || index
>= m_itemInfos
.count()) {
313 return (m_scrollOrientation
== Qt::Vertical
)
314 ? m_itemInfos
[index
].row
315 : m_itemInfos
[index
].column
;
318 int KItemListViewLayouter::maximumVisibleItems() const
320 const_cast<KItemListViewLayouter
*>(this)->doLayout();
322 const int height
= static_cast<int>(m_size
.height());
323 const int rowHeight
= static_cast<int>(m_itemSize
.height());
324 int rows
= height
/ rowHeight
;
325 if (height
% rowHeight
!= 0) {
329 return rows
* m_columnCount
;
332 bool KItemListViewLayouter::isFirstGroupItem(int itemIndex
) const
334 const_cast<KItemListViewLayouter
*>(this)->doLayout();
335 return m_groupItemIndexes
.contains(itemIndex
);
338 void KItemListViewLayouter::markAsDirty()
345 bool KItemListViewLayouter::isDirty()
351 void KItemListViewLayouter::doLayout()
354 #ifdef KITEMLISTVIEWLAYOUTER_DEBUG
358 m_visibleIndexesDirty
= true;
360 QSizeF itemSize
= m_itemSize
;
361 QSizeF itemMargin
= m_itemMargin
;
362 QSizeF size
= m_size
;
364 const bool grouped
= createGroupHeaders();
366 const bool horizontalScrolling
= (m_scrollOrientation
== Qt::Horizontal
);
367 if (horizontalScrolling
) {
368 // Flip everything so that the layout logically can work like having
369 // a vertical scrolling
370 itemSize
.setWidth(m_itemSize
.height());
371 itemSize
.setHeight(m_itemSize
.width());
372 itemMargin
.setWidth(m_itemMargin
.height());
373 itemMargin
.setHeight(m_itemMargin
.width());
374 size
.setWidth(m_size
.height());
375 size
.setHeight(m_size
.width());
378 // In the horizontal scrolling case all groups are aligned
379 // at the top, which decreases the available height. For the
380 // flipped data this means that the width must be decreased.
381 size
.rwidth() -= m_groupHeaderHeight
;
385 m_columnWidth
= itemSize
.width() + itemMargin
.width();
386 const qreal widthForColumns
= size
.width() - itemMargin
.width();
387 m_columnCount
= qMax(1, int(widthForColumns
/ m_columnWidth
));
388 m_xPosInc
= itemMargin
.width();
390 const int itemCount
= m_model
->count();
391 if (itemCount
> m_columnCount
&& m_columnWidth
>= 32) {
392 // Apply the unused width equally to each column
393 const qreal unusedWidth
= widthForColumns
- m_columnCount
* m_columnWidth
;
394 if (unusedWidth
> 0) {
395 const qreal columnInc
= unusedWidth
/ (m_columnCount
+ 1);
396 m_columnWidth
+= columnInc
;
397 m_xPosInc
+= columnInc
;
401 int rowCount
= itemCount
/ m_columnCount
;
402 if (itemCount
% m_columnCount
!= 0) {
406 m_itemInfos
.reserve(itemCount
);
408 qreal y
= m_headerHeight
+ itemMargin
.height();
412 while (index
< itemCount
) {
414 qreal maxItemHeight
= itemSize
.height();
417 if (horizontalScrolling
) {
418 // All group headers will always be aligned on the top and not
419 // flipped like the other properties
420 x
+= m_groupHeaderHeight
;
423 if (m_groupItemIndexes
.contains(index
)) {
424 // The item is the first item of a group.
425 // Increase the y-position to provide space
426 // for the group header.
428 // Only add a margin if there has been added another
429 // group already before
430 y
+= m_groupHeaderMargin
;
431 } else if (!horizontalScrolling
) {
432 // The first group header should be aligned on top
433 y
-= itemMargin
.height();
436 if (!horizontalScrolling
) {
437 y
+= m_groupHeaderHeight
;
443 while (index
< itemCount
&& column
< m_columnCount
) {
444 qreal requiredItemHeight
= itemSize
.height();
445 if (m_sizeHintResolver
) {
446 const QSizeF sizeHint
= m_sizeHintResolver
->sizeHint(index
);
447 const qreal sizeHintHeight
= horizontalScrolling
? sizeHint
.width() : sizeHint
.height();
448 if (sizeHintHeight
> requiredItemHeight
) {
449 requiredItemHeight
= sizeHintHeight
;
453 const QRectF
bounds(x
, y
, itemSize
.width(), requiredItemHeight
);
454 if (index
< m_itemInfos
.count()) {
455 m_itemInfos
[index
].rect
= bounds
;
456 m_itemInfos
[index
].column
= column
;
457 m_itemInfos
[index
].row
= row
;
460 itemInfo
.rect
= bounds
;
461 itemInfo
.column
= column
;
463 m_itemInfos
.append(itemInfo
);
466 if (grouped
&& horizontalScrolling
) {
467 // When grouping is enabled in the horizontal mode, the header alignment
469 // Header-1 Header-2 Header-3
470 // Item 1 Item 4 Item 7
471 // Item 2 Item 5 Item 8
472 // Item 3 Item 6 Item 9
473 // In this case 'requiredItemHeight' represents the column-width. We don't
474 // check the content of the header in the layouter to determine the required
475 // width, hence assure that at least a minimal width of 15 characters is given
476 // (in average a character requires the halve width of the font height).
478 // TODO: Let the group headers provide a minimum width and respect this width here
479 const qreal headerWidth
= minimumGroupHeaderWidth();
480 if (requiredItemHeight
< headerWidth
) {
481 requiredItemHeight
= headerWidth
;
485 maxItemHeight
= qMax(maxItemHeight
, requiredItemHeight
);
490 if (grouped
&& m_groupItemIndexes
.contains(index
)) {
491 // The item represents the first index of a group
492 // and must aligned in the first column
497 y
+= maxItemHeight
+ itemMargin
.height();
500 if (m_itemInfos
.count() > itemCount
) {
501 m_itemInfos
.erase(m_itemInfos
.begin() + itemCount
,
506 // Calculate the maximum y-range of the last row for m_maximumScrollOffset
507 m_maximumScrollOffset
= m_itemInfos
.last().rect
.bottom();
508 const qreal rowY
= m_itemInfos
.last().rect
.y();
510 int index
= m_itemInfos
.count() - 2;
511 while (index
>= 0 && m_itemInfos
[index
].rect
.bottom() >= rowY
) {
512 m_maximumScrollOffset
= qMax(m_maximumScrollOffset
, m_itemInfos
[index
].rect
.bottom());
516 m_maximumScrollOffset
+= itemMargin
.height();
518 m_maximumItemOffset
= m_columnCount
* m_columnWidth
;
520 m_maximumScrollOffset
= 0;
521 m_maximumItemOffset
= 0;
524 #ifdef KITEMLISTVIEWLAYOUTER_DEBUG
525 kDebug() << "[TIME] doLayout() for " << m_model
->count() << "items:" << timer
.elapsed();
530 updateVisibleIndexes();
533 void KItemListViewLayouter::updateVisibleIndexes()
535 if (!m_visibleIndexesDirty
) {
541 if (m_model
->count() <= 0) {
542 m_firstVisibleIndex
= -1;
543 m_lastVisibleIndex
= -1;
544 m_visibleIndexesDirty
= false;
548 const int maxIndex
= m_model
->count() - 1;
550 // Calculate the first visible index that is fully visible
555 mid
= (min
+ max
) / 2;
556 if (m_itemInfos
[mid
].rect
.top() < m_scrollOffset
) {
561 } while (min
<= max
);
564 // Include the row before the first fully visible index, as it might
566 if (m_itemInfos
[mid
].rect
.top() >= m_scrollOffset
) {
568 Q_ASSERT(m_itemInfos
[mid
].rect
.top() < m_scrollOffset
);
571 const qreal rowTop
= m_itemInfos
[mid
].rect
.top();
572 while (mid
> 0 && m_itemInfos
[mid
- 1].rect
.top() == rowTop
) {
576 m_firstVisibleIndex
= mid
;
578 // Calculate the last visible index that is (at least partly) visible
579 const int visibleHeight
= (m_scrollOrientation
== Qt::Horizontal
) ? m_size
.width() : m_size
.height();
580 qreal bottom
= m_scrollOffset
+ visibleHeight
;
581 if (m_model
->groupedSorting()) {
582 bottom
+= m_groupHeaderHeight
;
585 min
= m_firstVisibleIndex
;
588 mid
= (min
+ max
) / 2;
589 if (m_itemInfos
[mid
].rect
.y() <= bottom
) {
594 } while (min
<= max
);
596 while (mid
> 0 && m_itemInfos
[mid
].rect
.y() > bottom
) {
599 m_lastVisibleIndex
= mid
;
601 m_visibleIndexesDirty
= false;
604 bool KItemListViewLayouter::createGroupHeaders()
606 if (!m_model
->groupedSorting()) {
610 m_groupItemIndexes
.clear();
612 const QList
<QPair
<int, QVariant
> > groups
= m_model
->groups();
613 if (groups
.isEmpty()) {
617 for (int i
= 0; i
< groups
.count(); ++i
) {
618 const int firstItemIndex
= groups
.at(i
).first
;
619 m_groupItemIndexes
.insert(firstItemIndex
);
625 qreal
KItemListViewLayouter::minimumGroupHeaderWidth() const
630 #include "kitemlistviewlayouter_p.moc"