]> cloud.milkyroute.net Git - dolphin.git/blob - src/kitemviews/kfileitemlistview.cpp
Initial draft for bringing back the "Folders" panel
[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 QPixmap KFileItemListView::createDragPixmap(const QSet<int>& indexes) const
226 {
227 if (!model()) {
228 return QPixmap();
229 }
230
231 const int itemCount = indexes.count();
232 Q_ASSERT(itemCount > 0);
233
234 // If more than one item is dragged, align the items inside a
235 // rectangular grid. The maximum grid size is limited to 5 x 5 items.
236 int xCount;
237 int size;
238 if (itemCount > 16) {
239 xCount = 5;
240 size = KIconLoader::SizeSmall;
241 } else if (itemCount > 9) {
242 xCount = 4;
243 size = KIconLoader::SizeSmallMedium;
244 } else {
245 xCount = 3;
246 size = KIconLoader::SizeMedium;
247 }
248
249 if (itemCount < xCount) {
250 xCount = itemCount;
251 }
252
253 int yCount = itemCount / xCount;
254 if (itemCount % xCount != 0) {
255 ++yCount;
256 }
257 if (yCount > xCount) {
258 yCount = xCount;
259 }
260
261 // Draw the selected items into the grid cells.
262 QPixmap dragPixmap(xCount * size + xCount, yCount * size + yCount);
263 dragPixmap.fill(Qt::transparent);
264
265 QPainter painter(&dragPixmap);
266 int x = 0;
267 int y = 0;
268 QSetIterator<int> it(indexes);
269 while (it.hasNext()) {
270 const int index = it.next();
271
272 QPixmap pixmap = model()->data(index).value("iconPixmap").value<QPixmap>();
273 if (pixmap.isNull()) {
274 KIcon icon(model()->data(index).value("iconName").toString());
275 pixmap = icon.pixmap(size, size);
276 } else {
277 KPixmapModifier::scale(pixmap, QSize(size, size));
278 }
279
280 painter.drawPixmap(x, y, pixmap);
281
282 x += size + 1;
283 if (x >= dragPixmap.width()) {
284 x = 0;
285 y += size + 1;
286 }
287
288 if (y >= dragPixmap.height()) {
289 break;
290 }
291 }
292
293 return dragPixmap;
294 }
295
296 void KFileItemListView::initializeItemListWidget(KItemListWidget* item)
297 {
298 KFileItemListWidget* fileItemListWidget = static_cast<KFileItemListWidget*>(item);
299
300 switch (m_itemLayout) {
301 case IconsLayout: fileItemListWidget->setLayout(KFileItemListWidget::IconsLayout); break;
302 case CompactLayout: fileItemListWidget->setLayout(KFileItemListWidget::CompactLayout); break;
303 case DetailsLayout: fileItemListWidget->setLayout(KFileItemListWidget::DetailsLayout); break;
304 default: Q_ASSERT(false); break;
305 }
306
307 fileItemListWidget->setAlternatingBackgroundColors(m_itemLayout == DetailsLayout &&
308 visibleRoles().count() > 1);
309 }
310
311 bool KFileItemListView::itemSizeHintUpdateRequired(const QSet<QByteArray>& changedRoles) const
312 {
313 // Even if the icons have a different size they are always aligned within
314 // the area defined by KItemStyleOption.iconSize and hence result in no
315 // change of the item-size.
316 const bool containsIconName = changedRoles.contains("iconName");
317 const bool containsIconPixmap = changedRoles.contains("iconPixmap");
318 const int count = changedRoles.count();
319
320 const bool iconChanged = (containsIconName && containsIconPixmap && count == 2) ||
321 (containsIconName && count == 1) ||
322 (containsIconPixmap && count == 1);
323 return !iconChanged;
324 }
325
326 void KFileItemListView::onModelChanged(KItemModelBase* current, KItemModelBase* previous)
327 {
328 Q_UNUSED(previous);
329 Q_ASSERT(qobject_cast<KFileItemModel*>(current));
330
331 if (m_modelRolesUpdater) {
332 delete m_modelRolesUpdater;
333 }
334
335 m_modelRolesUpdater = new KFileItemModelRolesUpdater(static_cast<KFileItemModel*>(current), this);
336 const int size = styleOption().iconSize;
337 m_modelRolesUpdater->setIconSize(QSize(size, size));
338
339 applyRolesToModel();
340 }
341
342 void KFileItemListView::onScrollOrientationChanged(Qt::Orientation current, Qt::Orientation previous)
343 {
344 Q_UNUSED(current);
345 Q_UNUSED(previous);
346 updateLayoutOfVisibleItems();
347 }
348
349 void KFileItemListView::onItemSizeChanged(const QSizeF& current, const QSizeF& previous)
350 {
351 Q_UNUSED(current);
352 Q_UNUSED(previous);
353 triggerVisibleIndexRangeUpdate();
354 }
355
356 void KFileItemListView::onScrollOffsetChanged(qreal current, qreal previous)
357 {
358 Q_UNUSED(current);
359 Q_UNUSED(previous);
360 triggerVisibleIndexRangeUpdate();
361 }
362
363 void KFileItemListView::onVisibleRolesChanged(const QList<QByteArray>& current, const QList<QByteArray>& previous)
364 {
365 Q_UNUSED(current);
366 Q_UNUSED(previous);
367 applyRolesToModel();
368
369 if (m_itemLayout == DetailsLayout) {
370 // Only enable the alternating background colors if more than one role
371 // is visible
372 const int previousCount = previous.count();
373 const int currentCount = current.count();
374 if ((previousCount <= 1 && currentCount > 1) || (previousCount > 1 && currentCount <= 1)) {
375 const bool enabled = (currentCount > 1);
376 foreach (KItemListWidget* widget, visibleItemListWidgets()) {
377 widget->setAlternatingBackgroundColors(enabled);
378 }
379 }
380 }
381 }
382
383 void KFileItemListView::onStyleOptionChanged(const KItemListStyleOption& current, const KItemListStyleOption& previous)
384 {
385 Q_UNUSED(current);
386 Q_UNUSED(previous);
387 triggerIconSizeUpdate();
388 }
389
390 void KFileItemListView::onTransactionBegin()
391 {
392 m_modelRolesUpdater->setPaused(true);
393 }
394
395 void KFileItemListView::onTransactionEnd()
396 {
397 // Only unpause the model-roles-updater if no timer is active. If one
398 // timer is still active the model-roles-updater will be unpaused later as
399 // soon as the timer has been exceeded.
400 const bool timerActive = m_updateVisibleIndexRangeTimer->isActive() ||
401 m_updateIconSizeTimer->isActive();
402 if (!timerActive) {
403 m_modelRolesUpdater->setPaused(false);
404 }
405 }
406
407 void KFileItemListView::resizeEvent(QGraphicsSceneResizeEvent* event)
408 {
409 KItemListView::resizeEvent(event);
410 triggerVisibleIndexRangeUpdate();
411 }
412
413 void KFileItemListView::slotItemsRemoved(const KItemRangeList& itemRanges)
414 {
415 KItemListView::slotItemsRemoved(itemRanges);
416 updateTimersInterval();
417 }
418
419 void KFileItemListView::slotSortRoleChanged(const QByteArray& current, const QByteArray& previous)
420 {
421 const QByteArray sortRole = model()->sortRole();
422 if (!visibleRoles().contains(sortRole)) {
423 applyRolesToModel();
424 }
425
426 KItemListView::slotSortRoleChanged(current, previous);
427 }
428
429 void KFileItemListView::triggerVisibleIndexRangeUpdate()
430 {
431 if (!model()) {
432 return;
433 }
434 m_modelRolesUpdater->setPaused(true);
435 m_updateVisibleIndexRangeTimer->start();
436 }
437
438 void KFileItemListView::updateVisibleIndexRange()
439 {
440 if (!m_modelRolesUpdater) {
441 return;
442 }
443
444 const int index = firstVisibleIndex();
445 const int count = lastVisibleIndex() - index + 1;
446 m_modelRolesUpdater->setVisibleIndexRange(index, count);
447
448 if (m_updateIconSizeTimer->isActive()) {
449 // If the icon-size update is pending do an immediate update
450 // of the icon-size before unpausing m_modelRolesUpdater. This prevents
451 // an unnecessary expensive recreation of all previews afterwards.
452 m_updateIconSizeTimer->stop();
453 const KItemListStyleOption& option = styleOption();
454 m_modelRolesUpdater->setIconSize(QSize(option.iconSize, option.iconSize));
455 }
456
457 m_modelRolesUpdater->setPaused(isTransactionActive());
458 updateTimersInterval();
459 }
460
461 void KFileItemListView::triggerIconSizeUpdate()
462 {
463 if (!model()) {
464 return;
465 }
466 m_modelRolesUpdater->setPaused(true);
467 m_updateIconSizeTimer->start();
468 }
469
470 void KFileItemListView::updateIconSize()
471 {
472 if (!m_modelRolesUpdater) {
473 return;
474 }
475
476 const KItemListStyleOption& option = styleOption();
477 m_modelRolesUpdater->setIconSize(QSize(option.iconSize, option.iconSize));
478
479 if (m_updateVisibleIndexRangeTimer->isActive()) {
480 // If the visibility-index-range update is pending do an immediate update
481 // of the range before unpausing m_modelRolesUpdater. This prevents
482 // an unnecessary expensive recreation of all previews afterwards.
483 m_updateVisibleIndexRangeTimer->stop();
484 const int index = firstVisibleIndex();
485 const int count = lastVisibleIndex() - index + 1;
486 m_modelRolesUpdater->setVisibleIndexRange(index, count);
487 }
488
489 m_modelRolesUpdater->setPaused(isTransactionActive());
490 updateTimersInterval();
491 }
492
493 QSizeF KFileItemListView::visibleRoleSizeHint(int index, const QByteArray& role) const
494 {
495 const KItemListStyleOption& option = styleOption();
496
497 qreal width = m_minimumRolesWidths.value(role, 0);
498 const qreal height = option.margin * 2 + option.fontMetrics.height();
499
500 const QHash<QByteArray, QVariant> values = model()->data(index);
501 const QString text = KFileItemListWidget::roleText(role, values);
502 if (!text.isEmpty()) {
503 const qreal columnMargin = option.margin * 3;
504 width = qMax(width, qreal(2 * columnMargin + option.fontMetrics.width(text)));
505 }
506
507 if (role == "name") {
508 // Increase the width by the expansion-toggle and the current expansion level
509 const int expansionLevel = values.value("expansionLevel", 0).toInt();
510 width += option.margin + expansionLevel * itemSize().height() + KIconLoader::SizeSmall;
511
512 // Increase the width by the required space for the icon
513 width += option.margin * 2 + option.iconSize;
514 }
515
516 return QSizeF(width, height);
517 }
518
519 void KFileItemListView::updateLayoutOfVisibleItems()
520 {
521 if (!model()) {
522 return;
523 }
524
525 foreach (KItemListWidget* widget, visibleItemListWidgets()) {
526 initializeItemListWidget(widget);
527 }
528 triggerVisibleIndexRangeUpdate();
529 }
530
531 void KFileItemListView::updateTimersInterval()
532 {
533 if (!model()) {
534 return;
535 }
536
537 // The ShortInterval is used for cases like switching the directory: If the
538 // model is empty and filled later the creation of the previews should be done
539 // as soon as possible. The LongInterval is used when the model already contains
540 // items and assures that operations like zooming don't result in too many temporary
541 // recreations of the previews.
542
543 const int interval = (model()->count() <= 0) ? ShortInterval : LongInterval;
544 m_updateVisibleIndexRangeTimer->setInterval(interval);
545 m_updateIconSizeTimer->setInterval(interval);
546 }
547
548 void KFileItemListView::updateMinimumRolesWidths()
549 {
550 m_minimumRolesWidths.clear();
551
552 const KItemListStyleOption& option = styleOption();
553 const QString sizeText = QLatin1String("888888") + i18nc("@item:intable", "items");
554 m_minimumRolesWidths.insert("size", option.fontMetrics.width(sizeText));
555 }
556
557 void KFileItemListView::applyRolesToModel()
558 {
559 if (!model()) {
560 return;
561 }
562
563 Q_ASSERT(qobject_cast<KFileItemModel*>(model()));
564 KFileItemModel* fileItemModel = static_cast<KFileItemModel*>(model());
565
566 // KFileItemModel does not distinct between "visible" and "invisible" roles.
567 // Add all roles that are mandatory for having a working KFileItemListView:
568 QSet<QByteArray> roles = visibleRoles().toSet();
569 roles.insert("iconPixmap");
570 roles.insert("iconName");
571 roles.insert("name");
572 roles.insert("isDir");
573 if (m_itemLayout == DetailsLayout) {
574 roles.insert("isExpanded");
575 roles.insert("expansionLevel");
576 }
577
578 // Assure that the role that is used for sorting will be determined
579 roles.insert(fileItemModel->sortRole());
580
581 fileItemModel->setRoles(roles);
582 m_modelRolesUpdater->setRoles(roles);
583 }
584
585 #include "kfileitemlistview.moc"