]> cloud.milkyroute.net Git - dolphin.git/blob - src/kitemviews/private/kitemlistviewlayouter.cpp
Merge remote-tracking branch 'origin/KDE/4.12' into KDE/4.13
[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(KItemListSizeHintResolver* sizeHintResolver, 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(sizeHintResolver),
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 Q_ASSERT(m_sizeHintResolver);
57 }
58
59 KItemListViewLayouter::~KItemListViewLayouter()
60 {
61 }
62
63 void KItemListViewLayouter::setScrollOrientation(Qt::Orientation orientation)
64 {
65 if (m_scrollOrientation != orientation) {
66 m_scrollOrientation = orientation;
67 m_dirty = true;
68 }
69 }
70
71 Qt::Orientation KItemListViewLayouter::scrollOrientation() const
72 {
73 return m_scrollOrientation;
74 }
75
76 void KItemListViewLayouter::setSize(const QSizeF& size)
77 {
78 if (m_size != size) {
79 if (m_scrollOrientation == Qt::Vertical) {
80 if (m_size.width() != size.width()) {
81 m_dirty = true;
82 }
83 } else if (m_size.height() != size.height()) {
84 m_dirty = true;
85 }
86
87 m_size = size;
88 m_visibleIndexesDirty = true;
89 }
90 }
91
92 QSizeF KItemListViewLayouter::size() const
93 {
94 return m_size;
95 }
96
97 void KItemListViewLayouter::setItemSize(const QSizeF& size)
98 {
99 if (m_itemSize != size) {
100 m_itemSize = size;
101 m_dirty = true;
102 }
103 }
104
105 QSizeF KItemListViewLayouter::itemSize() const
106 {
107 return m_itemSize;
108 }
109
110 void KItemListViewLayouter::setItemMargin(const QSizeF& margin)
111 {
112 if (m_itemMargin != margin) {
113 m_itemMargin = margin;
114 m_dirty = true;
115 }
116 }
117
118 QSizeF KItemListViewLayouter::itemMargin() const
119 {
120 return m_itemMargin;
121 }
122
123 void KItemListViewLayouter::setHeaderHeight(qreal height)
124 {
125 if (m_headerHeight != height) {
126 m_headerHeight = height;
127 m_dirty = true;
128 }
129 }
130
131 qreal KItemListViewLayouter::headerHeight() const
132 {
133 return m_headerHeight;
134 }
135
136 void KItemListViewLayouter::setGroupHeaderHeight(qreal height)
137 {
138 if (m_groupHeaderHeight != height) {
139 m_groupHeaderHeight = height;
140 m_dirty = true;
141 }
142 }
143
144 qreal KItemListViewLayouter::groupHeaderHeight() const
145 {
146 return m_groupHeaderHeight;
147 }
148
149 void KItemListViewLayouter::setGroupHeaderMargin(qreal margin)
150 {
151 if (m_groupHeaderMargin != margin) {
152 m_groupHeaderMargin = margin;
153 m_dirty = true;
154 }
155 }
156
157 qreal KItemListViewLayouter::groupHeaderMargin() const
158 {
159 return m_groupHeaderMargin;
160 }
161
162 void KItemListViewLayouter::setScrollOffset(qreal offset)
163 {
164 if (m_scrollOffset != offset) {
165 m_scrollOffset = offset;
166 m_visibleIndexesDirty = true;
167 }
168 }
169
170 qreal KItemListViewLayouter::scrollOffset() const
171 {
172 return m_scrollOffset;
173 }
174
175 qreal KItemListViewLayouter::maximumScrollOffset() const
176 {
177 const_cast<KItemListViewLayouter*>(this)->doLayout();
178 return m_maximumScrollOffset;
179 }
180
181 void KItemListViewLayouter::setItemOffset(qreal offset)
182 {
183 if (m_itemOffset != offset) {
184 m_itemOffset = offset;
185 m_visibleIndexesDirty = true;
186 }
187 }
188
189 qreal KItemListViewLayouter::itemOffset() const
190 {
191 return m_itemOffset;
192 }
193
194 qreal KItemListViewLayouter::maximumItemOffset() const
195 {
196 const_cast<KItemListViewLayouter*>(this)->doLayout();
197 return m_maximumItemOffset;
198 }
199
200 void KItemListViewLayouter::setModel(const KItemModelBase* model)
201 {
202 if (m_model != model) {
203 m_model = model;
204 m_dirty = true;
205 }
206 }
207
208 const KItemModelBase* KItemListViewLayouter::model() const
209 {
210 return m_model;
211 }
212
213 int KItemListViewLayouter::firstVisibleIndex() const
214 {
215 const_cast<KItemListViewLayouter*>(this)->doLayout();
216 return m_firstVisibleIndex;
217 }
218
219 int KItemListViewLayouter::lastVisibleIndex() const
220 {
221 const_cast<KItemListViewLayouter*>(this)->doLayout();
222 return m_lastVisibleIndex;
223 }
224
225 QRectF KItemListViewLayouter::itemRect(int index) const
226 {
227 const_cast<KItemListViewLayouter*>(this)->doLayout();
228 if (index < 0 || index >= m_itemInfos.count()) {
229 return QRectF();
230 }
231
232 QSizeF sizeHint = m_sizeHintResolver->sizeHint(index);
233
234 const qreal x = m_columnOffsets.at(m_itemInfos.at(index).column);
235 const qreal y = m_rowOffsets.at(m_itemInfos.at(index).row);
236
237 if (m_scrollOrientation == Qt::Horizontal) {
238 // Rotate the logical direction which is always vertical by 90°
239 // to get the physical horizontal direction
240 QPointF pos(y, x);
241 pos.rx() -= m_scrollOffset;
242 return QRectF(pos, sizeHint);
243 }
244
245 if (sizeHint.width() <= 0) {
246 // In Details View, a size hint with negative width is used internally.
247 sizeHint.rwidth() = m_itemSize.width();
248 }
249
250 const QPointF pos(x - m_itemOffset, y - m_scrollOffset);
251 return QRectF(pos, sizeHint);
252 }
253
254 QRectF KItemListViewLayouter::groupHeaderRect(int index) const
255 {
256 const_cast<KItemListViewLayouter*>(this)->doLayout();
257
258 const QRectF firstItemRect = itemRect(index);
259 QPointF pos = firstItemRect.topLeft();
260 if (pos.isNull()) {
261 return QRectF();
262 }
263
264 QSizeF size;
265 if (m_scrollOrientation == Qt::Vertical) {
266 pos.rx() = 0;
267 pos.ry() -= m_groupHeaderHeight;
268 size = QSizeF(m_size.width(), m_groupHeaderHeight);
269 } else {
270 pos.rx() -= m_itemMargin.width();
271 pos.ry() = 0;
272
273 // Determine the maximum width used in the current column. As the
274 // scroll-direction is Qt::Horizontal and m_itemRects is accessed
275 // directly, the logical height represents the visual width, and
276 // the logical row represents the column.
277 qreal headerWidth = minimumGroupHeaderWidth();
278 const int row = m_itemInfos[index].row;
279 const int maxIndex = m_itemInfos.count() - 1;
280 while (index <= maxIndex) {
281 if (m_itemInfos[index].row != row) {
282 break;
283 }
284
285 const qreal itemWidth = m_sizeHintResolver->sizeHint(index).width();
286
287 if (itemWidth > headerWidth) {
288 headerWidth = itemWidth;
289 }
290
291 ++index;
292 }
293
294 size = QSizeF(headerWidth, m_size.height());
295 }
296 return QRectF(pos, size);
297 }
298
299 int KItemListViewLayouter::itemColumn(int index) const
300 {
301 const_cast<KItemListViewLayouter*>(this)->doLayout();
302 if (index < 0 || index >= m_itemInfos.count()) {
303 return -1;
304 }
305
306 return (m_scrollOrientation == Qt::Vertical)
307 ? m_itemInfos[index].column
308 : m_itemInfos[index].row;
309 }
310
311 int KItemListViewLayouter::itemRow(int index) const
312 {
313 const_cast<KItemListViewLayouter*>(this)->doLayout();
314 if (index < 0 || index >= m_itemInfos.count()) {
315 return -1;
316 }
317
318 return (m_scrollOrientation == Qt::Vertical)
319 ? m_itemInfos[index].row
320 : m_itemInfos[index].column;
321 }
322
323 int KItemListViewLayouter::maximumVisibleItems() const
324 {
325 const_cast<KItemListViewLayouter*>(this)->doLayout();
326
327 const int height = static_cast<int>(m_size.height());
328 const int rowHeight = static_cast<int>(m_itemSize.height());
329 int rows = height / rowHeight;
330 if (height % rowHeight != 0) {
331 ++rows;
332 }
333
334 return rows * m_columnCount;
335 }
336
337 bool KItemListViewLayouter::isFirstGroupItem(int itemIndex) const
338 {
339 const_cast<KItemListViewLayouter*>(this)->doLayout();
340 return m_groupItemIndexes.contains(itemIndex);
341 }
342
343 void KItemListViewLayouter::markAsDirty()
344 {
345 m_dirty = true;
346 }
347
348
349 #ifndef QT_NO_DEBUG
350 bool KItemListViewLayouter::isDirty()
351 {
352 return m_dirty;
353 }
354 #endif
355
356 void KItemListViewLayouter::doLayout()
357 {
358 if (m_dirty) {
359 #ifdef KITEMLISTVIEWLAYOUTER_DEBUG
360 QElapsedTimer timer;
361 timer.start();
362 #endif
363 m_visibleIndexesDirty = true;
364
365 QSizeF itemSize = m_itemSize;
366 QSizeF itemMargin = m_itemMargin;
367 QSizeF size = m_size;
368
369 const bool grouped = createGroupHeaders();
370
371 const bool horizontalScrolling = (m_scrollOrientation == Qt::Horizontal);
372 if (horizontalScrolling) {
373 // Flip everything so that the layout logically can work like having
374 // a vertical scrolling
375 itemSize.transpose();
376 itemMargin.transpose();
377 size.transpose();
378
379 if (grouped) {
380 // In the horizontal scrolling case all groups are aligned
381 // at the top, which decreases the available height. For the
382 // flipped data this means that the width must be decreased.
383 size.rwidth() -= m_groupHeaderHeight;
384 }
385 }
386
387 m_columnWidth = itemSize.width() + itemMargin.width();
388 const qreal widthForColumns = size.width() - itemMargin.width();
389 m_columnCount = qMax(1, int(widthForColumns / m_columnWidth));
390 m_xPosInc = itemMargin.width();
391
392 const int itemCount = m_model->count();
393 if (itemCount > m_columnCount && m_columnWidth >= 32) {
394 // Apply the unused width equally to each column
395 const qreal unusedWidth = widthForColumns - m_columnCount * m_columnWidth;
396 if (unusedWidth > 0) {
397 const qreal columnInc = unusedWidth / (m_columnCount + 1);
398 m_columnWidth += columnInc;
399 m_xPosInc += columnInc;
400 }
401 }
402
403 m_itemInfos.resize(itemCount);
404
405 // Calculate the offset of each column, i.e., the x-coordinate where the column starts.
406 m_columnOffsets.resize(m_columnCount);
407 qreal currentOffset = m_xPosInc;
408
409 if (grouped && horizontalScrolling) {
410 // All group headers will always be aligned on the top and not
411 // flipped like the other properties.
412 currentOffset += m_groupHeaderHeight;
413 }
414
415 for (int column = 0; column < m_columnCount; ++column) {
416 m_columnOffsets[column] = currentOffset;
417 currentOffset += m_columnWidth;
418 }
419
420 // Prepare the QVector which stores the y-coordinate for each new row.
421 int numberOfRows = (itemCount + m_columnCount - 1) / m_columnCount;
422 if (grouped && m_columnCount > 1) {
423 // In the worst case, a new row will be started for every group.
424 // We could calculate the exact number of rows now to prevent that we reserve
425 // too much memory, but the code required to do that might need much more
426 // memory than it would save in the average case.
427 numberOfRows += m_groupItemIndexes.count();
428 }
429 m_rowOffsets.resize(numberOfRows);
430
431 qreal y = m_headerHeight + itemMargin.height();
432 int row = 0;
433
434 int index = 0;
435 while (index < itemCount) {
436 qreal maxItemHeight = itemSize.height();
437
438 if (grouped) {
439 if (m_groupItemIndexes.contains(index)) {
440 // The item is the first item of a group.
441 // Increase the y-position to provide space
442 // for the group header.
443 if (index > 0) {
444 // Only add a margin if there has been added another
445 // group already before
446 y += m_groupHeaderMargin;
447 } else if (!horizontalScrolling) {
448 // The first group header should be aligned on top
449 y -= itemMargin.height();
450 }
451
452 if (!horizontalScrolling) {
453 y += m_groupHeaderHeight;
454 }
455 }
456 }
457
458 m_rowOffsets[row] = y;
459
460 int column = 0;
461 while (index < itemCount && column < m_columnCount) {
462 qreal requiredItemHeight = itemSize.height();
463 const QSizeF sizeHint = m_sizeHintResolver->sizeHint(index);
464 const qreal sizeHintHeight = horizontalScrolling ? sizeHint.width() : sizeHint.height();
465 if (sizeHintHeight > requiredItemHeight) {
466 requiredItemHeight = sizeHintHeight;
467 }
468
469 ItemInfo& itemInfo = m_itemInfos[index];
470 itemInfo.column = column;
471 itemInfo.row = row;
472
473 if (grouped && horizontalScrolling) {
474 // When grouping is enabled in the horizontal mode, the header alignment
475 // looks like this:
476 // Header-1 Header-2 Header-3
477 // Item 1 Item 4 Item 7
478 // Item 2 Item 5 Item 8
479 // Item 3 Item 6 Item 9
480 // In this case 'requiredItemHeight' represents the column-width. We don't
481 // check the content of the header in the layouter to determine the required
482 // width, hence assure that at least a minimal width of 15 characters is given
483 // (in average a character requires the halve width of the font height).
484 //
485 // TODO: Let the group headers provide a minimum width and respect this width here
486 const qreal headerWidth = minimumGroupHeaderWidth();
487 if (requiredItemHeight < headerWidth) {
488 requiredItemHeight = headerWidth;
489 }
490 }
491
492 maxItemHeight = qMax(maxItemHeight, requiredItemHeight);
493 ++index;
494 ++column;
495
496 if (grouped && m_groupItemIndexes.contains(index)) {
497 // The item represents the first index of a group
498 // and must aligned in the first column
499 break;
500 }
501 }
502
503 y += maxItemHeight + itemMargin.height();
504 ++row;
505 }
506
507 if (itemCount > 0) {
508 m_maximumScrollOffset = y;
509 m_maximumItemOffset = m_columnCount * m_columnWidth;
510 } else {
511 m_maximumScrollOffset = 0;
512 m_maximumItemOffset = 0;
513 }
514
515 #ifdef KITEMLISTVIEWLAYOUTER_DEBUG
516 kDebug() << "[TIME] doLayout() for " << m_model->count() << "items:" << timer.elapsed();
517 #endif
518 m_dirty = false;
519 }
520
521 updateVisibleIndexes();
522 }
523
524 void KItemListViewLayouter::updateVisibleIndexes()
525 {
526 if (!m_visibleIndexesDirty) {
527 return;
528 }
529
530 Q_ASSERT(!m_dirty);
531
532 if (m_model->count() <= 0) {
533 m_firstVisibleIndex = -1;
534 m_lastVisibleIndex = -1;
535 m_visibleIndexesDirty = false;
536 return;
537 }
538
539 const int maxIndex = m_model->count() - 1;
540
541 // Calculate the first visible index that is fully visible
542 int min = 0;
543 int max = maxIndex;
544 int mid = 0;
545 do {
546 mid = (min + max) / 2;
547 if (m_rowOffsets.at(m_itemInfos[mid].row) < m_scrollOffset) {
548 min = mid + 1;
549 } else {
550 max = mid - 1;
551 }
552 } while (min <= max);
553
554 if (mid > 0) {
555 // Include the row before the first fully visible index, as it might
556 // be partly visible
557 if (m_rowOffsets.at(m_itemInfos[mid].row) >= m_scrollOffset) {
558 --mid;
559 Q_ASSERT(m_rowOffsets.at(m_itemInfos[mid].row) < m_scrollOffset);
560 }
561
562 const int firstVisibleRow = m_itemInfos[mid].row;
563 while (mid > 0 && m_itemInfos[mid - 1].row == firstVisibleRow) {
564 --mid;
565 }
566 }
567 m_firstVisibleIndex = mid;
568
569 // Calculate the last visible index that is (at least partly) visible
570 const int visibleHeight = (m_scrollOrientation == Qt::Horizontal) ? m_size.width() : m_size.height();
571 qreal bottom = m_scrollOffset + visibleHeight;
572 if (m_model->groupedSorting()) {
573 bottom += m_groupHeaderHeight;
574 }
575
576 min = m_firstVisibleIndex;
577 max = maxIndex;
578 do {
579 mid = (min + max) / 2;
580 if (m_rowOffsets.at(m_itemInfos[mid].row) <= bottom) {
581 min = mid + 1;
582 } else {
583 max = mid - 1;
584 }
585 } while (min <= max);
586
587 while (mid > 0 && m_rowOffsets.at(m_itemInfos[mid].row) > bottom) {
588 --mid;
589 }
590 m_lastVisibleIndex = mid;
591
592 m_visibleIndexesDirty = false;
593 }
594
595 bool KItemListViewLayouter::createGroupHeaders()
596 {
597 if (!m_model->groupedSorting()) {
598 return false;
599 }
600
601 m_groupItemIndexes.clear();
602
603 const QList<QPair<int, QVariant> > groups = m_model->groups();
604 if (groups.isEmpty()) {
605 return false;
606 }
607
608 for (int i = 0; i < groups.count(); ++i) {
609 const int firstItemIndex = groups.at(i).first;
610 m_groupItemIndexes.insert(firstItemIndex);
611 }
612
613 return true;
614 }
615
616 qreal KItemListViewLayouter::minimumGroupHeaderWidth() const
617 {
618 return 100;
619 }
620
621 #include "kitemlistviewlayouter.moc"