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