]> cloud.milkyroute.net Git - dolphin.git/blob - src/kitemviews/private/kitemlistviewlayouter.cpp
Calculate all item size hints at once.
[dolphin.git] / src / kitemviews / private / kitemlistviewlayouter.cpp
1 /***************************************************************************
2 * Copyright (C) 2011 by Peter Penz <peter.penz19@gmail.com> *
3 * *
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. *
8 * *
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. *
13 * *
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 ***************************************************************************/
19
20 #include "kitemlistviewlayouter.h"
21
22 #include <kitemviews/kitemmodelbase.h>
23 #include "kitemlistsizehintresolver.h"
24
25 #include <KDebug>
26
27 // #define KITEMLISTVIEWLAYOUTER_DEBUG
28
29 KItemListViewLayouter::KItemListViewLayouter(QObject* parent) :
30 QObject(parent),
31 m_dirty(true),
32 m_visibleIndexesDirty(true),
33 m_scrollOrientation(Qt::Vertical),
34 m_size(),
35 m_itemSize(128, 128),
36 m_itemMargin(),
37 m_headerHeight(0),
38 m_model(0),
39 m_sizeHintResolver(0),
40 m_scrollOffset(0),
41 m_maximumScrollOffset(0),
42 m_itemOffset(0),
43 m_maximumItemOffset(0),
44 m_firstVisibleIndex(-1),
45 m_lastVisibleIndex(-1),
46 m_columnWidth(0),
47 m_xPosInc(0),
48 m_columnCount(0),
49 m_rowOffsets(),
50 m_columnOffsets(),
51 m_groupItemIndexes(),
52 m_groupHeaderHeight(0),
53 m_groupHeaderMargin(0),
54 m_itemInfos()
55 {
56 }
57
58 KItemListViewLayouter::~KItemListViewLayouter()
59 {
60 }
61
62 void KItemListViewLayouter::setScrollOrientation(Qt::Orientation orientation)
63 {
64 if (m_scrollOrientation != orientation) {
65 m_scrollOrientation = orientation;
66 m_dirty = true;
67 }
68 }
69
70 Qt::Orientation KItemListViewLayouter::scrollOrientation() const
71 {
72 return m_scrollOrientation;
73 }
74
75 void KItemListViewLayouter::setSize(const QSizeF& size)
76 {
77 if (m_size != size) {
78 if (m_scrollOrientation == Qt::Vertical) {
79 if (m_size.width() != size.width()) {
80 m_dirty = true;
81 }
82 } else if (m_size.height() != size.height()) {
83 m_dirty = true;
84 }
85
86 m_size = size;
87 m_visibleIndexesDirty = true;
88 }
89 }
90
91 QSizeF KItemListViewLayouter::size() const
92 {
93 return m_size;
94 }
95
96 void KItemListViewLayouter::setItemSize(const QSizeF& size)
97 {
98 if (m_itemSize != size) {
99 m_itemSize = size;
100 m_dirty = true;
101 }
102 }
103
104 QSizeF KItemListViewLayouter::itemSize() const
105 {
106 return m_itemSize;
107 }
108
109 void KItemListViewLayouter::setItemMargin(const QSizeF& margin)
110 {
111 if (m_itemMargin != margin) {
112 m_itemMargin = margin;
113 m_dirty = true;
114 }
115 }
116
117 QSizeF KItemListViewLayouter::itemMargin() const
118 {
119 return m_itemMargin;
120 }
121
122 void KItemListViewLayouter::setHeaderHeight(qreal height)
123 {
124 if (m_headerHeight != height) {
125 m_headerHeight = height;
126 m_dirty = true;
127 }
128 }
129
130 qreal KItemListViewLayouter::headerHeight() const
131 {
132 return m_headerHeight;
133 }
134
135 void KItemListViewLayouter::setGroupHeaderHeight(qreal height)
136 {
137 if (m_groupHeaderHeight != height) {
138 m_groupHeaderHeight = height;
139 m_dirty = true;
140 }
141 }
142
143 qreal KItemListViewLayouter::groupHeaderHeight() const
144 {
145 return m_groupHeaderHeight;
146 }
147
148 void KItemListViewLayouter::setGroupHeaderMargin(qreal margin)
149 {
150 if (m_groupHeaderMargin != margin) {
151 m_groupHeaderMargin = margin;
152 m_dirty = true;
153 }
154 }
155
156 qreal KItemListViewLayouter::groupHeaderMargin() const
157 {
158 return m_groupHeaderMargin;
159 }
160
161 void KItemListViewLayouter::setScrollOffset(qreal offset)
162 {
163 if (m_scrollOffset != offset) {
164 m_scrollOffset = offset;
165 m_visibleIndexesDirty = true;
166 }
167 }
168
169 qreal KItemListViewLayouter::scrollOffset() const
170 {
171 return m_scrollOffset;
172 }
173
174 qreal KItemListViewLayouter::maximumScrollOffset() const
175 {
176 const_cast<KItemListViewLayouter*>(this)->doLayout();
177 return m_maximumScrollOffset;
178 }
179
180 void KItemListViewLayouter::setItemOffset(qreal offset)
181 {
182 if (m_itemOffset != offset) {
183 m_itemOffset = offset;
184 m_visibleIndexesDirty = true;
185 }
186 }
187
188 qreal KItemListViewLayouter::itemOffset() const
189 {
190 return m_itemOffset;
191 }
192
193 qreal KItemListViewLayouter::maximumItemOffset() const
194 {
195 const_cast<KItemListViewLayouter*>(this)->doLayout();
196 return m_maximumItemOffset;
197 }
198
199 void KItemListViewLayouter::setModel(const KItemModelBase* model)
200 {
201 if (m_model != model) {
202 m_model = model;
203 m_dirty = true;
204 }
205 }
206
207 const KItemModelBase* KItemListViewLayouter::model() const
208 {
209 return m_model;
210 }
211
212 void KItemListViewLayouter::setSizeHintResolver(KItemListSizeHintResolver* sizeHintResolver)
213 {
214 if (m_sizeHintResolver != sizeHintResolver) {
215 m_sizeHintResolver = sizeHintResolver;
216 m_dirty = true;
217 }
218 }
219
220 const KItemListSizeHintResolver* KItemListViewLayouter::sizeHintResolver() const
221 {
222 return m_sizeHintResolver;
223 }
224
225 int KItemListViewLayouter::firstVisibleIndex() const
226 {
227 const_cast<KItemListViewLayouter*>(this)->doLayout();
228 return m_firstVisibleIndex;
229 }
230
231 int KItemListViewLayouter::lastVisibleIndex() const
232 {
233 const_cast<KItemListViewLayouter*>(this)->doLayout();
234 return m_lastVisibleIndex;
235 }
236
237 QRectF KItemListViewLayouter::itemRect(int index) const
238 {
239 const_cast<KItemListViewLayouter*>(this)->doLayout();
240 if (index < 0 || index >= m_itemInfos.count()) {
241 return QRectF();
242 }
243
244 QSizeF sizeHint;
245 if (m_sizeHintResolver) {
246 sizeHint = m_sizeHintResolver->sizeHint(index);
247 } else {
248 sizeHint = m_itemSize;
249 }
250
251 const qreal x = m_columnOffsets.at(m_itemInfos.at(index).column);
252 const qreal y = m_rowOffsets.at(m_itemInfos.at(index).row);
253
254 if (m_scrollOrientation == Qt::Horizontal) {
255 // Rotate the logical direction which is always vertical by 90°
256 // to get the physical horizontal direction
257 QPointF pos(y, x);
258 pos.rx() -= m_scrollOffset;
259 return QRectF(pos, sizeHint);
260 }
261
262 if (sizeHint.width() <= 0) {
263 // In Details View, a size hint with negative width is used internally.
264 sizeHint.rwidth() = m_itemSize.width();
265 }
266
267 const QPointF pos(x - m_itemOffset, y - m_scrollOffset);
268 return QRectF(pos, sizeHint);
269 }
270
271 QRectF KItemListViewLayouter::groupHeaderRect(int index) const
272 {
273 const_cast<KItemListViewLayouter*>(this)->doLayout();
274
275 const QRectF firstItemRect = itemRect(index);
276 QPointF pos = firstItemRect.topLeft();
277 if (pos.isNull()) {
278 return QRectF();
279 }
280
281 QSizeF size;
282 if (m_scrollOrientation == Qt::Vertical) {
283 pos.rx() = 0;
284 pos.ry() -= m_groupHeaderHeight;
285 size = QSizeF(m_size.width(), m_groupHeaderHeight);
286 } else {
287 pos.rx() -= m_itemMargin.width();
288 pos.ry() = 0;
289
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) {
299 break;
300 }
301
302 qreal itemWidth;
303 if (m_sizeHintResolver) {
304 itemWidth = m_sizeHintResolver->sizeHint(index).width();
305 } else {
306 itemWidth = m_itemSize.width();
307 }
308
309 if (itemWidth > headerWidth) {
310 headerWidth = itemWidth;
311 }
312
313 ++index;
314 }
315
316 size = QSizeF(headerWidth, m_size.height());
317 }
318 return QRectF(pos, size);
319 }
320
321 int KItemListViewLayouter::itemColumn(int index) const
322 {
323 const_cast<KItemListViewLayouter*>(this)->doLayout();
324 if (index < 0 || index >= m_itemInfos.count()) {
325 return -1;
326 }
327
328 return (m_scrollOrientation == Qt::Vertical)
329 ? m_itemInfos[index].column
330 : m_itemInfos[index].row;
331 }
332
333 int KItemListViewLayouter::itemRow(int index) const
334 {
335 const_cast<KItemListViewLayouter*>(this)->doLayout();
336 if (index < 0 || index >= m_itemInfos.count()) {
337 return -1;
338 }
339
340 return (m_scrollOrientation == Qt::Vertical)
341 ? m_itemInfos[index].row
342 : m_itemInfos[index].column;
343 }
344
345 int KItemListViewLayouter::maximumVisibleItems() const
346 {
347 const_cast<KItemListViewLayouter*>(this)->doLayout();
348
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) {
353 ++rows;
354 }
355
356 return rows * m_columnCount;
357 }
358
359 bool KItemListViewLayouter::isFirstGroupItem(int itemIndex) const
360 {
361 const_cast<KItemListViewLayouter*>(this)->doLayout();
362 return m_groupItemIndexes.contains(itemIndex);
363 }
364
365 void KItemListViewLayouter::markAsDirty()
366 {
367 m_dirty = true;
368 }
369
370
371 #ifndef QT_NO_DEBUG
372 bool KItemListViewLayouter::isDirty()
373 {
374 return m_dirty;
375 }
376 #endif
377
378 void KItemListViewLayouter::doLayout()
379 {
380 if (m_dirty) {
381 #ifdef KITEMLISTVIEWLAYOUTER_DEBUG
382 QElapsedTimer timer;
383 timer.start();
384 #endif
385 m_visibleIndexesDirty = true;
386
387 QSizeF itemSize = m_itemSize;
388 QSizeF itemMargin = m_itemMargin;
389 QSizeF size = m_size;
390
391 const bool grouped = createGroupHeaders();
392
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();
399 size.transpose();
400
401 if (grouped) {
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;
406 }
407 }
408
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();
413
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;
422 }
423 }
424
425 m_itemInfos.resize(itemCount);
426
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;
430
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;
435 }
436
437 for (int column = 0; column < m_columnCount; ++column) {
438 m_columnOffsets[column] = currentOffset;
439 currentOffset += m_columnWidth;
440 }
441
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();
450 }
451 m_rowOffsets.resize(numberOfRows);
452
453 qreal y = m_headerHeight + itemMargin.height();
454 int row = 0;
455
456 int index = 0;
457 while (index < itemCount) {
458 qreal maxItemHeight = itemSize.height();
459
460 if (grouped) {
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.
465 if (index > 0) {
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();
472 }
473
474 if (!horizontalScrolling) {
475 y += m_groupHeaderHeight;
476 }
477 }
478 }
479
480 m_rowOffsets[row] = y;
481
482 int column = 0;
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;
490 }
491 }
492
493 ItemInfo& itemInfo = m_itemInfos[index];
494 itemInfo.column = column;
495 itemInfo.row = row;
496
497 if (grouped && horizontalScrolling) {
498 // When grouping is enabled in the horizontal mode, the header alignment
499 // looks like this:
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).
508 //
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;
513 }
514 }
515
516 maxItemHeight = qMax(maxItemHeight, requiredItemHeight);
517 ++index;
518 ++column;
519
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
523 break;
524 }
525 }
526
527 y += maxItemHeight + itemMargin.height();
528 ++row;
529 }
530
531 if (itemCount > 0) {
532 m_maximumScrollOffset = y;
533 m_maximumItemOffset = m_columnCount * m_columnWidth;
534 } else {
535 m_maximumScrollOffset = 0;
536 m_maximumItemOffset = 0;
537 }
538
539 #ifdef KITEMLISTVIEWLAYOUTER_DEBUG
540 kDebug() << "[TIME] doLayout() for " << m_model->count() << "items:" << timer.elapsed();
541 #endif
542 m_dirty = false;
543 }
544
545 updateVisibleIndexes();
546 }
547
548 void KItemListViewLayouter::updateVisibleIndexes()
549 {
550 if (!m_visibleIndexesDirty) {
551 return;
552 }
553
554 Q_ASSERT(!m_dirty);
555
556 if (m_model->count() <= 0) {
557 m_firstVisibleIndex = -1;
558 m_lastVisibleIndex = -1;
559 m_visibleIndexesDirty = false;
560 return;
561 }
562
563 const int maxIndex = m_model->count() - 1;
564
565 // Calculate the first visible index that is fully visible
566 int min = 0;
567 int max = maxIndex;
568 int mid = 0;
569 do {
570 mid = (min + max) / 2;
571 if (m_rowOffsets.at(m_itemInfos[mid].row) < m_scrollOffset) {
572 min = mid + 1;
573 } else {
574 max = mid - 1;
575 }
576 } while (min <= max);
577
578 if (mid > 0) {
579 // Include the row before the first fully visible index, as it might
580 // be partly visible
581 if (m_rowOffsets.at(m_itemInfos[mid].row) >= m_scrollOffset) {
582 --mid;
583 Q_ASSERT(m_rowOffsets.at(m_itemInfos[mid].row) < m_scrollOffset);
584 }
585
586 const int firstVisibleRow = m_itemInfos[mid].row;
587 while (mid > 0 && m_itemInfos[mid - 1].row == firstVisibleRow) {
588 --mid;
589 }
590 }
591 m_firstVisibleIndex = mid;
592
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;
598 }
599
600 min = m_firstVisibleIndex;
601 max = maxIndex;
602 do {
603 mid = (min + max) / 2;
604 if (m_rowOffsets.at(m_itemInfos[mid].row) <= bottom) {
605 min = mid + 1;
606 } else {
607 max = mid - 1;
608 }
609 } while (min <= max);
610
611 while (mid > 0 && m_rowOffsets.at(m_itemInfos[mid].row) > bottom) {
612 --mid;
613 }
614 m_lastVisibleIndex = mid;
615
616 m_visibleIndexesDirty = false;
617 }
618
619 bool KItemListViewLayouter::createGroupHeaders()
620 {
621 if (!m_model->groupedSorting()) {
622 return false;
623 }
624
625 m_groupItemIndexes.clear();
626
627 const QList<QPair<int, QVariant> > groups = m_model->groups();
628 if (groups.isEmpty()) {
629 return false;
630 }
631
632 for (int i = 0; i < groups.count(); ++i) {
633 const int firstItemIndex = groups.at(i).first;
634 m_groupItemIndexes.insert(firstItemIndex);
635 }
636
637 return true;
638 }
639
640 qreal KItemListViewLayouter::minimumGroupHeaderWidth() const
641 {
642 return 100;
643 }
644
645 #include "kitemlistviewlayouter.moc"