]> cloud.milkyroute.net Git - dolphin.git/blob - src/kitemviews/kfileitemlistview.cpp
KFileItemModelRolesUpdater: Optimize updates
[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 applyRolesToModel();
380
381 if (m_itemLayout == DetailsLayout) {
382 // Only enable the alternating background colors if more than one role
383 // is visible
384 const int previousCount = previous.count();
385 const int currentCount = current.count();
386 if ((previousCount <= 1 && currentCount > 1) || (previousCount > 1 && currentCount <= 1)) {
387 const bool enabled = (currentCount > 1);
388 foreach (KItemListWidget* widget, visibleItemListWidgets()) {
389 widget->setAlternatingBackgroundColors(enabled);
390 }
391 }
392 }
393 }
394
395 void KFileItemListView::onStyleOptionChanged(const KItemListStyleOption& current, const KItemListStyleOption& previous)
396 {
397 Q_UNUSED(current);
398 Q_UNUSED(previous);
399 triggerIconSizeUpdate();
400 }
401
402 void KFileItemListView::onTransactionBegin()
403 {
404 m_modelRolesUpdater->setPaused(true);
405 }
406
407 void KFileItemListView::onTransactionEnd()
408 {
409 // Only unpause the model-roles-updater if no timer is active. If one
410 // timer is still active the model-roles-updater will be unpaused later as
411 // soon as the timer has been exceeded.
412 const bool timerActive = m_updateVisibleIndexRangeTimer->isActive() ||
413 m_updateIconSizeTimer->isActive();
414 if (!timerActive) {
415 m_modelRolesUpdater->setPaused(false);
416 }
417 }
418
419 void KFileItemListView::resizeEvent(QGraphicsSceneResizeEvent* event)
420 {
421 KItemListView::resizeEvent(event);
422 triggerVisibleIndexRangeUpdate();
423 }
424
425 void KFileItemListView::slotItemsRemoved(const KItemRangeList& itemRanges)
426 {
427 KItemListView::slotItemsRemoved(itemRanges);
428 updateTimersInterval();
429 }
430
431 void KFileItemListView::slotSortRoleChanged(const QByteArray& current, const QByteArray& previous)
432 {
433 const QByteArray sortRole = model()->sortRole();
434 if (!visibleRoles().contains(sortRole)) {
435 applyRolesToModel();
436 }
437
438 KItemListView::slotSortRoleChanged(current, previous);
439 }
440
441 void KFileItemListView::triggerVisibleIndexRangeUpdate()
442 {
443 if (!model()) {
444 return;
445 }
446 m_modelRolesUpdater->setPaused(true);
447 m_updateVisibleIndexRangeTimer->start();
448 }
449
450 void KFileItemListView::updateVisibleIndexRange()
451 {
452 if (!m_modelRolesUpdater) {
453 return;
454 }
455
456 const int index = firstVisibleIndex();
457 const int count = lastVisibleIndex() - index + 1;
458 m_modelRolesUpdater->setVisibleIndexRange(index, count);
459
460 if (m_updateIconSizeTimer->isActive()) {
461 // If the icon-size update is pending do an immediate update
462 // of the icon-size before unpausing m_modelRolesUpdater. This prevents
463 // an unnecessary expensive recreation of all previews afterwards.
464 m_updateIconSizeTimer->stop();
465 const KItemListStyleOption& option = styleOption();
466 m_modelRolesUpdater->setIconSize(QSize(option.iconSize, option.iconSize));
467 }
468
469 m_modelRolesUpdater->setPaused(isTransactionActive());
470 updateTimersInterval();
471 }
472
473 void KFileItemListView::triggerIconSizeUpdate()
474 {
475 if (!model()) {
476 return;
477 }
478 m_modelRolesUpdater->setPaused(true);
479 m_updateIconSizeTimer->start();
480 }
481
482 void KFileItemListView::updateIconSize()
483 {
484 if (!m_modelRolesUpdater) {
485 return;
486 }
487
488 const KItemListStyleOption& option = styleOption();
489 m_modelRolesUpdater->setIconSize(QSize(option.iconSize, option.iconSize));
490
491 if (m_updateVisibleIndexRangeTimer->isActive()) {
492 // If the visibility-index-range update is pending do an immediate update
493 // of the range before unpausing m_modelRolesUpdater. This prevents
494 // an unnecessary expensive recreation of all previews afterwards.
495 m_updateVisibleIndexRangeTimer->stop();
496 const int index = firstVisibleIndex();
497 const int count = lastVisibleIndex() - index + 1;
498 m_modelRolesUpdater->setVisibleIndexRange(index, count);
499 }
500
501 m_modelRolesUpdater->setPaused(isTransactionActive());
502 updateTimersInterval();
503 }
504
505 QSizeF KFileItemListView::visibleRoleSizeHint(int index, const QByteArray& role) const
506 {
507 const KItemListStyleOption& option = styleOption();
508
509 qreal width = m_minimumRolesWidths.value(role, 0);
510 const qreal height = option.margin * 2 + option.fontMetrics.height();
511
512 const QHash<QByteArray, QVariant> values = model()->data(index);
513 const QString text = KFileItemListWidget::roleText(role, values);
514 if (!text.isEmpty()) {
515 const qreal columnMargin = option.margin * 3;
516 width = qMax(width, qreal(2 * columnMargin + option.fontMetrics.width(text)));
517 }
518
519 if (role == "name") {
520 // Increase the width by the expansion-toggle and the current expansion level
521 const int expansionLevel = values.value("expansionLevel", 0).toInt();
522 width += option.margin + expansionLevel * itemSize().height() + KIconLoader::SizeSmall;
523
524 // Increase the width by the required space for the icon
525 width += option.margin * 2 + option.iconSize;
526 }
527
528 return QSizeF(width, height);
529 }
530
531 void KFileItemListView::updateLayoutOfVisibleItems()
532 {
533 if (!model()) {
534 return;
535 }
536
537 foreach (KItemListWidget* widget, visibleItemListWidgets()) {
538 initializeItemListWidget(widget);
539 }
540 triggerVisibleIndexRangeUpdate();
541 }
542
543 void KFileItemListView::updateTimersInterval()
544 {
545 if (!model()) {
546 return;
547 }
548
549 // The ShortInterval is used for cases like switching the directory: If the
550 // model is empty and filled later the creation of the previews should be done
551 // as soon as possible. The LongInterval is used when the model already contains
552 // items and assures that operations like zooming don't result in too many temporary
553 // recreations of the previews.
554
555 const int interval = (model()->count() <= 0) ? ShortInterval : LongInterval;
556 m_updateVisibleIndexRangeTimer->setInterval(interval);
557 m_updateIconSizeTimer->setInterval(interval);
558 }
559
560 void KFileItemListView::updateMinimumRolesWidths()
561 {
562 m_minimumRolesWidths.clear();
563
564 const KItemListStyleOption& option = styleOption();
565 const QString sizeText = QLatin1String("888888") + i18nc("@item:intable", "items");
566 m_minimumRolesWidths.insert("size", option.fontMetrics.width(sizeText));
567 }
568
569 void KFileItemListView::applyRolesToModel()
570 {
571 if (!model()) {
572 return;
573 }
574
575 Q_ASSERT(qobject_cast<KFileItemModel*>(model()));
576 KFileItemModel* fileItemModel = static_cast<KFileItemModel*>(model());
577
578 // KFileItemModel does not distinct between "visible" and "invisible" roles.
579 // Add all roles that are mandatory for having a working KFileItemListView:
580 QSet<QByteArray> roles = visibleRoles().toSet();
581 roles.insert("iconPixmap");
582 roles.insert("iconName");
583 roles.insert("name");
584 roles.insert("isDir");
585 if (m_itemLayout == DetailsLayout) {
586 roles.insert("isExpanded");
587 roles.insert("isExpandable");
588 roles.insert("expansionLevel");
589 }
590
591 // Assure that the role that is used for sorting will be determined
592 roles.insert(fileItemModel->sortRole());
593
594 fileItemModel->setRoles(roles);
595 m_modelRolesUpdater->setRoles(roles);
596 }
597
598 #include "kfileitemlistview.moc"