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