]> cloud.milkyroute.net Git - dolphin.git/blob - src/kitemviews/kitemlistviewlayouter.cpp
Details view: Expand the name-column like in Dolphin 1.x
[dolphin.git] / src / kitemviews / 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_p.h"
21
22 #include "kitemmodelbase.h"
23 #include "kitemlistsizehintresolver_p.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_groupItemIndexes(),
50 m_groupHeaderHeight(0),
51 m_groupHeaderMargin(0),
52 m_itemInfos()
53 {
54 }
55
56 KItemListViewLayouter::~KItemListViewLayouter()
57 {
58 }
59
60 void KItemListViewLayouter::setScrollOrientation(Qt::Orientation orientation)
61 {
62 if (m_scrollOrientation != orientation) {
63 m_scrollOrientation = orientation;
64 m_dirty = true;
65 }
66 }
67
68 Qt::Orientation KItemListViewLayouter::scrollOrientation() const
69 {
70 return m_scrollOrientation;
71 }
72
73 void KItemListViewLayouter::setSize(const QSizeF& size)
74 {
75 if (m_size != size) {
76 m_size = size;
77 m_dirty = true;
78 }
79 }
80
81 QSizeF KItemListViewLayouter::size() const
82 {
83 return m_size;
84 }
85
86 void KItemListViewLayouter::setItemSize(const QSizeF& size)
87 {
88 if (m_itemSize != size) {
89 m_itemSize = size;
90 m_dirty = true;
91 }
92 }
93
94 QSizeF KItemListViewLayouter::itemSize() const
95 {
96 return m_itemSize;
97 }
98
99 void KItemListViewLayouter::setItemMargin(const QSizeF& margin)
100 {
101 if (m_itemMargin != margin) {
102 m_itemMargin = margin;
103 m_dirty = true;
104 }
105 }
106
107 QSizeF KItemListViewLayouter::itemMargin() const
108 {
109 return m_itemMargin;
110 }
111
112 void KItemListViewLayouter::setHeaderHeight(qreal height)
113 {
114 if (m_headerHeight != height) {
115 m_headerHeight = height;
116 m_dirty = true;
117 }
118 }
119
120 qreal KItemListViewLayouter::headerHeight() const
121 {
122 return m_headerHeight;
123 }
124
125 void KItemListViewLayouter::setGroupHeaderHeight(qreal height)
126 {
127 if (m_groupHeaderHeight != height) {
128 m_groupHeaderHeight = height;
129 m_dirty = true;
130 }
131 }
132
133 qreal KItemListViewLayouter::groupHeaderHeight() const
134 {
135 return m_groupHeaderHeight;
136 }
137
138 void KItemListViewLayouter::setGroupHeaderMargin(qreal margin)
139 {
140 if (m_groupHeaderMargin != margin) {
141 m_groupHeaderMargin = margin;
142 m_dirty = true;
143 }
144 }
145
146 qreal KItemListViewLayouter::groupHeaderMargin() const
147 {
148 return m_groupHeaderMargin;
149 }
150
151 void KItemListViewLayouter::setScrollOffset(qreal offset)
152 {
153 if (m_scrollOffset != offset) {
154 m_scrollOffset = offset;
155 m_visibleIndexesDirty = true;
156 }
157 }
158
159 qreal KItemListViewLayouter::scrollOffset() const
160 {
161 return m_scrollOffset;
162 }
163
164 qreal KItemListViewLayouter::maximumScrollOffset() const
165 {
166 const_cast<KItemListViewLayouter*>(this)->doLayout();
167 return m_maximumScrollOffset;
168 }
169
170 void KItemListViewLayouter::setItemOffset(qreal offset)
171 {
172 if (m_itemOffset != offset) {
173 m_itemOffset = offset;
174 m_visibleIndexesDirty = true;
175 }
176 }
177
178 qreal KItemListViewLayouter::itemOffset() const
179 {
180 return m_itemOffset;
181 }
182
183 qreal KItemListViewLayouter::maximumItemOffset() const
184 {
185 const_cast<KItemListViewLayouter*>(this)->doLayout();
186 return m_maximumItemOffset;
187 }
188
189 void KItemListViewLayouter::setModel(const KItemModelBase* model)
190 {
191 if (m_model != model) {
192 m_model = model;
193 m_dirty = true;
194 }
195 }
196
197 const KItemModelBase* KItemListViewLayouter::model() const
198 {
199 return m_model;
200 }
201
202 void KItemListViewLayouter::setSizeHintResolver(const KItemListSizeHintResolver* sizeHintResolver)
203 {
204 if (m_sizeHintResolver != sizeHintResolver) {
205 m_sizeHintResolver = sizeHintResolver;
206 m_dirty = true;
207 }
208 }
209
210 const KItemListSizeHintResolver* KItemListViewLayouter::sizeHintResolver() const
211 {
212 return m_sizeHintResolver;
213 }
214
215 int KItemListViewLayouter::firstVisibleIndex() const
216 {
217 const_cast<KItemListViewLayouter*>(this)->doLayout();
218 return m_firstVisibleIndex;
219 }
220
221 int KItemListViewLayouter::lastVisibleIndex() const
222 {
223 const_cast<KItemListViewLayouter*>(this)->doLayout();
224 return m_lastVisibleIndex;
225 }
226
227 QRectF KItemListViewLayouter::itemRect(int index) const
228 {
229 const_cast<KItemListViewLayouter*>(this)->doLayout();
230 if (index < 0 || index >= m_itemInfos.count()) {
231 return QRectF();
232 }
233
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;
241 bounds.moveTo(pos);
242 return bounds;
243 }
244
245 QRectF bounds = m_itemInfos[index].rect;
246 bounds.moveTo(bounds.topLeft() - QPointF(m_itemOffset, m_scrollOffset));
247 return bounds;
248 }
249
250 QRectF KItemListViewLayouter::groupHeaderRect(int index) const
251 {
252 const_cast<KItemListViewLayouter*>(this)->doLayout();
253
254 const QRectF firstItemRect = itemRect(index);
255 QPointF pos = firstItemRect.topLeft();
256 if (pos.isNull()) {
257 return QRectF();
258 }
259
260 QSizeF size;
261 if (m_scrollOrientation == Qt::Vertical) {
262 pos.rx() = 0;
263 pos.ry() -= m_groupHeaderHeight;
264 size = QSizeF(m_size.width(), m_groupHeaderHeight);
265 } else {
266 pos.rx() -= m_itemMargin.width();
267 pos.ry() = 0;
268
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) {
279 break;
280 }
281
282 if (bounds.height() > width) {
283 width = bounds.height();
284 }
285
286 ++index;
287 }
288
289 size = QSizeF(width, m_size.height());
290 }
291 return QRectF(pos, size);
292 }
293
294 int KItemListViewLayouter::itemColumn(int index) const
295 {
296 const_cast<KItemListViewLayouter*>(this)->doLayout();
297 if (index < 0 || index >= m_itemInfos.count()) {
298 return -1;
299 }
300
301 return (m_scrollOrientation == Qt::Vertical)
302 ? m_itemInfos[index].column
303 : m_itemInfos[index].row;
304 }
305
306 int KItemListViewLayouter::itemRow(int index) const
307 {
308 const_cast<KItemListViewLayouter*>(this)->doLayout();
309 if (index < 0 || index >= m_itemInfos.count()) {
310 return -1;
311 }
312
313 return (m_scrollOrientation == Qt::Vertical)
314 ? m_itemInfos[index].row
315 : m_itemInfos[index].column;
316 }
317
318 int KItemListViewLayouter::maximumVisibleItems() const
319 {
320 const_cast<KItemListViewLayouter*>(this)->doLayout();
321
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) {
326 ++rows;
327 }
328
329 return rows * m_columnCount;
330 }
331
332 bool KItemListViewLayouter::isFirstGroupItem(int itemIndex) const
333 {
334 const_cast<KItemListViewLayouter*>(this)->doLayout();
335 return m_groupItemIndexes.contains(itemIndex);
336 }
337
338 void KItemListViewLayouter::markAsDirty()
339 {
340 m_dirty = true;
341 }
342
343
344 #ifndef QT_NO_DEBUG
345 bool KItemListViewLayouter::isDirty()
346 {
347 return m_dirty;
348 }
349 #endif
350
351 void KItemListViewLayouter::doLayout()
352 {
353 if (m_dirty) {
354 #ifdef KITEMLISTVIEWLAYOUTER_DEBUG
355 QElapsedTimer timer;
356 timer.start();
357 #endif
358 m_visibleIndexesDirty = true;
359
360 QSizeF itemSize = m_itemSize;
361 QSizeF itemMargin = m_itemMargin;
362 QSizeF size = m_size;
363
364 const bool grouped = createGroupHeaders();
365
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());
376
377 if (grouped) {
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;
382 }
383 }
384
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();
389
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;
398 }
399 }
400
401 int rowCount = itemCount / m_columnCount;
402 if (itemCount % m_columnCount != 0) {
403 ++rowCount;
404 }
405
406 m_itemInfos.reserve(itemCount);
407
408 qreal y = m_headerHeight + itemMargin.height();
409 int row = 0;
410
411 int index = 0;
412 while (index < itemCount) {
413 qreal x = m_xPosInc;
414 qreal maxItemHeight = itemSize.height();
415
416 if (grouped) {
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;
421 }
422
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.
427 if (index > 0) {
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();
434 }
435
436 if (!horizontalScrolling) {
437 y += m_groupHeaderHeight;
438 }
439 }
440 }
441
442 int column = 0;
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;
450 }
451 }
452
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;
458 } else {
459 ItemInfo itemInfo;
460 itemInfo.rect = bounds;
461 itemInfo.column = column;
462 itemInfo.row = row;
463 m_itemInfos.append(itemInfo);
464 }
465
466 if (grouped && horizontalScrolling) {
467 // When grouping is enabled in the horizontal mode, the header alignment
468 // looks like this:
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).
477 //
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;
482 }
483 }
484
485 maxItemHeight = qMax(maxItemHeight, requiredItemHeight);
486 x += m_columnWidth;
487 ++index;
488 ++column;
489
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
493 break;
494 }
495 }
496
497 y += maxItemHeight + itemMargin.height();
498 ++row;
499 }
500 if (m_itemInfos.count() > itemCount) {
501 m_itemInfos.erase(m_itemInfos.begin() + itemCount,
502 m_itemInfos.end());
503 }
504
505 if (itemCount > 0) {
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();
509
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());
513 --index;
514 }
515
516 m_maximumScrollOffset += itemMargin.height();
517
518 m_maximumItemOffset = m_columnCount * m_columnWidth;
519 } else {
520 m_maximumScrollOffset = 0;
521 m_maximumItemOffset = 0;
522 }
523
524 #ifdef KITEMLISTVIEWLAYOUTER_DEBUG
525 kDebug() << "[TIME] doLayout() for " << m_model->count() << "items:" << timer.elapsed();
526 #endif
527 m_dirty = false;
528 }
529
530 updateVisibleIndexes();
531 }
532
533 void KItemListViewLayouter::updateVisibleIndexes()
534 {
535 if (!m_visibleIndexesDirty) {
536 return;
537 }
538
539 Q_ASSERT(!m_dirty);
540
541 if (m_model->count() <= 0) {
542 m_firstVisibleIndex = -1;
543 m_lastVisibleIndex = -1;
544 m_visibleIndexesDirty = false;
545 return;
546 }
547
548 const int maxIndex = m_model->count() - 1;
549
550 // Calculate the first visible index that is fully visible
551 int min = 0;
552 int max = maxIndex;
553 int mid = 0;
554 do {
555 mid = (min + max) / 2;
556 if (m_itemInfos[mid].rect.top() < m_scrollOffset) {
557 min = mid + 1;
558 } else {
559 max = mid - 1;
560 }
561 } while (min <= max);
562
563 if (mid > 0) {
564 // Include the row before the first fully visible index, as it might
565 // be partly visible
566 if (m_itemInfos[mid].rect.top() >= m_scrollOffset) {
567 --mid;
568 Q_ASSERT(m_itemInfos[mid].rect.top() < m_scrollOffset);
569 }
570
571 const qreal rowTop = m_itemInfos[mid].rect.top();
572 while (mid > 0 && m_itemInfos[mid - 1].rect.top() == rowTop) {
573 --mid;
574 }
575 }
576 m_firstVisibleIndex = mid;
577
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;
583 }
584
585 min = m_firstVisibleIndex;
586 max = maxIndex;
587 do {
588 mid = (min + max) / 2;
589 if (m_itemInfos[mid].rect.y() <= bottom) {
590 min = mid + 1;
591 } else {
592 max = mid - 1;
593 }
594 } while (min <= max);
595
596 while (mid > 0 && m_itemInfos[mid].rect.y() > bottom) {
597 --mid;
598 }
599 m_lastVisibleIndex = mid;
600
601 m_visibleIndexesDirty = false;
602 }
603
604 bool KItemListViewLayouter::createGroupHeaders()
605 {
606 if (!m_model->groupedSorting()) {
607 return false;
608 }
609
610 m_groupItemIndexes.clear();
611
612 const QList<QPair<int, QVariant> > groups = m_model->groups();
613 if (groups.isEmpty()) {
614 return false;
615 }
616
617 for (int i = 0; i < groups.count(); ++i) {
618 const int firstItemIndex = groups.at(i).first;
619 m_groupItemIndexes.insert(firstItemIndex);
620 }
621
622 return true;
623 }
624
625 qreal KItemListViewLayouter::minimumGroupHeaderWidth() const
626 {
627 return 100;
628 }
629
630 #include "kitemlistviewlayouter_p.moc"