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