]> cloud.milkyroute.net Git - dolphin.git/blob - src/kitemviews/private/kitemlistviewlayouter.cpp
Do not select items when navigating back/forward with the mouse
[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_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 if (m_scrollOrientation == Qt::Vertical) {
77 if (m_size.width() != size.width()) {
78 m_dirty = true;
79 }
80 } else if (m_size.height() != size.height()) {
81 m_dirty = true;
82 }
83
84 m_size = size;
85 m_visibleIndexesDirty = true;
86 }
87 }
88
89 QSizeF KItemListViewLayouter::size() const
90 {
91 return m_size;
92 }
93
94 void KItemListViewLayouter::setItemSize(const QSizeF& size)
95 {
96 if (m_itemSize != size) {
97 m_itemSize = size;
98 m_dirty = true;
99 }
100 }
101
102 QSizeF KItemListViewLayouter::itemSize() const
103 {
104 return m_itemSize;
105 }
106
107 void KItemListViewLayouter::setItemMargin(const QSizeF& margin)
108 {
109 if (m_itemMargin != margin) {
110 m_itemMargin = margin;
111 m_dirty = true;
112 }
113 }
114
115 QSizeF KItemListViewLayouter::itemMargin() const
116 {
117 return m_itemMargin;
118 }
119
120 void KItemListViewLayouter::setHeaderHeight(qreal height)
121 {
122 if (m_headerHeight != height) {
123 m_headerHeight = height;
124 m_dirty = true;
125 }
126 }
127
128 qreal KItemListViewLayouter::headerHeight() const
129 {
130 return m_headerHeight;
131 }
132
133 void KItemListViewLayouter::setGroupHeaderHeight(qreal height)
134 {
135 if (m_groupHeaderHeight != height) {
136 m_groupHeaderHeight = height;
137 m_dirty = true;
138 }
139 }
140
141 qreal KItemListViewLayouter::groupHeaderHeight() const
142 {
143 return m_groupHeaderHeight;
144 }
145
146 void KItemListViewLayouter::setGroupHeaderMargin(qreal margin)
147 {
148 if (m_groupHeaderMargin != margin) {
149 m_groupHeaderMargin = margin;
150 m_dirty = true;
151 }
152 }
153
154 qreal KItemListViewLayouter::groupHeaderMargin() const
155 {
156 return m_groupHeaderMargin;
157 }
158
159 void KItemListViewLayouter::setScrollOffset(qreal offset)
160 {
161 if (m_scrollOffset != offset) {
162 m_scrollOffset = offset;
163 m_visibleIndexesDirty = true;
164 }
165 }
166
167 qreal KItemListViewLayouter::scrollOffset() const
168 {
169 return m_scrollOffset;
170 }
171
172 qreal KItemListViewLayouter::maximumScrollOffset() const
173 {
174 const_cast<KItemListViewLayouter*>(this)->doLayout();
175 return m_maximumScrollOffset;
176 }
177
178 void KItemListViewLayouter::setItemOffset(qreal offset)
179 {
180 if (m_itemOffset != offset) {
181 m_itemOffset = offset;
182 m_visibleIndexesDirty = true;
183 }
184 }
185
186 qreal KItemListViewLayouter::itemOffset() const
187 {
188 return m_itemOffset;
189 }
190
191 qreal KItemListViewLayouter::maximumItemOffset() const
192 {
193 const_cast<KItemListViewLayouter*>(this)->doLayout();
194 return m_maximumItemOffset;
195 }
196
197 void KItemListViewLayouter::setModel(const KItemModelBase* model)
198 {
199 if (m_model != model) {
200 m_model = model;
201 m_dirty = true;
202 }
203 }
204
205 const KItemModelBase* KItemListViewLayouter::model() const
206 {
207 return m_model;
208 }
209
210 void KItemListViewLayouter::setSizeHintResolver(const KItemListSizeHintResolver* sizeHintResolver)
211 {
212 if (m_sizeHintResolver != sizeHintResolver) {
213 m_sizeHintResolver = sizeHintResolver;
214 m_dirty = true;
215 }
216 }
217
218 const KItemListSizeHintResolver* KItemListViewLayouter::sizeHintResolver() const
219 {
220 return m_sizeHintResolver;
221 }
222
223 int KItemListViewLayouter::firstVisibleIndex() const
224 {
225 const_cast<KItemListViewLayouter*>(this)->doLayout();
226 return m_firstVisibleIndex;
227 }
228
229 int KItemListViewLayouter::lastVisibleIndex() const
230 {
231 const_cast<KItemListViewLayouter*>(this)->doLayout();
232 return m_lastVisibleIndex;
233 }
234
235 QRectF KItemListViewLayouter::itemRect(int index) const
236 {
237 const_cast<KItemListViewLayouter*>(this)->doLayout();
238 if (index < 0 || index >= m_itemInfos.count()) {
239 return QRectF();
240 }
241
242 if (m_scrollOrientation == Qt::Horizontal) {
243 // Rotate the logical direction which is always vertical by 90°
244 // to get the physical horizontal direction
245 const QRectF& b = m_itemInfos[index].rect;
246 QRectF bounds(b.y(), b.x(), b.height(), b.width());
247 QPointF pos = bounds.topLeft();
248 pos.rx() -= m_scrollOffset;
249 bounds.moveTo(pos);
250 return bounds;
251 }
252
253 QRectF bounds = m_itemInfos[index].rect;
254 bounds.moveTo(bounds.topLeft() - QPointF(m_itemOffset, m_scrollOffset));
255 return bounds;
256 }
257
258 QRectF KItemListViewLayouter::groupHeaderRect(int index) const
259 {
260 const_cast<KItemListViewLayouter*>(this)->doLayout();
261
262 const QRectF firstItemRect = itemRect(index);
263 QPointF pos = firstItemRect.topLeft();
264 if (pos.isNull()) {
265 return QRectF();
266 }
267
268 QSizeF size;
269 if (m_scrollOrientation == Qt::Vertical) {
270 pos.rx() = 0;
271 pos.ry() -= m_groupHeaderHeight;
272 size = QSizeF(m_size.width(), m_groupHeaderHeight);
273 } else {
274 pos.rx() -= m_itemMargin.width();
275 pos.ry() = 0;
276
277 // Determine the maximum width used in the
278 // current column. As the scroll-direction is
279 // Qt::Horizontal and m_itemRects is accessed directly,
280 // the logical height represents the visual width.
281 qreal width = minimumGroupHeaderWidth();
282 const qreal y = m_itemInfos[index].rect.y();
283 const int maxIndex = m_itemInfos.count() - 1;
284 while (index <= maxIndex) {
285 QRectF bounds = m_itemInfos[index].rect;
286 if (bounds.y() != y) {
287 break;
288 }
289
290 if (bounds.height() > width) {
291 width = bounds.height();
292 }
293
294 ++index;
295 }
296
297 size = QSizeF(width, 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.setWidth(m_itemSize.height());
379 itemSize.setHeight(m_itemSize.width());
380 itemMargin.setWidth(m_itemMargin.height());
381 itemMargin.setHeight(m_itemMargin.width());
382 size.setWidth(m_size.height());
383 size.setHeight(m_size.width());
384
385 if (grouped) {
386 // In the horizontal scrolling case all groups are aligned
387 // at the top, which decreases the available height. For the
388 // flipped data this means that the width must be decreased.
389 size.rwidth() -= m_groupHeaderHeight;
390 }
391 }
392
393 m_columnWidth = itemSize.width() + itemMargin.width();
394 const qreal widthForColumns = size.width() - itemMargin.width();
395 m_columnCount = qMax(1, int(widthForColumns / m_columnWidth));
396 m_xPosInc = itemMargin.width();
397
398 const int itemCount = m_model->count();
399 if (itemCount > m_columnCount && m_columnWidth >= 32) {
400 // Apply the unused width equally to each column
401 const qreal unusedWidth = widthForColumns - m_columnCount * m_columnWidth;
402 if (unusedWidth > 0) {
403 const qreal columnInc = unusedWidth / (m_columnCount + 1);
404 m_columnWidth += columnInc;
405 m_xPosInc += columnInc;
406 }
407 }
408
409 int rowCount = itemCount / m_columnCount;
410 if (itemCount % m_columnCount != 0) {
411 ++rowCount;
412 }
413
414 m_itemInfos.reserve(itemCount);
415
416 qreal y = m_headerHeight + itemMargin.height();
417 int row = 0;
418
419 int index = 0;
420 while (index < itemCount) {
421 qreal x = m_xPosInc;
422 qreal maxItemHeight = itemSize.height();
423
424 if (grouped) {
425 if (horizontalScrolling) {
426 // All group headers will always be aligned on the top and not
427 // flipped like the other properties
428 x += m_groupHeaderHeight;
429 }
430
431 if (m_groupItemIndexes.contains(index)) {
432 // The item is the first item of a group.
433 // Increase the y-position to provide space
434 // for the group header.
435 if (index > 0) {
436 // Only add a margin if there has been added another
437 // group already before
438 y += m_groupHeaderMargin;
439 } else if (!horizontalScrolling) {
440 // The first group header should be aligned on top
441 y -= itemMargin.height();
442 }
443
444 if (!horizontalScrolling) {
445 y += m_groupHeaderHeight;
446 }
447 }
448 }
449
450 int column = 0;
451 while (index < itemCount && column < m_columnCount) {
452 qreal requiredItemHeight = itemSize.height();
453 if (m_sizeHintResolver) {
454 const QSizeF sizeHint = m_sizeHintResolver->sizeHint(index);
455 const qreal sizeHintHeight = horizontalScrolling ? sizeHint.width() : sizeHint.height();
456 if (sizeHintHeight > requiredItemHeight) {
457 requiredItemHeight = sizeHintHeight;
458 }
459 }
460
461 const QRectF bounds(x, y, itemSize.width(), requiredItemHeight);
462 if (index < m_itemInfos.count()) {
463 m_itemInfos[index].rect = bounds;
464 m_itemInfos[index].column = column;
465 m_itemInfos[index].row = row;
466 } else {
467 ItemInfo itemInfo;
468 itemInfo.rect = bounds;
469 itemInfo.column = column;
470 itemInfo.row = row;
471 m_itemInfos.append(itemInfo);
472 }
473
474 if (grouped && horizontalScrolling) {
475 // When grouping is enabled in the horizontal mode, the header alignment
476 // looks like this:
477 // Header-1 Header-2 Header-3
478 // Item 1 Item 4 Item 7
479 // Item 2 Item 5 Item 8
480 // Item 3 Item 6 Item 9
481 // In this case 'requiredItemHeight' represents the column-width. We don't
482 // check the content of the header in the layouter to determine the required
483 // width, hence assure that at least a minimal width of 15 characters is given
484 // (in average a character requires the halve width of the font height).
485 //
486 // TODO: Let the group headers provide a minimum width and respect this width here
487 const qreal headerWidth = minimumGroupHeaderWidth();
488 if (requiredItemHeight < headerWidth) {
489 requiredItemHeight = headerWidth;
490 }
491 }
492
493 maxItemHeight = qMax(maxItemHeight, requiredItemHeight);
494 x += m_columnWidth;
495 ++index;
496 ++column;
497
498 if (grouped && m_groupItemIndexes.contains(index)) {
499 // The item represents the first index of a group
500 // and must aligned in the first column
501 break;
502 }
503 }
504
505 y += maxItemHeight + itemMargin.height();
506 ++row;
507 }
508 if (m_itemInfos.count() > itemCount) {
509 m_itemInfos.erase(m_itemInfos.begin() + itemCount,
510 m_itemInfos.end());
511 }
512
513 if (itemCount > 0) {
514 // Calculate the maximum y-range of the last row for m_maximumScrollOffset
515 m_maximumScrollOffset = m_itemInfos.last().rect.bottom();
516 const qreal rowY = m_itemInfos.last().rect.y();
517
518 int index = m_itemInfos.count() - 2;
519 while (index >= 0 && m_itemInfos[index].rect.bottom() >= rowY) {
520 m_maximumScrollOffset = qMax(m_maximumScrollOffset, m_itemInfos[index].rect.bottom());
521 --index;
522 }
523
524 m_maximumScrollOffset += itemMargin.height();
525
526 m_maximumItemOffset = m_columnCount * m_columnWidth;
527 } else {
528 m_maximumScrollOffset = 0;
529 m_maximumItemOffset = 0;
530 }
531
532 #ifdef KITEMLISTVIEWLAYOUTER_DEBUG
533 kDebug() << "[TIME] doLayout() for " << m_model->count() << "items:" << timer.elapsed();
534 #endif
535 m_dirty = false;
536 }
537
538 updateVisibleIndexes();
539 }
540
541 void KItemListViewLayouter::updateVisibleIndexes()
542 {
543 if (!m_visibleIndexesDirty) {
544 return;
545 }
546
547 Q_ASSERT(!m_dirty);
548
549 if (m_model->count() <= 0) {
550 m_firstVisibleIndex = -1;
551 m_lastVisibleIndex = -1;
552 m_visibleIndexesDirty = false;
553 return;
554 }
555
556 const int maxIndex = m_model->count() - 1;
557
558 // Calculate the first visible index that is fully visible
559 int min = 0;
560 int max = maxIndex;
561 int mid = 0;
562 do {
563 mid = (min + max) / 2;
564 if (m_itemInfos[mid].rect.top() < m_scrollOffset) {
565 min = mid + 1;
566 } else {
567 max = mid - 1;
568 }
569 } while (min <= max);
570
571 if (mid > 0) {
572 // Include the row before the first fully visible index, as it might
573 // be partly visible
574 if (m_itemInfos[mid].rect.top() >= m_scrollOffset) {
575 --mid;
576 Q_ASSERT(m_itemInfos[mid].rect.top() < m_scrollOffset);
577 }
578
579 const qreal rowTop = m_itemInfos[mid].rect.top();
580 while (mid > 0 && m_itemInfos[mid - 1].rect.top() == rowTop) {
581 --mid;
582 }
583 }
584 m_firstVisibleIndex = mid;
585
586 // Calculate the last visible index that is (at least partly) visible
587 const int visibleHeight = (m_scrollOrientation == Qt::Horizontal) ? m_size.width() : m_size.height();
588 qreal bottom = m_scrollOffset + visibleHeight;
589 if (m_model->groupedSorting()) {
590 bottom += m_groupHeaderHeight;
591 }
592
593 min = m_firstVisibleIndex;
594 max = maxIndex;
595 do {
596 mid = (min + max) / 2;
597 if (m_itemInfos[mid].rect.y() <= bottom) {
598 min = mid + 1;
599 } else {
600 max = mid - 1;
601 }
602 } while (min <= max);
603
604 while (mid > 0 && m_itemInfos[mid].rect.y() > bottom) {
605 --mid;
606 }
607 m_lastVisibleIndex = mid;
608
609 m_visibleIndexesDirty = false;
610 }
611
612 bool KItemListViewLayouter::createGroupHeaders()
613 {
614 if (!m_model->groupedSorting()) {
615 return false;
616 }
617
618 m_groupItemIndexes.clear();
619
620 const QList<QPair<int, QVariant> > groups = m_model->groups();
621 if (groups.isEmpty()) {
622 return false;
623 }
624
625 for (int i = 0; i < groups.count(); ++i) {
626 const int firstItemIndex = groups.at(i).first;
627 m_groupItemIndexes.insert(firstItemIndex);
628 }
629
630 return true;
631 }
632
633 qreal KItemListViewLayouter::minimumGroupHeaderWidth() const
634 {
635 return 100;
636 }
637
638 #include "kitemlistviewlayouter.moc"