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