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