]> cloud.milkyroute.net Git - dolphin.git/blob - src/kitemviews/kitemlistviewlayouter.cpp
Implement group-header layouting
[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 namespace {
30 // TODO: Calculate dynamically
31 const int GroupHeaderHeight = 50;
32 };
33
34 KItemListViewLayouter::KItemListViewLayouter(QObject* parent) :
35 QObject(parent),
36 m_dirty(true),
37 m_visibleIndexesDirty(true),
38 m_scrollOrientation(Qt::Vertical),
39 m_size(),
40 m_itemSize(128, 128),
41 m_headerHeight(0),
42 m_model(0),
43 m_sizeHintResolver(0),
44 m_scrollOffset(0),
45 m_maximumScrollOffset(0),
46 m_itemOffset(0),
47 m_maximumItemOffset(0),
48 m_firstVisibleIndex(-1),
49 m_lastVisibleIndex(-1),
50 m_columnWidth(0),
51 m_xPosInc(0),
52 m_columnCount(0),
53 m_groups(),
54 m_groupIndexes(),
55 m_itemBoundingRects()
56 {
57 }
58
59 KItemListViewLayouter::~KItemListViewLayouter()
60 {
61 }
62
63 void KItemListViewLayouter::setScrollOrientation(Qt::Orientation orientation)
64 {
65 if (m_scrollOrientation != orientation) {
66 m_scrollOrientation = orientation;
67 m_dirty = true;
68 }
69 }
70
71 Qt::Orientation KItemListViewLayouter::scrollOrientation() const
72 {
73 return m_scrollOrientation;
74 }
75
76 void KItemListViewLayouter::setSize(const QSizeF& size)
77 {
78 if (m_size != size) {
79 m_size = size;
80 m_dirty = true;
81 }
82 }
83
84 QSizeF KItemListViewLayouter::size() const
85 {
86 return m_size;
87 }
88
89 void KItemListViewLayouter::setItemSize(const QSizeF& size)
90 {
91 if (m_itemSize != size) {
92 m_itemSize = size;
93 m_dirty = true;
94 }
95 }
96
97 QSizeF KItemListViewLayouter::itemSize() const
98 {
99 return m_itemSize;
100 }
101
102 void KItemListViewLayouter::setHeaderHeight(qreal height)
103 {
104 if (m_headerHeight != height) {
105 m_headerHeight = height;
106 m_dirty = true;
107 }
108 }
109
110 qreal KItemListViewLayouter::headerHeight() const
111 {
112 return m_headerHeight;
113 }
114
115 void KItemListViewLayouter::setScrollOffset(qreal offset)
116 {
117 if (m_scrollOffset != offset) {
118 m_scrollOffset = offset;
119 m_visibleIndexesDirty = true;
120 }
121 }
122
123 qreal KItemListViewLayouter::scrollOffset() const
124 {
125 return m_scrollOffset;
126 }
127
128 qreal KItemListViewLayouter::maximumScrollOffset() const
129 {
130 const_cast<KItemListViewLayouter*>(this)->doLayout();
131 return m_maximumScrollOffset;
132 }
133
134 void KItemListViewLayouter::setItemOffset(qreal offset)
135 {
136 if (m_itemOffset != offset) {
137 m_itemOffset = offset;
138 m_visibleIndexesDirty = true;
139 }
140 }
141
142 qreal KItemListViewLayouter::itemOffset() const
143 {
144 return m_itemOffset;
145 }
146
147 qreal KItemListViewLayouter::maximumItemOffset() const
148 {
149 const_cast<KItemListViewLayouter*>(this)->doLayout();
150 return m_maximumItemOffset;
151 }
152
153 void KItemListViewLayouter::setModel(const KItemModelBase* model)
154 {
155 if (m_model != model) {
156 m_model = model;
157 m_dirty = true;
158 }
159 }
160
161 const KItemModelBase* KItemListViewLayouter::model() const
162 {
163 return m_model;
164 }
165
166 void KItemListViewLayouter::setSizeHintResolver(const KItemListSizeHintResolver* sizeHintResolver)
167 {
168 if (m_sizeHintResolver != sizeHintResolver) {
169 m_sizeHintResolver = sizeHintResolver;
170 m_dirty = true;
171 }
172 }
173
174 const KItemListSizeHintResolver* KItemListViewLayouter::sizeHintResolver() const
175 {
176 return m_sizeHintResolver;
177 }
178
179 int KItemListViewLayouter::firstVisibleIndex() const
180 {
181 const_cast<KItemListViewLayouter*>(this)->doLayout();
182 return m_firstVisibleIndex;
183 }
184
185 int KItemListViewLayouter::lastVisibleIndex() const
186 {
187 const_cast<KItemListViewLayouter*>(this)->doLayout();
188 return m_lastVisibleIndex;
189 }
190
191 QRectF KItemListViewLayouter::itemBoundingRect(int index) const
192 {
193 const_cast<KItemListViewLayouter*>(this)->doLayout();
194 if (index < 0 || index >= m_itemBoundingRects.count()) {
195 return QRectF();
196 }
197
198 if (m_scrollOrientation == Qt::Horizontal) {
199 // Rotate the logical direction which is always vertical by 90°
200 // to get the physical horizontal direction
201 const QRectF& b = m_itemBoundingRects[index];
202 QRectF bounds(b.y(), b.x(), b.height(), b.width());
203 QPointF pos = bounds.topLeft();
204 pos.rx() -= m_scrollOffset;
205 bounds.moveTo(pos);
206 return bounds;
207 }
208
209 QRectF bounds = m_itemBoundingRects[index];
210 bounds.moveTo(bounds.topLeft() - QPointF(m_itemOffset, m_scrollOffset));
211 return bounds;
212 }
213
214 int KItemListViewLayouter::maximumVisibleItems() const
215 {
216 const_cast<KItemListViewLayouter*>(this)->doLayout();
217
218 const int height = static_cast<int>(m_size.height());
219 const int rowHeight = static_cast<int>(m_itemSize.height());
220 int rows = height / rowHeight;
221 if (height % rowHeight != 0) {
222 ++rows;
223 }
224
225 return rows * m_columnCount;
226 }
227
228 int KItemListViewLayouter::itemsPerOffset() const
229 {
230 return m_columnCount;
231 }
232
233 bool KItemListViewLayouter::isFirstGroupItem(int itemIndex) const
234 {
235 return m_groupIndexes.contains(itemIndex);
236 }
237
238 void KItemListViewLayouter::markAsDirty()
239 {
240 m_dirty = true;
241 }
242
243 void KItemListViewLayouter::doLayout()
244 {
245 if (m_dirty) {
246 #ifdef KITEMLISTVIEWLAYOUTER_DEBUG
247 QElapsedTimer timer;
248 timer.start();
249 #endif
250 m_visibleIndexesDirty = true;
251
252 QSizeF itemSize = m_itemSize;
253 QSizeF size = m_size;
254
255 const bool horizontalScrolling = (m_scrollOrientation == Qt::Horizontal);
256 if (horizontalScrolling) {
257 itemSize.setWidth(m_itemSize.height());
258 itemSize.setHeight(m_itemSize.width());
259 size.setWidth(m_size.height());
260 size.setHeight(m_size.width());
261 }
262
263 m_columnWidth = itemSize.width();
264 m_columnCount = qMax(1, int(size.width() / m_columnWidth));
265 m_xPosInc = 0;
266
267 const int itemCount = m_model->count();
268 if (itemCount > m_columnCount) {
269 // Apply the unused width equally to each column
270 const qreal unusedWidth = size.width() - m_columnCount * m_columnWidth;
271 if (unusedWidth > 0) {
272 const qreal columnInc = unusedWidth / (m_columnCount + 1);
273 m_columnWidth += columnInc;
274 m_xPosInc += columnInc;
275 }
276 }
277
278 int rowCount = itemCount / m_columnCount;
279 if (itemCount % m_columnCount != 0) {
280 ++rowCount;
281 }
282
283 m_itemBoundingRects.reserve(itemCount);
284
285 qreal y = m_headerHeight;
286 int rowIndex = 0;
287
288 const bool grouped = createGroupHeaders();
289 int groupIndex = 0;
290 int firstItemIndexOfGroup = 0;
291
292 int index = 0;
293 while (index < itemCount) {
294 qreal x = m_xPosInc;
295 qreal maxItemHeight = itemSize.height();
296
297 if (grouped) {
298 if (horizontalScrolling) {
299 // All group headers will always be aligned on the top and not
300 // flipped like the other properties
301 x += GroupHeaderHeight;
302 }
303
304 if (index == firstItemIndexOfGroup) {
305 if (!horizontalScrolling) {
306 // The item is the first item of a group.
307 // Increase the y-position to provide space
308 // for the group header.
309 y += GroupHeaderHeight;
310 }
311
312 // Calculate the index of the first item for
313 // the next group
314 ++groupIndex;
315 if (groupIndex < m_groups.count()) {
316 firstItemIndexOfGroup = m_groups.at(groupIndex);
317 } else {
318 firstItemIndexOfGroup = -1;
319 }
320 }
321 }
322
323 int column = 0;
324 while (index < itemCount && column < m_columnCount) {
325 qreal requiredItemHeight = itemSize.height();
326 if (m_sizeHintResolver) {
327 const QSizeF sizeHint = m_sizeHintResolver->sizeHint(index);
328 const qreal sizeHintHeight = horizontalScrolling ? sizeHint.width() : sizeHint.height();
329 if (sizeHintHeight > requiredItemHeight) {
330 requiredItemHeight = sizeHintHeight;
331 }
332 }
333
334 const QRectF bounds(x, y, itemSize.width(), requiredItemHeight);
335 if (index < m_itemBoundingRects.count()) {
336 m_itemBoundingRects[index] = bounds;
337 } else {
338 m_itemBoundingRects.append(bounds);
339 }
340
341 maxItemHeight = qMax(maxItemHeight, requiredItemHeight);
342 x += m_columnWidth;
343 ++index;
344 ++column;
345
346 if (grouped && index == firstItemIndexOfGroup) {
347 // The item represents the first index of a group
348 // and must aligned in the first column
349 break;
350 }
351 }
352
353 y += maxItemHeight;
354 ++rowIndex;
355 }
356 if (m_itemBoundingRects.count() > itemCount) {
357 m_itemBoundingRects.erase(m_itemBoundingRects.begin() + itemCount,
358 m_itemBoundingRects.end());
359 }
360
361 if (itemCount > 0) {
362 m_maximumScrollOffset = m_itemBoundingRects.last().bottom();
363 m_maximumItemOffset = m_columnCount * m_columnWidth;
364 } else {
365 m_maximumScrollOffset = 0;
366 m_maximumItemOffset = 0;
367 }
368
369 #ifdef KITEMLISTVIEWLAYOUTER_DEBUG
370 kDebug() << "[TIME] doLayout() for " << m_model->count() << "items:" << timer.elapsed();
371 #endif
372 m_dirty = false;
373 }
374
375 updateVisibleIndexes();
376 }
377
378 void KItemListViewLayouter::updateVisibleIndexes()
379 {
380 if (!m_visibleIndexesDirty) {
381 return;
382 }
383
384 Q_ASSERT(!m_dirty);
385
386 if (m_model->count() <= 0) {
387 m_firstVisibleIndex = -1;
388 m_lastVisibleIndex = -1;
389 m_visibleIndexesDirty = false;
390 return;
391 }
392
393 const int maxIndex = m_model->count() - 1;
394
395 // Calculate the first visible index that is (at least partly) visible
396 int min = 0;
397 int max = maxIndex;
398 int mid = 0;
399 do {
400 mid = (min + max) / 2;
401 if (m_itemBoundingRects[mid].bottom() < m_scrollOffset) {
402 min = mid + 1;
403 } else {
404 max = mid - 1;
405 }
406 } while (min <= max);
407
408 while (mid < maxIndex && m_itemBoundingRects[mid].bottom() < m_scrollOffset) {
409 ++mid;
410 }
411 m_firstVisibleIndex = mid;
412
413 // Calculate the last visible index that is (at least partly) visible
414 const int visibleHeight = (m_scrollOrientation == Qt::Horizontal) ? m_size.width() : m_size.height();
415 qreal bottom = m_scrollOffset + visibleHeight;
416 if (m_model->groupedSorting()) {
417 bottom += GroupHeaderHeight;
418 }
419
420 min = m_firstVisibleIndex;
421 max = maxIndex;
422 do {
423 mid = (min + max) / 2;
424 if (m_itemBoundingRects[mid].y() <= bottom) {
425 min = mid + 1;
426 } else {
427 max = mid - 1;
428 }
429 } while (min <= max);
430
431 while (mid > 0 && m_itemBoundingRects[mid].y() > bottom) {
432 --mid;
433 }
434 m_lastVisibleIndex = mid;
435
436 m_visibleIndexesDirty = false;
437 }
438
439 bool KItemListViewLayouter::createGroupHeaders()
440 {
441 if (!m_model->groupedSorting()) {
442 return false;
443 }
444
445 m_groups.clear();
446 m_groupIndexes.clear();
447
448 const QList<QPair<int, QVariant> > groups = m_model->groups();
449 if (groups.isEmpty()) {
450 return false;
451 }
452
453 m_groups.reserve(groups.count());
454 for (int i = 0; i < groups.count(); ++i) {
455 const int firstItemIndex = groups.at(i).first;
456 m_groups.append(firstItemIndex);
457 m_groupIndexes.insert(firstItemIndex);
458 }
459
460 return true;
461 }
462
463 #include "kitemlistviewlayouter_p.moc"