]> cloud.milkyroute.net Git - dolphin.git/blob - src/kitemviews/private/kitemlistviewlayouter.cpp
Merge branch 'master' into frameworks
[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 "dolphindebug.h"
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 sizeHint.transpose();
243 return QRectF(pos, sizeHint);
244 }
245
246 if (sizeHint.width() <= 0) {
247 // In Details View, a size hint with negative width is used internally.
248 sizeHint.rwidth() = m_itemSize.width();
249 }
250
251 const QPointF pos(x - m_itemOffset, y - m_scrollOffset);
252 return QRectF(pos, sizeHint);
253 }
254
255 QRectF KItemListViewLayouter::groupHeaderRect(int index) const
256 {
257 const_cast<KItemListViewLayouter*>(this)->doLayout();
258
259 const QRectF firstItemRect = itemRect(index);
260 QPointF pos = firstItemRect.topLeft();
261 if (pos.isNull()) {
262 return QRectF();
263 }
264
265 QSizeF size;
266 if (m_scrollOrientation == Qt::Vertical) {
267 pos.rx() = 0;
268 pos.ry() -= m_groupHeaderHeight;
269 size = QSizeF(m_size.width(), m_groupHeaderHeight);
270 } else {
271 pos.rx() -= m_itemMargin.width();
272 pos.ry() = 0;
273
274 // Determine the maximum width used in the current column. As the
275 // scroll-direction is Qt::Horizontal and m_itemRects is accessed
276 // directly, the logical height represents the visual width, and
277 // the logical row represents the column.
278 qreal headerWidth = minimumGroupHeaderWidth();
279 const int row = m_itemInfos[index].row;
280 const int maxIndex = m_itemInfos.count() - 1;
281 while (index <= maxIndex) {
282 if (m_itemInfos[index].row != row) {
283 break;
284 }
285
286 const qreal itemWidth = (m_scrollOrientation == Qt::Vertical)
287 ? m_sizeHintResolver->sizeHint(index).width()
288 : m_sizeHintResolver->sizeHint(index).height();
289
290 if (itemWidth > headerWidth) {
291 headerWidth = itemWidth;
292 }
293
294 ++index;
295 }
296
297 size = QSizeF(headerWidth, m_size.height());
298 }
299 return QRectF(pos, size);
300 }
301
302 int KItemListViewLayouter::itemColumn(int index) const
303 {
304 const_cast<KItemListViewLayouter*>(this)->doLayout();
305 if (index < 0 || index >= m_itemInfos.count()) {
306 return -1;
307 }
308
309 return (m_scrollOrientation == Qt::Vertical)
310 ? m_itemInfos[index].column
311 : m_itemInfos[index].row;
312 }
313
314 int KItemListViewLayouter::itemRow(int index) const
315 {
316 const_cast<KItemListViewLayouter*>(this)->doLayout();
317 if (index < 0 || index >= m_itemInfos.count()) {
318 return -1;
319 }
320
321 return (m_scrollOrientation == Qt::Vertical)
322 ? m_itemInfos[index].row
323 : m_itemInfos[index].column;
324 }
325
326 int KItemListViewLayouter::maximumVisibleItems() const
327 {
328 const_cast<KItemListViewLayouter*>(this)->doLayout();
329
330 const int height = static_cast<int>(m_size.height());
331 const int rowHeight = static_cast<int>(m_itemSize.height());
332 int rows = height / rowHeight;
333 if (height % rowHeight != 0) {
334 ++rows;
335 }
336
337 return rows * m_columnCount;
338 }
339
340 bool KItemListViewLayouter::isFirstGroupItem(int itemIndex) const
341 {
342 const_cast<KItemListViewLayouter*>(this)->doLayout();
343 return m_groupItemIndexes.contains(itemIndex);
344 }
345
346 void KItemListViewLayouter::markAsDirty()
347 {
348 m_dirty = true;
349 }
350
351
352 #ifndef QT_NO_DEBUG
353 bool KItemListViewLayouter::isDirty()
354 {
355 return m_dirty;
356 }
357 #endif
358
359 void KItemListViewLayouter::doLayout()
360 {
361 if (m_dirty) {
362 #ifdef KITEMLISTVIEWLAYOUTER_DEBUG
363 QElapsedTimer timer;
364 timer.start();
365 #endif
366 m_visibleIndexesDirty = true;
367
368 QSizeF itemSize = m_itemSize;
369 QSizeF itemMargin = m_itemMargin;
370 QSizeF size = m_size;
371
372 const bool grouped = createGroupHeaders();
373
374 const bool horizontalScrolling = (m_scrollOrientation == Qt::Horizontal);
375 if (horizontalScrolling) {
376 // Flip everything so that the layout logically can work like having
377 // a vertical scrolling
378 itemSize.transpose();
379 itemMargin.transpose();
380 size.transpose();
381
382 if (grouped) {
383 // In the horizontal scrolling case all groups are aligned
384 // at the top, which decreases the available height. For the
385 // flipped data this means that the width must be decreased.
386 size.rwidth() -= m_groupHeaderHeight;
387 }
388 }
389
390 m_columnWidth = itemSize.width() + itemMargin.width();
391 const qreal widthForColumns = size.width() - itemMargin.width();
392 m_columnCount = qMax(1, int(widthForColumns / m_columnWidth));
393 m_xPosInc = itemMargin.width();
394
395 const int itemCount = m_model->count();
396 if (itemCount > m_columnCount && m_columnWidth >= 32) {
397 // Apply the unused width equally to each column
398 const qreal unusedWidth = widthForColumns - m_columnCount * m_columnWidth;
399 if (unusedWidth > 0) {
400 const qreal columnInc = unusedWidth / (m_columnCount + 1);
401 m_columnWidth += columnInc;
402 m_xPosInc += columnInc;
403 }
404 }
405
406 m_itemInfos.resize(itemCount);
407
408 // Calculate the offset of each column, i.e., the x-coordinate where the column starts.
409 m_columnOffsets.resize(m_columnCount);
410 qreal currentOffset = m_xPosInc;
411
412 if (grouped && horizontalScrolling) {
413 // All group headers will always be aligned on the top and not
414 // flipped like the other properties.
415 currentOffset += m_groupHeaderHeight;
416 }
417
418 for (int column = 0; column < m_columnCount; ++column) {
419 m_columnOffsets[column] = currentOffset;
420 currentOffset += m_columnWidth;
421 }
422
423 // Prepare the QVector which stores the y-coordinate for each new row.
424 int numberOfRows = (itemCount + m_columnCount - 1) / m_columnCount;
425 if (grouped && m_columnCount > 1) {
426 // In the worst case, a new row will be started for every group.
427 // We could calculate the exact number of rows now to prevent that we reserve
428 // too much memory, but the code required to do that might need much more
429 // memory than it would save in the average case.
430 numberOfRows += m_groupItemIndexes.count();
431 }
432 m_rowOffsets.resize(numberOfRows);
433
434 qreal y = m_headerHeight + itemMargin.height();
435 int row = 0;
436
437 int index = 0;
438 while (index < itemCount) {
439 qreal maxItemHeight = itemSize.height();
440
441 if (grouped) {
442 if (m_groupItemIndexes.contains(index)) {
443 // The item is the first item of a group.
444 // Increase the y-position to provide space
445 // for the group header.
446 if (index > 0) {
447 // Only add a margin if there has been added another
448 // group already before
449 y += m_groupHeaderMargin;
450 } else if (!horizontalScrolling) {
451 // The first group header should be aligned on top
452 y -= itemMargin.height();
453 }
454
455 if (!horizontalScrolling) {
456 y += m_groupHeaderHeight;
457 }
458 }
459 }
460
461 m_rowOffsets[row] = y;
462
463 int column = 0;
464 while (index < itemCount && column < m_columnCount) {
465 qreal requiredItemHeight = itemSize.height();
466 const QSizeF sizeHint = m_sizeHintResolver->sizeHint(index);
467 const qreal sizeHintHeight = sizeHint.height();
468 if (sizeHintHeight > requiredItemHeight) {
469 requiredItemHeight = sizeHintHeight;
470 }
471
472 ItemInfo& itemInfo = m_itemInfos[index];
473 itemInfo.column = column;
474 itemInfo.row = row;
475
476 if (grouped && horizontalScrolling) {
477 // When grouping is enabled in the horizontal mode, the header alignment
478 // looks like this:
479 // Header-1 Header-2 Header-3
480 // Item 1 Item 4 Item 7
481 // Item 2 Item 5 Item 8
482 // Item 3 Item 6 Item 9
483 // In this case 'requiredItemHeight' represents the column-width. We don't
484 // check the content of the header in the layouter to determine the required
485 // width, hence assure that at least a minimal width of 15 characters is given
486 // (in average a character requires the halve width of the font height).
487 //
488 // TODO: Let the group headers provide a minimum width and respect this width here
489 const qreal headerWidth = minimumGroupHeaderWidth();
490 if (requiredItemHeight < headerWidth) {
491 requiredItemHeight = headerWidth;
492 }
493 }
494
495 maxItemHeight = qMax(maxItemHeight, requiredItemHeight);
496 ++index;
497 ++column;
498
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
502 break;
503 }
504 }
505
506 y += maxItemHeight + itemMargin.height();
507 ++row;
508 }
509
510 if (itemCount > 0) {
511 m_maximumScrollOffset = y;
512 m_maximumItemOffset = m_columnCount * m_columnWidth;
513 } else {
514 m_maximumScrollOffset = 0;
515 m_maximumItemOffset = 0;
516 }
517
518 #ifdef KITEMLISTVIEWLAYOUTER_DEBUG
519 qCDebug(DolphinDebug) << "[TIME] doLayout() for " << m_model->count() << "items:" << timer.elapsed();
520 #endif
521 m_dirty = false;
522 }
523
524 updateVisibleIndexes();
525 }
526
527 void KItemListViewLayouter::updateVisibleIndexes()
528 {
529 if (!m_visibleIndexesDirty) {
530 return;
531 }
532
533 Q_ASSERT(!m_dirty);
534
535 if (m_model->count() <= 0) {
536 m_firstVisibleIndex = -1;
537 m_lastVisibleIndex = -1;
538 m_visibleIndexesDirty = false;
539 return;
540 }
541
542 const int maxIndex = m_model->count() - 1;
543
544 // Calculate the first visible index that is fully visible
545 int min = 0;
546 int max = maxIndex;
547 int mid = 0;
548 do {
549 mid = (min + max) / 2;
550 if (m_rowOffsets.at(m_itemInfos[mid].row) < m_scrollOffset) {
551 min = mid + 1;
552 } else {
553 max = mid - 1;
554 }
555 } while (min <= max);
556
557 if (mid > 0) {
558 // Include the row before the first fully visible index, as it might
559 // be partly visible
560 if (m_rowOffsets.at(m_itemInfos[mid].row) >= m_scrollOffset) {
561 --mid;
562 Q_ASSERT(m_rowOffsets.at(m_itemInfos[mid].row) < m_scrollOffset);
563 }
564
565 const int firstVisibleRow = m_itemInfos[mid].row;
566 while (mid > 0 && m_itemInfos[mid - 1].row == firstVisibleRow) {
567 --mid;
568 }
569 }
570 m_firstVisibleIndex = mid;
571
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;
577 }
578
579 min = m_firstVisibleIndex;
580 max = maxIndex;
581 do {
582 mid = (min + max) / 2;
583 if (m_rowOffsets.at(m_itemInfos[mid].row) <= bottom) {
584 min = mid + 1;
585 } else {
586 max = mid - 1;
587 }
588 } while (min <= max);
589
590 while (mid > 0 && m_rowOffsets.at(m_itemInfos[mid].row) > bottom) {
591 --mid;
592 }
593 m_lastVisibleIndex = mid;
594
595 m_visibleIndexesDirty = false;
596 }
597
598 bool KItemListViewLayouter::createGroupHeaders()
599 {
600 if (!m_model->groupedSorting()) {
601 return false;
602 }
603
604 m_groupItemIndexes.clear();
605
606 const QList<QPair<int, QVariant> > groups = m_model->groups();
607 if (groups.isEmpty()) {
608 return false;
609 }
610
611 for (int i = 0; i < groups.count(); ++i) {
612 const int firstItemIndex = groups.at(i).first;
613 m_groupItemIndexes.insert(firstItemIndex);
614 }
615
616 return true;
617 }
618
619 qreal KItemListViewLayouter::minimumGroupHeaderWidth() const
620 {
621 return 100;
622 }
623