]> cloud.milkyroute.net Git - dolphin.git/blob - src/kitemviews/kitemlistviewlayouter.cpp
Icon-rectangle and selection-toggle optimizations
[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_itemRects()
52 {
53 }
54
55 KItemListViewLayouter::~KItemListViewLayouter()
56 {
57 }
58
59 void KItemListViewLayouter::setScrollOrientation(Qt::Orientation orientation)
60 {
61 if (m_scrollOrientation != orientation) {
62 m_scrollOrientation = orientation;
63 m_dirty = true;
64 }
65 }
66
67 Qt::Orientation KItemListViewLayouter::scrollOrientation() const
68 {
69 return m_scrollOrientation;
70 }
71
72 void KItemListViewLayouter::setSize(const QSizeF& size)
73 {
74 if (m_size != size) {
75 m_size = size;
76 m_dirty = 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::setScrollOffset(qreal offset)
138 {
139 if (m_scrollOffset != offset) {
140 m_scrollOffset = offset;
141 m_visibleIndexesDirty = true;
142 }
143 }
144
145 qreal KItemListViewLayouter::scrollOffset() const
146 {
147 return m_scrollOffset;
148 }
149
150 qreal KItemListViewLayouter::maximumScrollOffset() const
151 {
152 const_cast<KItemListViewLayouter*>(this)->doLayout();
153 return m_maximumScrollOffset;
154 }
155
156 void KItemListViewLayouter::setItemOffset(qreal offset)
157 {
158 if (m_itemOffset != offset) {
159 m_itemOffset = offset;
160 m_visibleIndexesDirty = true;
161 }
162 }
163
164 qreal KItemListViewLayouter::itemOffset() const
165 {
166 return m_itemOffset;
167 }
168
169 qreal KItemListViewLayouter::maximumItemOffset() const
170 {
171 const_cast<KItemListViewLayouter*>(this)->doLayout();
172 return m_maximumItemOffset;
173 }
174
175 void KItemListViewLayouter::setModel(const KItemModelBase* model)
176 {
177 if (m_model != model) {
178 m_model = model;
179 m_dirty = true;
180 }
181 }
182
183 const KItemModelBase* KItemListViewLayouter::model() const
184 {
185 return m_model;
186 }
187
188 void KItemListViewLayouter::setSizeHintResolver(const KItemListSizeHintResolver* sizeHintResolver)
189 {
190 if (m_sizeHintResolver != sizeHintResolver) {
191 m_sizeHintResolver = sizeHintResolver;
192 m_dirty = true;
193 }
194 }
195
196 const KItemListSizeHintResolver* KItemListViewLayouter::sizeHintResolver() const
197 {
198 return m_sizeHintResolver;
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_itemRects.count()) {
217 return QRectF();
218 }
219
220 if (m_scrollOrientation == Qt::Horizontal) {
221 // Rotate the logical direction which is always vertical by 90°
222 // to get the physical horizontal direction
223 const QRectF& b = m_itemRects[index];
224 QRectF bounds(b.y(), b.x(), b.height(), b.width());
225 QPointF pos = bounds.topLeft();
226 pos.rx() -= m_scrollOffset;
227 bounds.moveTo(pos);
228 return bounds;
229 }
230
231 QRectF bounds = m_itemRects[index];
232 bounds.moveTo(bounds.topLeft() - QPointF(m_itemOffset, m_scrollOffset));
233 return bounds;
234 }
235
236 QRectF KItemListViewLayouter::groupHeaderRect(int index) const
237 {
238 const_cast<KItemListViewLayouter*>(this)->doLayout();
239
240 const QRectF firstItemRect = itemRect(index);
241 QPointF pos = firstItemRect.topLeft();
242 if (pos.isNull()) {
243 return QRectF();
244 }
245
246 pos.ry() -= m_groupHeaderHeight;
247
248 QSizeF size;
249 if (m_scrollOrientation == Qt::Vertical) {
250 pos.rx() = 0;
251 size = QSizeF(m_size.width(), m_groupHeaderHeight);
252 } else {
253 size = QSizeF(minimumGroupHeaderWidth(), m_groupHeaderHeight);
254 }
255 return QRectF(pos, size);
256 }
257
258 int KItemListViewLayouter::maximumVisibleItems() const
259 {
260 const_cast<KItemListViewLayouter*>(this)->doLayout();
261
262 const int height = static_cast<int>(m_size.height());
263 const int rowHeight = static_cast<int>(m_itemSize.height());
264 int rows = height / rowHeight;
265 if (height % rowHeight != 0) {
266 ++rows;
267 }
268
269 return rows * m_columnCount;
270 }
271
272 bool KItemListViewLayouter::isFirstGroupItem(int itemIndex) const
273 {
274 const_cast<KItemListViewLayouter*>(this)->doLayout();
275 return m_groupItemIndexes.contains(itemIndex);
276 }
277
278 void KItemListViewLayouter::markAsDirty()
279 {
280 m_dirty = true;
281 }
282
283 void KItemListViewLayouter::doLayout()
284 {
285 if (m_dirty) {
286 #ifdef KITEMLISTVIEWLAYOUTER_DEBUG
287 QElapsedTimer timer;
288 timer.start();
289 #endif
290 m_visibleIndexesDirty = true;
291
292 QSizeF itemSize = m_itemSize;
293 QSizeF itemMargin = m_itemMargin;
294 QSizeF size = m_size;
295
296 const bool grouped = createGroupHeaders();
297
298 const bool horizontalScrolling = (m_scrollOrientation == Qt::Horizontal);
299 if (horizontalScrolling) {
300 // Flip everything so that the layout logically can work like having
301 // a vertical scrolling
302 itemSize.setWidth(m_itemSize.height());
303 itemSize.setHeight(m_itemSize.width());
304 itemMargin.setWidth(m_itemMargin.height());
305 itemMargin.setHeight(m_itemMargin.width());
306 size.setWidth(m_size.height());
307 size.setHeight(m_size.width());
308
309 if (grouped) {
310 // In the horizontal scrolling case all groups are aligned
311 // at the top, which decreases the available height. For the
312 // flipped data this means that the width must be decreased.
313 size.rwidth() -= m_groupHeaderHeight;
314 }
315 }
316
317 m_columnWidth = itemSize.width() + itemMargin.width();
318 const qreal widthForColumns = size.width() - itemMargin.width();
319 m_columnCount = qMax(1, int(widthForColumns / m_columnWidth));
320 m_xPosInc = itemMargin.width();
321
322 const int itemCount = m_model->count();
323 if (itemCount > m_columnCount && m_columnWidth >= 32) {
324 // Apply the unused width equally to each column
325 const qreal unusedWidth = size.width() - m_columnCount * m_columnWidth;
326 if (unusedWidth > 0) {
327 const qreal columnInc = unusedWidth / (m_columnCount + 1);
328 m_columnWidth += columnInc;
329 m_xPosInc += columnInc;
330 }
331 }
332
333 int rowCount = itemCount / m_columnCount;
334 if (itemCount % m_columnCount != 0) {
335 ++rowCount;
336 }
337
338 m_itemRects.reserve(itemCount);
339
340 qreal y = m_headerHeight + itemMargin.height();
341 int rowIndex = 0;
342
343 int index = 0;
344 while (index < itemCount) {
345 qreal x = m_xPosInc;
346 qreal maxItemHeight = itemSize.height();
347
348 if (grouped) {
349 if (horizontalScrolling) {
350 // All group headers will always be aligned on the top and not
351 // flipped like the other properties
352 x += m_groupHeaderHeight;
353 }
354
355 if (m_groupItemIndexes.contains(index)) {
356 if (!horizontalScrolling) {
357 // The item is the first item of a group.
358 // Increase the y-position to provide space
359 // for the group header.
360 y += m_groupHeaderHeight;
361 }
362 }
363 }
364
365 int column = 0;
366 while (index < itemCount && column < m_columnCount) {
367 qreal requiredItemHeight = itemSize.height();
368 if (m_sizeHintResolver) {
369 const QSizeF sizeHint = m_sizeHintResolver->sizeHint(index);
370 const qreal sizeHintHeight = horizontalScrolling ? sizeHint.width() : sizeHint.height();
371 if (sizeHintHeight > requiredItemHeight) {
372 requiredItemHeight = sizeHintHeight;
373 }
374 }
375
376 const QRectF bounds(x, y, itemSize.width(), requiredItemHeight);
377 if (index < m_itemRects.count()) {
378 m_itemRects[index] = bounds;
379 } else {
380 m_itemRects.append(bounds);
381 }
382
383 if (grouped && horizontalScrolling) {
384 // When grouping is enabled in the horizontal mode, the header alignment
385 // looks like this:
386 // Header-1 Header-2 Header-3
387 // Item 1 Item 4 Item 7
388 // Item 2 Item 5 Item 8
389 // Item 3 Item 6 Item 9
390 // In this case 'requiredItemHeight' represents the column-width. We don't
391 // check the content of the header in the layouter to determine the required
392 // width, hence assure that at least a minimal width of 15 characters is given
393 // (in average a character requires the halve width of the font height).
394 //
395 // TODO: Let the group headers provide a minimum width and respect this width here
396 const qreal headerWidth = minimumGroupHeaderWidth();
397 if (requiredItemHeight < headerWidth) {
398 requiredItemHeight = headerWidth;
399 }
400 }
401
402 maxItemHeight = qMax(maxItemHeight, requiredItemHeight);
403 x += m_columnWidth;
404 ++index;
405 ++column;
406
407 if (grouped && m_groupItemIndexes.contains(index)) {
408 // The item represents the first index of a group
409 // and must aligned in the first column
410 break;
411 }
412 }
413
414 y += maxItemHeight + itemMargin.height();
415 ++rowIndex;
416 }
417 if (m_itemRects.count() > itemCount) {
418 m_itemRects.erase(m_itemRects.begin() + itemCount,
419 m_itemRects.end());
420 }
421
422 if (itemCount > 0) {
423 // Calculate the maximum y-range of the last row for m_maximumScrollOffset
424 m_maximumScrollOffset = m_itemRects.last().bottom();
425 const qreal rowY = m_itemRects.last().y();
426
427 int index = m_itemRects.count() - 2;
428 while (index >= 0 && m_itemRects.at(index).bottom() >= rowY) {
429 m_maximumScrollOffset = qMax(m_maximumScrollOffset, m_itemRects.at(index).bottom());
430 --index;
431 }
432
433 m_maximumItemOffset = m_columnCount * m_columnWidth;
434 } else {
435 m_maximumScrollOffset = 0;
436 m_maximumItemOffset = 0;
437 }
438
439 #ifdef KITEMLISTVIEWLAYOUTER_DEBUG
440 kDebug() << "[TIME] doLayout() for " << m_model->count() << "items:" << timer.elapsed();
441 #endif
442 m_dirty = false;
443 }
444
445 updateVisibleIndexes();
446 }
447
448 void KItemListViewLayouter::updateVisibleIndexes()
449 {
450 if (!m_visibleIndexesDirty) {
451 return;
452 }
453
454 Q_ASSERT(!m_dirty);
455
456 if (m_model->count() <= 0) {
457 m_firstVisibleIndex = -1;
458 m_lastVisibleIndex = -1;
459 m_visibleIndexesDirty = false;
460 return;
461 }
462
463 const int maxIndex = m_model->count() - 1;
464
465 // Calculate the first visible index that is fully visible
466 int min = 0;
467 int max = maxIndex;
468 int mid = 0;
469 do {
470 mid = (min + max) / 2;
471 if (m_itemRects[mid].top() < m_scrollOffset) {
472 min = mid + 1;
473 } else {
474 max = mid - 1;
475 }
476 } while (min <= max);
477
478 if (mid > 0) {
479 // Include the row before the first fully visible index, as it might
480 // be partly visible
481 if (m_itemRects[mid].top() >= m_scrollOffset) {
482 --mid;
483 Q_ASSERT(m_itemRects[mid].top() < m_scrollOffset);
484 }
485
486 const qreal rowTop = m_itemRects[mid].top();
487 while (mid > 0 && m_itemRects[mid - 1].top() == rowTop) {
488 --mid;
489 }
490 }
491 m_firstVisibleIndex = mid;
492
493 // Calculate the last visible index that is (at least partly) visible
494 const int visibleHeight = (m_scrollOrientation == Qt::Horizontal) ? m_size.width() : m_size.height();
495 qreal bottom = m_scrollOffset + visibleHeight;
496 if (m_model->groupedSorting()) {
497 bottom += m_groupHeaderHeight;
498 }
499
500 min = m_firstVisibleIndex;
501 max = maxIndex;
502 do {
503 mid = (min + max) / 2;
504 if (m_itemRects[mid].y() <= bottom) {
505 min = mid + 1;
506 } else {
507 max = mid - 1;
508 }
509 } while (min <= max);
510
511 while (mid > 0 && m_itemRects[mid].y() > bottom) {
512 --mid;
513 }
514 m_lastVisibleIndex = mid;
515
516 m_visibleIndexesDirty = false;
517 }
518
519 bool KItemListViewLayouter::createGroupHeaders()
520 {
521 if (!m_model->groupedSorting()) {
522 return false;
523 }
524
525 m_groupItemIndexes.clear();
526
527 const QList<QPair<int, QVariant> > groups = m_model->groups();
528 if (groups.isEmpty()) {
529 return false;
530 }
531
532 for (int i = 0; i < groups.count(); ++i) {
533 const int firstItemIndex = groups.at(i).first;
534 m_groupItemIndexes.insert(firstItemIndex);
535 }
536
537 return true;
538 }
539
540 qreal KItemListViewLayouter::minimumGroupHeaderWidth() const
541 {
542 return m_groupHeaderHeight * 15 / 2;
543 }
544
545 #include "kitemlistviewlayouter_p.moc"