]> cloud.milkyroute.net Git - dolphin.git/blob - src/kitemviews/kfileitemlistview.cpp
Folders Panel fixes
[dolphin.git] / src / kitemviews / kfileitemlistview.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 "kfileitemlistview.h"
21
22 #include "kfileitemlistgroupheader.h"
23 #include "kfileitemmodelrolesupdater.h"
24 #include "kfileitemlistwidget.h"
25 #include "kfileitemmodel.h"
26 #include "kpixmapmodifier_p.h"
27 #include <KLocale>
28 #include <KStringHandler>
29
30 #include <KDebug>
31 #include <KIcon>
32
33 #include <QPainter>
34 #include <QTextLine>
35 #include <QTimer>
36
37 #define KFILEITEMLISTVIEW_DEBUG
38
39 namespace {
40 const int ShortInterval = 50;
41 const int LongInterval = 300;
42 }
43
44 KFileItemListView::KFileItemListView(QGraphicsWidget* parent) :
45 KItemListView(parent),
46 m_itemLayout(IconsLayout),
47 m_modelRolesUpdater(0),
48 m_updateVisibleIndexRangeTimer(0),
49 m_updateIconSizeTimer(0),
50 m_minimumRolesWidths()
51 {
52 setAcceptDrops(true);
53
54 setScrollOrientation(Qt::Vertical);
55 setWidgetCreator(new KItemListWidgetCreator<KFileItemListWidget>());
56 setGroupHeaderCreator(new KItemListGroupHeaderCreator<KFileItemListGroupHeader>());
57
58 m_updateVisibleIndexRangeTimer = new QTimer(this);
59 m_updateVisibleIndexRangeTimer->setSingleShot(true);
60 m_updateVisibleIndexRangeTimer->setInterval(ShortInterval);
61 connect(m_updateVisibleIndexRangeTimer, SIGNAL(timeout()), this, SLOT(updateVisibleIndexRange()));
62
63 m_updateIconSizeTimer = new QTimer(this);
64 m_updateIconSizeTimer->setSingleShot(true);
65 m_updateIconSizeTimer->setInterval(ShortInterval);
66 connect(m_updateIconSizeTimer, SIGNAL(timeout()), this, SLOT(updateIconSize()));
67
68 setVisibleRoles(QList<QByteArray>() << "name");
69
70 updateMinimumRolesWidths();
71 }
72
73 KFileItemListView::~KFileItemListView()
74 {
75 // The group headers are children of the widgets created by
76 // widgetCreator(). So it is mandatory to delete the group headers
77 // first.
78 delete groupHeaderCreator();
79 delete widgetCreator();
80
81 delete m_modelRolesUpdater;
82 m_modelRolesUpdater = 0;
83 }
84
85 void KFileItemListView::setPreviewsShown(bool show)
86 {
87 if (m_modelRolesUpdater) {
88 m_modelRolesUpdater->setPreviewShown(show);
89 }
90 }
91
92 bool KFileItemListView::previewsShown() const
93 {
94 return m_modelRolesUpdater->isPreviewShown();
95 }
96
97 void KFileItemListView::setItemLayout(Layout layout)
98 {
99 if (m_itemLayout != layout) {
100 m_itemLayout = layout;
101 updateLayoutOfVisibleItems();
102 }
103 }
104
105 KFileItemListView::Layout KFileItemListView::itemLayout() const
106 {
107 return m_itemLayout;
108 }
109
110 QSizeF KFileItemListView::itemSizeHint(int index) const
111 {
112 const QHash<QByteArray, QVariant> values = model()->data(index);
113 const KItemListStyleOption& option = styleOption();
114 const int additionalRolesCount = qMax(visibleRoles().count() - 1, 0);
115
116 switch (m_itemLayout) {
117 case IconsLayout: {
118 const QString text = KStringHandler::preProcessWrap(values["name"].toString());
119
120 const qreal maxWidth = itemSize().width() - 2 * option.margin;
121 int textLinesCount = 0;
122 QTextLine line;
123
124 // Calculate the number of lines required for wrapping the name
125 QTextOption textOption(Qt::AlignHCenter);
126 textOption.setWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere);
127
128 QTextLayout layout(text, option.font);
129 layout.setTextOption(textOption);
130 layout.beginLayout();
131 while ((line = layout.createLine()).isValid()) {
132 line.setLineWidth(maxWidth);
133 line.naturalTextWidth();
134 ++textLinesCount;
135 }
136 layout.endLayout();
137
138 // Add one line for each additional information
139 textLinesCount += additionalRolesCount;
140
141 const qreal height = textLinesCount * option.fontMetrics.height() +
142 option.iconSize +
143 option.margin * 4;
144 return QSizeF(itemSize().width(), height);
145 }
146
147 case CompactLayout: {
148 // For each row exactly one role is shown. Calculate the maximum required width that is necessary
149 // to show all roles without horizontal clipping.
150 qreal maximumRequiredWidth = 0.0;
151
152 foreach (const QByteArray& role, visibleRoles()) {
153 const QString text = KFileItemListWidget::roleText(role, values);
154 const qreal requiredWidth = option.fontMetrics.width(text);
155 maximumRequiredWidth = qMax(maximumRequiredWidth, requiredWidth);
156 }
157
158 const qreal width = option.margin * 4 + option.iconSize + maximumRequiredWidth;
159 const qreal height = option.margin * 2 + qMax(option.iconSize, (1 + additionalRolesCount) * option.fontMetrics.height());
160 return QSizeF(width, height);
161 }
162
163 case DetailsLayout: {
164 // The width will be determined dynamically by KFileItemListView::visibleRoleSizes()
165 const qreal height = option.margin * 2 + qMax(option.iconSize, option.fontMetrics.height());
166 return QSizeF(-1, height);
167 }
168
169 default:
170 Q_ASSERT(false);
171 break;
172 }
173
174 return QSize();
175 }
176
177 QHash<QByteArray, QSizeF> KFileItemListView::visibleRolesSizes(const KItemRangeList& itemRanges) const
178 {
179 QElapsedTimer timer;
180 timer.start();
181
182 QHash<QByteArray, QSizeF> sizes;
183
184 int calculatedItemCount = 0;
185 bool maxTimeExceeded = false;
186 foreach (const KItemRange& itemRange, itemRanges) {
187 const int startIndex = itemRange.index;
188 const int endIndex = startIndex + itemRange.count - 1;
189
190 for (int i = startIndex; i <= endIndex; ++i) {
191 foreach (const QByteArray& visibleRole, visibleRoles()) {
192 QSizeF maxSize = sizes.value(visibleRole, QSizeF(0, 0));
193 const QSizeF itemSize = visibleRoleSizeHint(i, visibleRole);
194 maxSize = maxSize.expandedTo(itemSize);
195 sizes.insert(visibleRole, maxSize);
196 }
197
198 if (calculatedItemCount > 100 && timer.elapsed() > 200) {
199 // When having several thousands of items calculating the sizes can get
200 // very expensive. We accept a possibly too small role-size in favour
201 // of having no blocking user interface.
202 #ifdef KFILEITEMLISTVIEW_DEBUG
203 kDebug() << "Timer exceeded, stopped after" << calculatedItemCount << "items";
204 #endif
205 maxTimeExceeded = true;
206 break;
207 }
208 ++calculatedItemCount;
209 }
210 if (maxTimeExceeded) {
211 break;
212 }
213 }
214
215 #ifdef KFILEITEMLISTVIEW_DEBUG
216 int rangesItemCount = 0;
217 foreach (const KItemRange& itemRange, itemRanges) {
218 rangesItemCount += itemRange.count;
219 }
220 kDebug() << "[TIME] Calculated dynamic item size for " << rangesItemCount << "items:" << timer.elapsed();
221 #endif
222 return sizes;
223 }
224
225 bool KFileItemListView::supportsItemExpanding() const
226 {
227 return m_itemLayout == DetailsLayout;
228 }
229
230 QPixmap KFileItemListView::createDragPixmap(const QSet<int>& indexes) const
231 {
232 if (!model()) {
233 return QPixmap();
234 }
235
236 const int itemCount = indexes.count();
237 Q_ASSERT(itemCount > 0);
238
239 // If more than one item is dragged, align the items inside a
240 // rectangular grid. The maximum grid size is limited to 5 x 5 items.
241 int xCount;
242 int size;
243 if (itemCount > 16) {
244 xCount = 5;
245 size = KIconLoader::SizeSmall;
246 } else if (itemCount > 9) {
247 xCount = 4;
248 size = KIconLoader::SizeSmallMedium;
249 } else {
250 xCount = 3;
251 size = KIconLoader::SizeMedium;
252 }
253
254 if (itemCount < xCount) {
255 xCount = itemCount;
256 }
257
258 int yCount = itemCount / xCount;
259 if (itemCount % xCount != 0) {
260 ++yCount;
261 }
262 if (yCount > xCount) {
263 yCount = xCount;
264 }
265
266 // Draw the selected items into the grid cells.
267 QPixmap dragPixmap(xCount * size + xCount, yCount * size + yCount);
268 dragPixmap.fill(Qt::transparent);
269
270 QPainter painter(&dragPixmap);
271 int x = 0;
272 int y = 0;
273 QSetIterator<int> it(indexes);
274 while (it.hasNext()) {
275 const int index = it.next();
276
277 QPixmap pixmap = model()->data(index).value("iconPixmap").value<QPixmap>();
278 if (pixmap.isNull()) {
279 KIcon icon(model()->data(index).value("iconName").toString());
280 pixmap = icon.pixmap(size, size);
281 } else {
282 KPixmapModifier::scale(pixmap, QSize(size, size));
283 }
284
285 painter.drawPixmap(x, y, pixmap);
286
287 x += size + 1;
288 if (x >= dragPixmap.width()) {
289 x = 0;
290 y += size + 1;
291 }
292
293 if (y >= dragPixmap.height()) {
294 break;
295 }
296 }
297
298 return dragPixmap;
299 }
300
301 void KFileItemListView::initializeItemListWidget(KItemListWidget* item)
302 {
303 KFileItemListWidget* fileItemListWidget = static_cast<KFileItemListWidget*>(item);
304
305 switch (m_itemLayout) {
306 case IconsLayout: fileItemListWidget->setLayout(KFileItemListWidget::IconsLayout); break;
307 case CompactLayout: fileItemListWidget->setLayout(KFileItemListWidget::CompactLayout); break;
308 case DetailsLayout: fileItemListWidget->setLayout(KFileItemListWidget::DetailsLayout); break;
309 default: Q_ASSERT(false); break;
310 }
311
312 fileItemListWidget->setAlternatingBackgroundColors(m_itemLayout == DetailsLayout &&
313 visibleRoles().count() > 1);
314 }
315
316 bool KFileItemListView::itemSizeHintUpdateRequired(const QSet<QByteArray>& changedRoles) const
317 {
318 // Even if the icons have a different size they are always aligned within
319 // the area defined by KItemStyleOption.iconSize and hence result in no
320 // change of the item-size.
321 const bool containsIconName = changedRoles.contains("iconName");
322 const bool containsIconPixmap = changedRoles.contains("iconPixmap");
323 const int count = changedRoles.count();
324
325 const bool iconChanged = (containsIconName && containsIconPixmap && count == 2) ||
326 (containsIconName && count == 1) ||
327 (containsIconPixmap && count == 1);
328 return !iconChanged;
329 }
330
331 void KFileItemListView::onModelChanged(KItemModelBase* current, KItemModelBase* previous)
332 {
333 Q_UNUSED(previous);
334 Q_ASSERT(qobject_cast<KFileItemModel*>(current));
335
336 if (m_modelRolesUpdater) {
337 delete m_modelRolesUpdater;
338 }
339
340 m_modelRolesUpdater = new KFileItemModelRolesUpdater(static_cast<KFileItemModel*>(current), this);
341 const int size = styleOption().iconSize;
342 m_modelRolesUpdater->setIconSize(QSize(size, size));
343
344 applyRolesToModel();
345 }
346
347 void KFileItemListView::onScrollOrientationChanged(Qt::Orientation current, Qt::Orientation previous)
348 {
349 Q_UNUSED(current);
350 Q_UNUSED(previous);
351 updateLayoutOfVisibleItems();
352 }
353
354 void KFileItemListView::onItemSizeChanged(const QSizeF& current, const QSizeF& previous)
355 {
356 Q_UNUSED(current);
357 Q_UNUSED(previous);
358 triggerVisibleIndexRangeUpdate();
359 }
360
361 void KFileItemListView::onScrollOffsetChanged(qreal current, qreal previous)
362 {
363 Q_UNUSED(current);
364 Q_UNUSED(previous);
365 triggerVisibleIndexRangeUpdate();
366 }
367
368 void KFileItemListView::onVisibleRolesChanged(const QList<QByteArray>& current, const QList<QByteArray>& previous)
369 {
370 Q_UNUSED(current);
371 Q_UNUSED(previous);
372 applyRolesToModel();
373
374 if (m_itemLayout == DetailsLayout) {
375 // Only enable the alternating background colors if more than one role
376 // is visible
377 const int previousCount = previous.count();
378 const int currentCount = current.count();
379 if ((previousCount <= 1 && currentCount > 1) || (previousCount > 1 && currentCount <= 1)) {
380 const bool enabled = (currentCount > 1);
381 foreach (KItemListWidget* widget, visibleItemListWidgets()) {
382 widget->setAlternatingBackgroundColors(enabled);
383 }
384 }
385 }
386 }
387
388 void KFileItemListView::onStyleOptionChanged(const KItemListStyleOption& current, const KItemListStyleOption& previous)
389 {
390 Q_UNUSED(current);
391 Q_UNUSED(previous);
392 triggerIconSizeUpdate();
393 }
394
395 void KFileItemListView::onTransactionBegin()
396 {
397 m_modelRolesUpdater->setPaused(true);
398 }
399
400 void KFileItemListView::onTransactionEnd()
401 {
402 // Only unpause the model-roles-updater if no timer is active. If one
403 // timer is still active the model-roles-updater will be unpaused later as
404 // soon as the timer has been exceeded.
405 const bool timerActive = m_updateVisibleIndexRangeTimer->isActive() ||
406 m_updateIconSizeTimer->isActive();
407 if (!timerActive) {
408 m_modelRolesUpdater->setPaused(false);
409 }
410 }
411
412 void KFileItemListView::resizeEvent(QGraphicsSceneResizeEvent* event)
413 {
414 KItemListView::resizeEvent(event);
415 triggerVisibleIndexRangeUpdate();
416 }
417
418 void KFileItemListView::slotItemsRemoved(const KItemRangeList& itemRanges)
419 {
420 KItemListView::slotItemsRemoved(itemRanges);
421 updateTimersInterval();
422 }
423
424 void KFileItemListView::slotSortRoleChanged(const QByteArray& current, const QByteArray& previous)
425 {
426 const QByteArray sortRole = model()->sortRole();
427 if (!visibleRoles().contains(sortRole)) {
428 applyRolesToModel();
429 }
430
431 KItemListView::slotSortRoleChanged(current, previous);
432 }
433
434 void KFileItemListView::triggerVisibleIndexRangeUpdate()
435 {
436 if (!model()) {
437 return;
438 }
439 m_modelRolesUpdater->setPaused(true);
440 m_updateVisibleIndexRangeTimer->start();
441 }
442
443 void KFileItemListView::updateVisibleIndexRange()
444 {
445 if (!m_modelRolesUpdater) {
446 return;
447 }
448
449 const int index = firstVisibleIndex();
450 const int count = lastVisibleIndex() - index + 1;
451 m_modelRolesUpdater->setVisibleIndexRange(index, count);
452
453 if (m_updateIconSizeTimer->isActive()) {
454 // If the icon-size update is pending do an immediate update
455 // of the icon-size before unpausing m_modelRolesUpdater. This prevents
456 // an unnecessary expensive recreation of all previews afterwards.
457 m_updateIconSizeTimer->stop();
458 const KItemListStyleOption& option = styleOption();
459 m_modelRolesUpdater->setIconSize(QSize(option.iconSize, option.iconSize));
460 }
461
462 m_modelRolesUpdater->setPaused(isTransactionActive());
463 updateTimersInterval();
464 }
465
466 void KFileItemListView::triggerIconSizeUpdate()
467 {
468 if (!model()) {
469 return;
470 }
471 m_modelRolesUpdater->setPaused(true);
472 m_updateIconSizeTimer->start();
473 }
474
475 void KFileItemListView::updateIconSize()
476 {
477 if (!m_modelRolesUpdater) {
478 return;
479 }
480
481 const KItemListStyleOption& option = styleOption();
482 m_modelRolesUpdater->setIconSize(QSize(option.iconSize, option.iconSize));
483
484 if (m_updateVisibleIndexRangeTimer->isActive()) {
485 // If the visibility-index-range update is pending do an immediate update
486 // of the range before unpausing m_modelRolesUpdater. This prevents
487 // an unnecessary expensive recreation of all previews afterwards.
488 m_updateVisibleIndexRangeTimer->stop();
489 const int index = firstVisibleIndex();
490 const int count = lastVisibleIndex() - index + 1;
491 m_modelRolesUpdater->setVisibleIndexRange(index, count);
492 }
493
494 m_modelRolesUpdater->setPaused(isTransactionActive());
495 updateTimersInterval();
496 }
497
498 QSizeF KFileItemListView::visibleRoleSizeHint(int index, const QByteArray& role) const
499 {
500 const KItemListStyleOption& option = styleOption();
501
502 qreal width = m_minimumRolesWidths.value(role, 0);
503 const qreal height = option.margin * 2 + option.fontMetrics.height();
504
505 const QHash<QByteArray, QVariant> values = model()->data(index);
506 const QString text = KFileItemListWidget::roleText(role, values);
507 if (!text.isEmpty()) {
508 const qreal columnMargin = option.margin * 3;
509 width = qMax(width, qreal(2 * columnMargin + option.fontMetrics.width(text)));
510 }
511
512 if (role == "name") {
513 // Increase the width by the expansion-toggle and the current expansion level
514 const int expansionLevel = values.value("expansionLevel", 0).toInt();
515 width += option.margin + expansionLevel * itemSize().height() + KIconLoader::SizeSmall;
516
517 // Increase the width by the required space for the icon
518 width += option.margin * 2 + option.iconSize;
519 }
520
521 return QSizeF(width, height);
522 }
523
524 void KFileItemListView::updateLayoutOfVisibleItems()
525 {
526 if (!model()) {
527 return;
528 }
529
530 foreach (KItemListWidget* widget, visibleItemListWidgets()) {
531 initializeItemListWidget(widget);
532 }
533 triggerVisibleIndexRangeUpdate();
534 }
535
536 void KFileItemListView::updateTimersInterval()
537 {
538 if (!model()) {
539 return;
540 }
541
542 // The ShortInterval is used for cases like switching the directory: If the
543 // model is empty and filled later the creation of the previews should be done
544 // as soon as possible. The LongInterval is used when the model already contains
545 // items and assures that operations like zooming don't result in too many temporary
546 // recreations of the previews.
547
548 const int interval = (model()->count() <= 0) ? ShortInterval : LongInterval;
549 m_updateVisibleIndexRangeTimer->setInterval(interval);
550 m_updateIconSizeTimer->setInterval(interval);
551 }
552
553 void KFileItemListView::updateMinimumRolesWidths()
554 {
555 m_minimumRolesWidths.clear();
556
557 const KItemListStyleOption& option = styleOption();
558 const QString sizeText = QLatin1String("888888") + i18nc("@item:intable", "items");
559 m_minimumRolesWidths.insert("size", option.fontMetrics.width(sizeText));
560 }
561
562 void KFileItemListView::applyRolesToModel()
563 {
564 if (!model()) {
565 return;
566 }
567
568 Q_ASSERT(qobject_cast<KFileItemModel*>(model()));
569 KFileItemModel* fileItemModel = static_cast<KFileItemModel*>(model());
570
571 // KFileItemModel does not distinct between "visible" and "invisible" roles.
572 // Add all roles that are mandatory for having a working KFileItemListView:
573 QSet<QByteArray> roles = visibleRoles().toSet();
574 roles.insert("iconPixmap");
575 roles.insert("iconName");
576 roles.insert("name");
577 roles.insert("isDir");
578 if (m_itemLayout == DetailsLayout) {
579 roles.insert("isExpanded");
580 roles.insert("expansionLevel");
581 }
582
583 // Assure that the role that is used for sorting will be determined
584 roles.insert(fileItemModel->sortRole());
585
586 fileItemModel->setRoles(roles);
587 m_modelRolesUpdater->setRoles(roles);
588 }
589
590 #include "kfileitemlistview.moc"