]> cloud.milkyroute.net Git - dolphin.git/blob - src/kitemviews/kfileitemmodelrolesupdater.cpp
Prevent repeated re-layouting of all items while previews are generated
[dolphin.git] / src / kitemviews / kfileitemmodelrolesupdater.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 "kfileitemmodelrolesupdater.h"
21
22 #include "kfileitemmodel.h"
23
24 #include <KConfig>
25 #include <KConfigGroup>
26 #include <KDebug>
27 #include <KDirWatch>
28 #include <KFileItem>
29 #include <KGlobal>
30 #include <KIO/JobUiDelegate>
31 #include <KIO/PreviewJob>
32
33 #include "private/kpixmapmodifier.h"
34
35 #include <QApplication>
36 #include <QPainter>
37 #include <QPixmap>
38 #include <QElapsedTimer>
39 #include <QTimer>
40
41 #ifdef HAVE_NEPOMUK
42 #include "private/knepomukrolesprovider.h"
43 #include <Nepomuk2/ResourceWatcher>
44 #endif
45
46 // Required includes for subItemsCount():
47 #ifdef Q_WS_WIN
48 #include <QDir>
49 #else
50 #include <dirent.h>
51 #include <QFile>
52 #endif
53
54 // #define KFILEITEMMODELROLESUPDATER_DEBUG
55
56 namespace {
57 // Maximum time in ms that the KFileItemModelRolesUpdater
58 // may perform a blocking operation
59 const int MaxBlockTimeout = 200;
60
61 // Maximum number of items that will get resolved synchronously.
62 // The value should roughly represent the number of maximum visible
63 // items, as it does not make sense to resolve more items synchronously
64 // and probably reach the MaxBlockTimeout because of invisible items.
65 const int MaxResolveItemsCount = 100;
66 }
67
68 KFileItemModelRolesUpdater::KFileItemModelRolesUpdater(KFileItemModel* model, QObject* parent) :
69 QObject(parent),
70 m_paused(false),
71 m_previewChangedDuringPausing(false),
72 m_iconSizeChangedDuringPausing(false),
73 m_rolesChangedDuringPausing(false),
74 m_previewShown(false),
75 m_enlargeSmallPreviews(true),
76 m_clearPreviews(false),
77 m_sortingProgress(-1),
78 m_model(model),
79 m_iconSize(),
80 m_firstVisibleIndex(0),
81 m_lastVisibleIndex(-1),
82 m_roles(),
83 m_enabledPlugins(),
84 m_pendingVisibleItems(),
85 m_pendingInvisibleItems(),
86 m_previewJobs(),
87 m_changedItemsTimer(0),
88 m_changedItems(),
89 m_dirWatcher(0),
90 m_watchedDirs()
91 #ifdef HAVE_NEPOMUK
92 , m_nepomukResourceWatcher(0),
93 m_nepomukUriItems()
94 #endif
95 {
96 Q_ASSERT(model);
97
98 const KConfigGroup globalConfig(KGlobal::config(), "PreviewSettings");
99 m_enabledPlugins = globalConfig.readEntry("Plugins", QStringList()
100 << "directorythumbnail"
101 << "imagethumbnail"
102 << "jpegthumbnail");
103
104 connect(m_model, SIGNAL(itemsInserted(KItemRangeList)),
105 this, SLOT(slotItemsInserted(KItemRangeList)));
106 connect(m_model, SIGNAL(itemsRemoved(KItemRangeList)),
107 this, SLOT(slotItemsRemoved(KItemRangeList)));
108 connect(m_model, SIGNAL(itemsChanged(KItemRangeList,QSet<QByteArray>)),
109 this, SLOT(slotItemsChanged(KItemRangeList,QSet<QByteArray>)));
110 connect(m_model, SIGNAL(sortRoleChanged(QByteArray,QByteArray)),
111 this, SLOT(slotSortRoleChanged(QByteArray,QByteArray)));
112
113 // Use a timer to prevent that each call of slotItemsChanged() results in a synchronous
114 // resolving of the roles. Postpone the resolving until no update has been done for 1 second.
115 m_changedItemsTimer = new QTimer(this);
116 m_changedItemsTimer->setInterval(1000);
117 m_changedItemsTimer->setSingleShot(true);
118 connect(m_changedItemsTimer, SIGNAL(timeout()), this, SLOT(resolveChangedItems()));
119
120 m_resolvableRoles.insert("size");
121 m_resolvableRoles.insert("type");
122 m_resolvableRoles.insert("isExpandable");
123 #ifdef HAVE_NEPOMUK
124 m_resolvableRoles += KNepomukRolesProvider::instance().roles();
125 #endif
126
127 // When folders are expandable or the item-count is shown for folders, it is necessary
128 // to watch the number of items of the sub-folder to be able to react on changes.
129 m_dirWatcher = new KDirWatch(this);
130 connect(m_dirWatcher, SIGNAL(dirty(QString)), this, SLOT(slotDirWatchDirty(QString)));
131 }
132
133 KFileItemModelRolesUpdater::~KFileItemModelRolesUpdater()
134 {
135 resetPendingRoles();
136 }
137
138 void KFileItemModelRolesUpdater::setIconSize(const QSize& size)
139 {
140 if (size != m_iconSize) {
141 m_iconSize = size;
142 if (m_paused) {
143 m_iconSizeChangedDuringPausing = true;
144 } else if (m_previewShown) {
145 // An icon size change requires the regenerating of
146 // all previews
147 sortAndResolveAllRoles();
148 } else {
149 sortAndResolvePendingRoles();
150 }
151 }
152 }
153
154 QSize KFileItemModelRolesUpdater::iconSize() const
155 {
156 return m_iconSize;
157 }
158
159 void KFileItemModelRolesUpdater::setVisibleIndexRange(int index, int count)
160 {
161 if (index < 0) {
162 index = 0;
163 }
164 if (count < 0) {
165 count = 0;
166 }
167
168 if (index == m_firstVisibleIndex && count == m_lastVisibleIndex - m_firstVisibleIndex + 1) {
169 // The range has not been changed
170 return;
171 }
172
173 m_firstVisibleIndex = index;
174 m_lastVisibleIndex = qMin(index + count - 1, m_model->count() - 1);
175
176 if (hasPendingRoles() && !m_paused) {
177 sortAndResolvePendingRoles();
178 }
179 }
180
181 void KFileItemModelRolesUpdater::setPreviewsShown(bool show)
182 {
183 if (show == m_previewShown) {
184 return;
185 }
186
187 m_previewShown = show;
188 if (!show) {
189 m_clearPreviews = true;
190 }
191
192 updateAllPreviews();
193 }
194
195 bool KFileItemModelRolesUpdater::previewsShown() const
196 {
197 return m_previewShown;
198 }
199
200 void KFileItemModelRolesUpdater::setEnlargeSmallPreviews(bool enlarge)
201 {
202 if (enlarge != m_enlargeSmallPreviews) {
203 m_enlargeSmallPreviews = enlarge;
204 if (m_previewShown) {
205 updateAllPreviews();
206 }
207 }
208 }
209
210 bool KFileItemModelRolesUpdater::enlargeSmallPreviews() const
211 {
212 return m_enlargeSmallPreviews;
213 }
214
215 void KFileItemModelRolesUpdater::setEnabledPlugins(const QStringList& list)
216 {
217 if (m_enabledPlugins != list) {
218 m_enabledPlugins = list;
219 if (m_previewShown) {
220 updateAllPreviews();
221 }
222 }
223 }
224
225 void KFileItemModelRolesUpdater::setPaused(bool paused)
226 {
227 if (paused == m_paused) {
228 return;
229 }
230
231 m_paused = paused;
232 if (paused) {
233 if (hasPendingRoles()) {
234 foreach (KJob* job, m_previewJobs) {
235 job->kill();
236 }
237 Q_ASSERT(m_previewJobs.isEmpty());
238 }
239 } else {
240 const bool resolveAll = (m_iconSizeChangedDuringPausing && m_previewShown) ||
241 m_previewChangedDuringPausing ||
242 m_rolesChangedDuringPausing;
243 if (resolveAll) {
244 sortAndResolveAllRoles();
245 } else {
246 sortAndResolvePendingRoles();
247 }
248
249 m_iconSizeChangedDuringPausing = false;
250 m_previewChangedDuringPausing = false;
251 m_rolesChangedDuringPausing = false;
252 }
253 }
254
255 void KFileItemModelRolesUpdater::setRoles(const QSet<QByteArray>& roles)
256 {
257 if (m_roles != roles) {
258 m_roles = roles;
259
260 #ifdef HAVE_NEPOMUK
261 // Check whether there is at least one role that must be resolved
262 // with the help of Nepomuk. If this is the case, a (quite expensive)
263 // resolving will be done in KFileItemModelRolesUpdater::rolesData() and
264 // the role gets watched for changes.
265 const KNepomukRolesProvider& rolesProvider = KNepomukRolesProvider::instance();
266 bool hasNepomukRole = false;
267 QSetIterator<QByteArray> it(roles);
268 while (it.hasNext()) {
269 const QByteArray& role = it.next();
270 if (rolesProvider.roles().contains(role)) {
271 hasNepomukRole = true;
272 break;
273 }
274 }
275
276 if (hasNepomukRole && !m_nepomukResourceWatcher) {
277 Q_ASSERT(m_nepomukUriItems.isEmpty());
278
279 m_nepomukResourceWatcher = new Nepomuk2::ResourceWatcher(this);
280 connect(m_nepomukResourceWatcher, SIGNAL(propertyChanged(Nepomuk2::Resource,Nepomuk2::Types::Property,QVariantList,QVariantList)),
281 this, SLOT(applyChangedNepomukRoles(Nepomuk2::Resource)));
282 } else if (!hasNepomukRole && m_nepomukResourceWatcher) {
283 delete m_nepomukResourceWatcher;
284 m_nepomukResourceWatcher = 0;
285 m_nepomukUriItems.clear();
286 }
287 #endif
288
289 updateSortProgress();
290
291 if (m_paused) {
292 m_rolesChangedDuringPausing = true;
293 } else {
294 sortAndResolveAllRoles();
295 }
296 }
297 }
298
299 QSet<QByteArray> KFileItemModelRolesUpdater::roles() const
300 {
301 return m_roles;
302 }
303
304 bool KFileItemModelRolesUpdater::isPaused() const
305 {
306 return m_paused;
307 }
308
309 QStringList KFileItemModelRolesUpdater::enabledPlugins() const
310 {
311 return m_enabledPlugins;
312 }
313
314 void KFileItemModelRolesUpdater::slotItemsInserted(const KItemRangeList& itemRanges)
315 {
316 startUpdating(itemRanges);
317 }
318
319 void KFileItemModelRolesUpdater::slotItemsRemoved(const KItemRangeList& itemRanges)
320 {
321 Q_UNUSED(itemRanges);
322
323 const bool allItemsRemoved = (m_model->count() == 0);
324
325 if (!m_watchedDirs.isEmpty()) {
326 // Don't let KDirWatch watch for removed items
327 if (allItemsRemoved) {
328 foreach (const QString& path, m_watchedDirs) {
329 m_dirWatcher->removeDir(path);
330 }
331 m_watchedDirs.clear();
332 } else {
333 QMutableSetIterator<QString> it(m_watchedDirs);
334 while (it.hasNext()) {
335 const QString& path = it.next();
336 if (m_model->index(KUrl(path)) < 0) {
337 m_dirWatcher->removeDir(path);
338 it.remove();
339 }
340 }
341 }
342 }
343
344 #ifdef HAVE_NEPOMUK
345 if (m_nepomukResourceWatcher) {
346 // Don't let the ResourceWatcher watch for removed items
347 if (allItemsRemoved) {
348 m_nepomukResourceWatcher->setResources(QList<Nepomuk2::Resource>());
349 m_nepomukResourceWatcher->stop();
350 m_nepomukUriItems.clear();
351 } else {
352 QList<Nepomuk2::Resource> newResources;
353 const QList<Nepomuk2::Resource> oldResources = m_nepomukResourceWatcher->resources();
354 foreach (const Nepomuk2::Resource& resource, oldResources) {
355 const QUrl uri = resource.uri();
356 const KUrl itemUrl = m_nepomukUriItems.value(uri);
357 if (m_model->index(itemUrl) >= 0) {
358 newResources.append(resource);
359 } else {
360 m_nepomukUriItems.remove(uri);
361 }
362 }
363 m_nepomukResourceWatcher->setResources(newResources);
364 if (newResources.isEmpty()) {
365 Q_ASSERT(m_nepomukUriItems.isEmpty());
366 m_nepomukResourceWatcher->stop();
367 }
368 }
369 }
370 #endif
371
372 m_firstVisibleIndex = 0;
373 m_lastVisibleIndex = -1;
374 if (!hasPendingRoles()) {
375 return;
376 }
377
378 if (allItemsRemoved) {
379 // Most probably a directory change is done. Clear all pending items
380 // and also kill all ongoing preview-jobs.
381 resetPendingRoles();
382
383 m_changedItems.clear();
384 m_changedItemsTimer->stop();
385 } else {
386 // Remove all items from m_pendingVisibleItems and m_pendingInvisibleItems
387 // that are not part of the model anymore. The items from m_changedItems
388 // don't need to be handled here, removed items are just skipped in
389 // resolveChangedItems().
390 for (int i = 0; i <= 1; ++i) {
391 QSet<KFileItem>& pendingItems = (i == 0) ? m_pendingVisibleItems : m_pendingInvisibleItems;
392 QMutableSetIterator<KFileItem> it(pendingItems);
393 while (it.hasNext()) {
394 const KFileItem item = it.next();
395 if (m_model->index(item) < 0) {
396 pendingItems.remove(item);
397 }
398 }
399 }
400 }
401 }
402
403 void KFileItemModelRolesUpdater::slotItemsChanged(const KItemRangeList& itemRanges,
404 const QSet<QByteArray>& roles)
405 {
406 Q_UNUSED(roles);
407
408 if (m_changedItemsTimer->isActive()) {
409 // A call of slotItemsChanged() has been done recently. Postpone the resolving
410 // of the roles until the timer has exceeded.
411 foreach (const KItemRange& itemRange, itemRanges) {
412 int index = itemRange.index;
413 for (int count = itemRange.count; count > 0; --count) {
414 m_changedItems.insert(m_model->fileItem(index));
415 ++index;
416 }
417 }
418 } else {
419 // No call of slotItemsChanged() has been done recently, resolve the roles now.
420 startUpdating(itemRanges);
421 }
422 m_changedItemsTimer->start();
423 }
424
425 void KFileItemModelRolesUpdater::slotSortRoleChanged(const QByteArray& current,
426 const QByteArray& previous)
427 {
428 Q_UNUSED(current);
429 Q_UNUSED(previous);
430 updateSortProgress();
431 }
432
433 void KFileItemModelRolesUpdater::slotGotPreview(const KFileItem& item, const QPixmap& pixmap)
434 {
435 m_pendingVisibleItems.remove(item);
436 m_pendingInvisibleItems.remove(item);
437
438 const int index = m_model->index(item);
439 if (index < 0) {
440 return;
441 }
442
443 QPixmap scaledPixmap = pixmap;
444
445 const QString mimeType = item.mimetype();
446 const int slashIndex = mimeType.indexOf(QLatin1Char('/'));
447 const QString mimeTypeGroup = mimeType.left(slashIndex);
448 if (mimeTypeGroup == QLatin1String("image")) {
449 if (m_enlargeSmallPreviews) {
450 KPixmapModifier::applyFrame(scaledPixmap, m_iconSize);
451 } else {
452 // Assure that small previews don't get enlarged. Instead they
453 // should be shown centered within the frame.
454 const QSize contentSize = KPixmapModifier::sizeInsideFrame(m_iconSize);
455 const bool enlargingRequired = scaledPixmap.width() < contentSize.width() &&
456 scaledPixmap.height() < contentSize.height();
457 if (enlargingRequired) {
458 QSize frameSize = scaledPixmap.size();
459 frameSize.scale(m_iconSize, Qt::KeepAspectRatio);
460
461 QPixmap largeFrame(frameSize);
462 largeFrame.fill(Qt::transparent);
463
464 KPixmapModifier::applyFrame(largeFrame, frameSize);
465
466 QPainter painter(&largeFrame);
467 painter.drawPixmap((largeFrame.width() - scaledPixmap.width()) / 2,
468 (largeFrame.height() - scaledPixmap.height()) / 2,
469 scaledPixmap);
470 scaledPixmap = largeFrame;
471 } else {
472 // The image must be shrinked as it is too large to fit into
473 // the available icon size
474 KPixmapModifier::applyFrame(scaledPixmap, m_iconSize);
475 }
476 }
477 } else {
478 KPixmapModifier::scale(scaledPixmap, m_iconSize);
479 }
480
481 QHash<QByteArray, QVariant> data = rolesData(item);
482 data.insert("iconPixmap", scaledPixmap);
483
484 disconnect(m_model, SIGNAL(itemsChanged(KItemRangeList,QSet<QByteArray>)),
485 this, SLOT(slotItemsChanged(KItemRangeList,QSet<QByteArray>)));
486 m_model->setData(index, data);
487 connect(m_model, SIGNAL(itemsChanged(KItemRangeList,QSet<QByteArray>)),
488 this, SLOT(slotItemsChanged(KItemRangeList,QSet<QByteArray>)));
489
490 applySortProgressToModel();
491 }
492
493 void KFileItemModelRolesUpdater::slotPreviewFailed(const KFileItem& item)
494 {
495 m_pendingVisibleItems.remove(item);
496 m_pendingInvisibleItems.remove(item);
497
498 const bool clearPreviews = m_clearPreviews;
499 m_clearPreviews = true;
500 applyResolvedRoles(item, ResolveAll);
501 m_clearPreviews = clearPreviews;
502
503 applySortProgressToModel();
504 }
505
506 void KFileItemModelRolesUpdater::slotPreviewJobFinished(KJob* job)
507 {
508 #ifdef KFILEITEMMODELROLESUPDATER_DEBUG
509 kDebug() << "Preview job finished. Pending visible:" << m_pendingVisibleItems.count() << "invisible:" << m_pendingInvisibleItems.count();
510 #endif
511
512 m_previewJobs.removeOne(job);
513 if (!m_previewJobs.isEmpty() || !hasPendingRoles()) {
514 return;
515 }
516
517 const KFileItemList visibleItems = sortedItems(m_pendingVisibleItems);
518 startPreviewJob(visibleItems + m_pendingInvisibleItems.toList());
519 }
520
521 void KFileItemModelRolesUpdater::resolveNextPendingRoles()
522 {
523 if (m_paused) {
524 return;
525 }
526
527 if (m_previewShown) {
528 // The preview has been turned on since the last run. Skip
529 // resolving further pending roles as this is done as soon
530 // as a preview has been received.
531 return;
532 }
533
534 int resolvedCount = 0;
535 bool changed = false;
536 for (int i = 0; i <= 1; ++i) {
537 QSet<KFileItem>& pendingItems = (i == 0) ? m_pendingVisibleItems : m_pendingInvisibleItems;
538 QSet<KFileItem>::iterator it = pendingItems.begin();
539 while (it != pendingItems.end() && !changed && resolvedCount < MaxResolveItemsCount) {
540 changed = applyResolvedRoles(*it, ResolveAll);
541 it = pendingItems.erase(it);
542 ++resolvedCount;
543 }
544 }
545
546 if (hasPendingRoles()) {
547 QTimer::singleShot(0, this, SLOT(resolveNextPendingRoles()));
548 } else {
549 m_clearPreviews = false;
550 }
551
552 applySortProgressToModel();
553
554 #ifdef KFILEITEMMODELROLESUPDATER_DEBUG
555 static int callCount = 0;
556 ++callCount;
557 if (callCount % 100 == 0) {
558 kDebug() << "Remaining visible roles to resolve:" << m_pendingVisibleItems.count()
559 << "invisible:" << m_pendingInvisibleItems.count();
560 }
561 #endif
562 }
563
564 void KFileItemModelRolesUpdater::resolveChangedItems()
565 {
566 if (m_changedItems.isEmpty()) {
567 return;
568 }
569
570 KItemRangeList itemRanges;
571
572 QSetIterator<KFileItem> it(m_changedItems);
573 while (it.hasNext()) {
574 const KFileItem& item = it.next();
575 const int index = m_model->index(item);
576 if (index >= 0) {
577 itemRanges.append(KItemRange(index, 1));
578 }
579 }
580 m_changedItems.clear();
581
582 startUpdating(itemRanges);
583 }
584
585 void KFileItemModelRolesUpdater::applyChangedNepomukRoles(const Nepomuk2::Resource& resource)
586 {
587 #ifdef HAVE_NEPOMUK
588 const KUrl itemUrl = m_nepomukUriItems.value(resource.uri());
589 const KFileItem item = m_model->fileItem(itemUrl);
590
591 if (item.isNull()) {
592 // itemUrl is not in the model anymore, probably because
593 // the corresponding file has been deleted in the meantime.
594 return;
595 }
596
597 QHash<QByteArray, QVariant> data = rolesData(item);
598
599 const KNepomukRolesProvider& rolesProvider = KNepomukRolesProvider::instance();
600 QHashIterator<QByteArray, QVariant> it(rolesProvider.roleValues(resource, m_roles));
601 while (it.hasNext()) {
602 it.next();
603 data.insert(it.key(), it.value());
604 }
605
606 disconnect(m_model, SIGNAL(itemsChanged(KItemRangeList,QSet<QByteArray>)),
607 this, SLOT(slotItemsChanged(KItemRangeList,QSet<QByteArray>)));
608 const int index = m_model->index(item);
609 m_model->setData(index, data);
610 connect(m_model, SIGNAL(itemsChanged(KItemRangeList,QSet<QByteArray>)),
611 this, SLOT(slotItemsChanged(KItemRangeList,QSet<QByteArray>)));
612 #else
613 #ifndef Q_CC_MSVC
614 Q_UNUSED(resource);
615 #endif
616 #endif
617 }
618
619 void KFileItemModelRolesUpdater::slotDirWatchDirty(const QString& path)
620 {
621 const bool getSizeRole = m_roles.contains("size");
622 const bool getIsExpandableRole = m_roles.contains("isExpandable");
623
624 if (getSizeRole || getIsExpandableRole) {
625 const int index = m_model->index(KUrl(path));
626 if (index >= 0) {
627 QHash<QByteArray, QVariant> data;
628
629 const int count = subItemsCount(path);
630 if (getSizeRole) {
631 data.insert("size", count);
632 }
633 if (getIsExpandableRole) {
634 data.insert("isExpandable", count > 0);
635 }
636
637 m_model->setData(index, data);
638 }
639 }
640 }
641
642 void KFileItemModelRolesUpdater::startUpdating(const KItemRangeList& itemRanges)
643 {
644 // If no valid index range is given assume that all items are visible.
645 // A cleanup will be done later as soon as the index range has been set.
646 const bool hasValidIndexRange = (m_lastVisibleIndex >= 0);
647
648 if (hasValidIndexRange) {
649 // Move all current pending visible items that are not visible anymore
650 // to the pending invisible items.
651 QSet<KFileItem>::iterator it = m_pendingVisibleItems.begin();
652 while (it != m_pendingVisibleItems.end()) {
653 const KFileItem item = *it;
654 const int index = m_model->index(item);
655 if (index < m_firstVisibleIndex || index > m_lastVisibleIndex) {
656 it = m_pendingVisibleItems.erase(it);
657 m_pendingInvisibleItems.insert(item);
658 } else {
659 ++it;
660 }
661 }
662 }
663
664 int rangesCount = 0;
665
666 foreach (const KItemRange& range, itemRanges) {
667 rangesCount += range.count;
668
669 // Add the inserted items to the pending visible and invisible items
670 const int lastIndex = range.index + range.count - 1;
671 for (int i = range.index; i <= lastIndex; ++i) {
672 const KFileItem item = m_model->fileItem(i);
673 if (!hasValidIndexRange || (i >= m_firstVisibleIndex && i <= m_lastVisibleIndex)) {
674 m_pendingVisibleItems.insert(item);
675 } else {
676 m_pendingInvisibleItems.insert(item);
677 }
678 }
679 }
680
681 resolvePendingRoles();
682 }
683
684 void KFileItemModelRolesUpdater::startPreviewJob(const KFileItemList& items)
685 {
686 if (items.isEmpty() || m_paused) {
687 return;
688 }
689
690 // PreviewJob internally caches items always with the size of
691 // 128 x 128 pixels or 256 x 256 pixels. A (slow) downscaling is done
692 // by PreviewJob if a smaller size is requested. For images KFileItemModelRolesUpdater must
693 // do a downscaling anyhow because of the frame, so in this case only the provided
694 // cache sizes are requested.
695 const QSize cacheSize = (m_iconSize.width() > 128) || (m_iconSize.height() > 128)
696 ? QSize(256, 256) : QSize(128, 128);
697
698 // KIO::filePreview() will request the MIME-type of all passed items, which (in the
699 // worst case) might block the application for several seconds. To prevent such
700 // a blocking the MIME-type of the items will determined until the MaxBlockTimeout
701 // has been reached and only those items will get passed. As soon as the MIME-type
702 // has been resolved once KIO::PreviewJob() can already access the resolved
703 // MIME-type in a fast way.
704 QElapsedTimer timer;
705 timer.start();
706
707 KFileItemList itemSubSet;
708 const int count = items.count();
709 itemSubSet.reserve(count);
710 for (int i = 0; i < count; ++i) {
711 KFileItem item = items.at(i);
712 item.determineMimeType();
713 itemSubSet.append(item);
714 if (timer.elapsed() > MaxBlockTimeout) {
715 #ifdef KFILEITEMMODELROLESUPDATER_DEBUG
716 kDebug() << "Maximum time of" << MaxBlockTimeout << "ms exceeded, creating only previews for"
717 << (i + 1) << "items," << (items.count() - (i + 1)) << "will be resolved later";
718 #endif
719 break;
720 }
721 }
722 KIO::PreviewJob* job = new KIO::PreviewJob(itemSubSet, cacheSize, &m_enabledPlugins);
723 job->setIgnoreMaximumSize(items.first().isLocalFile());
724 if (job->ui()) {
725 job->ui()->setWindow(qApp->activeWindow());
726 }
727
728 connect(job, SIGNAL(gotPreview(KFileItem,QPixmap)),
729 this, SLOT(slotGotPreview(KFileItem,QPixmap)));
730 connect(job, SIGNAL(failed(KFileItem)),
731 this, SLOT(slotPreviewFailed(KFileItem)));
732 connect(job, SIGNAL(finished(KJob*)),
733 this, SLOT(slotPreviewJobFinished(KJob*)));
734
735 m_previewJobs.append(job);
736 }
737
738
739 bool KFileItemModelRolesUpdater::hasPendingRoles() const
740 {
741 return !m_pendingVisibleItems.isEmpty() || !m_pendingInvisibleItems.isEmpty();
742 }
743
744 void KFileItemModelRolesUpdater::resolvePendingRoles()
745 {
746 int resolvedCount = 0;
747
748 bool hasSlowRoles = m_previewShown;
749 if (!hasSlowRoles) {
750 QSetIterator<QByteArray> it(m_roles);
751 while (it.hasNext()) {
752 if (m_resolvableRoles.contains(it.next())) {
753 hasSlowRoles = true;
754 break;
755 }
756 }
757 }
758
759 const ResolveHint resolveHint = hasSlowRoles ? ResolveFast : ResolveAll;
760
761 // Resolving the MIME type can be expensive. Assure that not more than MaxBlockTimeout ms are
762 // spend for resolving them synchronously. Usually this is more than enough to determine
763 // all visible items, but there are corner cases where this limit gets easily exceeded.
764 QElapsedTimer timer;
765 timer.start();
766
767 // Resolve the MIME type of all visible items
768 QSet<KFileItem>::iterator visibleIt = m_pendingVisibleItems.begin();
769 while (visibleIt != m_pendingVisibleItems.end()) {
770 const KFileItem item = *visibleIt;
771 if (!hasSlowRoles) {
772 Q_ASSERT(!m_pendingInvisibleItems.contains(item));
773 // All roles will be resolved by applyResolvedRoles()
774 visibleIt = m_pendingVisibleItems.erase(visibleIt);
775 } else {
776 ++visibleIt;
777 }
778 applyResolvedRoles(item, resolveHint);
779 ++resolvedCount;
780
781 if (timer.elapsed() > MaxBlockTimeout) {
782 break;
783 }
784 }
785
786 // Resolve the MIME type of the invisible items at least until the timeout
787 // has been exceeded or the maximum number of items has been reached
788 KFileItemList invisibleItems;
789 if (m_lastVisibleIndex >= 0) {
790 // The visible range is valid, don't care about the order how the MIME
791 // type of invisible items get resolved
792 invisibleItems = m_pendingInvisibleItems.toList();
793 } else {
794 // The visible range is temporary invalid (e.g. happens when loading
795 // a directory) so take care to sort the currently invisible items where
796 // a part will get visible later
797 invisibleItems = sortedItems(m_pendingInvisibleItems);
798 }
799
800 int index = 0;
801 while (resolvedCount < MaxResolveItemsCount && index < invisibleItems.count() && timer.elapsed() <= MaxBlockTimeout) {
802 const KFileItem item = invisibleItems.at(index);
803 applyResolvedRoles(item, resolveHint);
804
805 if (!hasSlowRoles) {
806 // All roles have been resolved already by applyResolvedRoles()
807 m_pendingInvisibleItems.remove(item);
808 }
809 ++index;
810 ++resolvedCount;
811 }
812
813 if (m_previewShown) {
814 KFileItemList items = sortedItems(m_pendingVisibleItems);
815 items += invisibleItems;
816 startPreviewJob(items);
817 } else {
818 QTimer::singleShot(0, this, SLOT(resolveNextPendingRoles()));
819 }
820
821 #ifdef KFILEITEMMODELROLESUPDATER_DEBUG
822 if (timer.elapsed() > MaxBlockTimeout) {
823 kDebug() << "Maximum time of" << MaxBlockTimeout
824 << "ms exceeded, skipping items... Remaining visible:" << m_pendingVisibleItems.count()
825 << "invisible:" << m_pendingInvisibleItems.count();
826 }
827 kDebug() << "[TIME] Resolved pending roles:" << timer.elapsed();
828 #endif
829
830 applySortProgressToModel();
831 }
832
833 void KFileItemModelRolesUpdater::resetPendingRoles()
834 {
835 m_pendingVisibleItems.clear();
836 m_pendingInvisibleItems.clear();
837
838 foreach (KJob* job, m_previewJobs) {
839 job->kill();
840 }
841 Q_ASSERT(m_previewJobs.isEmpty());
842 }
843
844 void KFileItemModelRolesUpdater::sortAndResolveAllRoles()
845 {
846 if (m_paused) {
847 return;
848 }
849
850 resetPendingRoles();
851 Q_ASSERT(m_pendingVisibleItems.isEmpty());
852 Q_ASSERT(m_pendingInvisibleItems.isEmpty());
853
854 if (m_model->count() == 0) {
855 return;
856 }
857
858 // Determine all visible items
859 Q_ASSERT(m_firstVisibleIndex >= 0);
860 for (int i = m_firstVisibleIndex; i <= m_lastVisibleIndex; ++i) {
861 const KFileItem item = m_model->fileItem(i);
862 if (!item.isNull()) {
863 m_pendingVisibleItems.insert(item);
864 }
865 }
866
867 // Determine all invisible items
868 for (int i = 0; i < m_firstVisibleIndex; ++i) {
869 const KFileItem item = m_model->fileItem(i);
870 if (!item.isNull()) {
871 m_pendingInvisibleItems.insert(item);
872 }
873 }
874 const int count = m_model->count();
875 for (int i = m_lastVisibleIndex + 1; i < count; ++i) {
876 const KFileItem item = m_model->fileItem(i);
877 if (!item.isNull()) {
878 m_pendingInvisibleItems.insert(item);
879 }
880 }
881
882 resolvePendingRoles();
883 }
884
885 void KFileItemModelRolesUpdater::sortAndResolvePendingRoles()
886 {
887 Q_ASSERT(!m_paused);
888 if (m_model->count() == 0) {
889 return;
890 }
891
892 // If no valid index range is given assume that all items are visible.
893 // A cleanup will be done later as soon as the index range has been set.
894 const bool hasValidIndexRange = (m_lastVisibleIndex >= 0);
895
896 // Trigger a preview generation of all pending items. Assure that the visible
897 // pending items get generated first.
898
899 // Step 1: Check if any items in m_pendingVisibleItems are not visible any more
900 // and move them to m_pendingInvisibleItems.
901 QSet<KFileItem>::iterator itVisible = m_pendingVisibleItems.begin();
902 while (itVisible != m_pendingVisibleItems.end()) {
903 const KFileItem item = *itVisible;
904 if (item.isNull()) {
905 itVisible = m_pendingVisibleItems.erase(itVisible);
906 continue;
907 }
908
909 const int index = m_model->index(item);
910 if (!hasValidIndexRange || (index >= m_firstVisibleIndex && index <= m_lastVisibleIndex)) {
911 ++itVisible;
912 } else {
913 itVisible = m_pendingVisibleItems.erase(itVisible);
914 m_pendingInvisibleItems.insert(item);
915 }
916 }
917
918 // Step 2: Check if any items in m_pendingInvisibleItems have become visible
919 // and move them to m_pendingVisibleItems.
920 QSet<KFileItem>::iterator itInvisible = m_pendingInvisibleItems.begin();
921 while (itInvisible != m_pendingInvisibleItems.end()) {
922 const KFileItem item = *itInvisible;
923 if (item.isNull()) {
924 itInvisible = m_pendingInvisibleItems.erase(itInvisible);
925 continue;
926 }
927
928 const int index = m_model->index(item);
929 if (!hasValidIndexRange || (index >= m_firstVisibleIndex && index <= m_lastVisibleIndex)) {
930 itInvisible = m_pendingInvisibleItems.erase(itInvisible);
931 m_pendingVisibleItems.insert(item);
932 } else {
933 ++itInvisible;
934 }
935 }
936
937 resolvePendingRoles();
938 }
939
940 void KFileItemModelRolesUpdater::applySortProgressToModel()
941 {
942 if (m_sortingProgress < 0) {
943 return;
944 }
945
946 // Inform the model about the progress of the resolved items,
947 // so that it can give an indication when the sorting has been finished.
948 const int resolvedCount = m_model->count()
949 - m_pendingVisibleItems.count()
950 - m_pendingInvisibleItems.count();
951 if (resolvedCount > 0) {
952 m_model->emitSortProgress(resolvedCount);
953 if (resolvedCount == m_model->count()) {
954 m_sortingProgress = -1;
955 }
956 }
957 }
958
959 void KFileItemModelRolesUpdater::updateSortProgress()
960 {
961 const QByteArray sortRole = m_model->sortRole();
962
963 // Optimization if the sorting is done by type: In case if all MIME-types
964 // are known, the types have been resolved already by KFileItemModel and
965 // no sort-progress feedback is required.
966 const bool showProgress = (sortRole == "type")
967 ? hasUnknownMimeTypes()
968 : m_resolvableRoles.contains(sortRole);
969
970 if (m_sortingProgress >= 0) {
971 // Mark the current sorting as finished
972 m_model->emitSortProgress(m_model->count());
973 }
974 m_sortingProgress = showProgress ? 0 : -1;
975 }
976
977 bool KFileItemModelRolesUpdater::hasUnknownMimeTypes() const
978 {
979 const int count = m_model->count();
980 for (int i = 0; i < count; ++i) {
981 const KFileItem item = m_model->fileItem(i);
982 if (!item.isMimeTypeKnown()) {
983 return true;
984 }
985 }
986
987 return false;
988 }
989
990 bool KFileItemModelRolesUpdater::applyResolvedRoles(const KFileItem& item, ResolveHint hint)
991 {
992 if (item.isNull()) {
993 return false;
994 }
995
996 const bool resolveAll = (hint == ResolveAll);
997
998 bool mimeTypeChanged = false;
999 if (!item.isMimeTypeKnown()) {
1000 item.determineMimeType();
1001 mimeTypeChanged = true;
1002 }
1003
1004 if (mimeTypeChanged || resolveAll || m_clearPreviews) {
1005 const int index = m_model->index(item);
1006 if (index < 0) {
1007 return false;
1008 }
1009
1010 QHash<QByteArray, QVariant> data;
1011 if (resolveAll) {
1012 data = rolesData(item);
1013 }
1014
1015 data.insert("iconName", item.iconName());
1016
1017 if (m_clearPreviews) {
1018 data.insert("iconPixmap", QPixmap());
1019 }
1020
1021 disconnect(m_model, SIGNAL(itemsChanged(KItemRangeList,QSet<QByteArray>)),
1022 this, SLOT(slotItemsChanged(KItemRangeList,QSet<QByteArray>)));
1023 m_model->setData(index, data);
1024 connect(m_model, SIGNAL(itemsChanged(KItemRangeList,QSet<QByteArray>)),
1025 this, SLOT(slotItemsChanged(KItemRangeList,QSet<QByteArray>)));
1026 return true;
1027 }
1028
1029 return false;
1030 }
1031
1032 QHash<QByteArray, QVariant> KFileItemModelRolesUpdater::rolesData(const KFileItem& item) const
1033 {
1034 QHash<QByteArray, QVariant> data;
1035
1036 const bool getSizeRole = m_roles.contains("size");
1037 const bool getIsExpandableRole = m_roles.contains("isExpandable");
1038
1039 if ((getSizeRole || getIsExpandableRole) && item.isDir()) {
1040 if (item.isLocalFile()) {
1041 const QString path = item.localPath();
1042 const int count = subItemsCount(path);
1043 if (getSizeRole) {
1044 data.insert("size", count);
1045 }
1046 if (getIsExpandableRole) {
1047 data.insert("isExpandable", count > 0);
1048 }
1049
1050 if (!m_dirWatcher->contains(path)) {
1051 m_dirWatcher->addDir(path);
1052 m_watchedDirs.insert(path);
1053 }
1054 } else if (getSizeRole) {
1055 data.insert("size", -1); // -1 indicates an unknown number of items
1056 }
1057 }
1058
1059 if (m_roles.contains("type")) {
1060 data.insert("type", item.mimeComment());
1061 }
1062
1063 data.insert("iconOverlays", item.overlays());
1064
1065 #ifdef HAVE_NEPOMUK
1066 if (m_nepomukResourceWatcher) {
1067 const KNepomukRolesProvider& rolesProvider = KNepomukRolesProvider::instance();
1068 Nepomuk2::Resource resource(item.nepomukUri());
1069 QHashIterator<QByteArray, QVariant> it(rolesProvider.roleValues(resource, m_roles));
1070 while (it.hasNext()) {
1071 it.next();
1072 data.insert(it.key(), it.value());
1073 }
1074
1075 QUrl uri = resource.uri();
1076 if (uri.isEmpty()) {
1077 // TODO: Is there another way to explicitly create a resource?
1078 // We need a resource to be able to track it for changes.
1079 resource.setRating(0);
1080 uri = resource.uri();
1081 }
1082 if (!uri.isEmpty() && !m_nepomukUriItems.contains(uri)) {
1083 m_nepomukResourceWatcher->addResource(resource);
1084
1085 if (m_nepomukUriItems.isEmpty()) {
1086 m_nepomukResourceWatcher->start();
1087 }
1088
1089 m_nepomukUriItems.insert(uri, item.url());
1090 }
1091 }
1092 #endif
1093
1094 return data;
1095 }
1096
1097 KFileItemList KFileItemModelRolesUpdater::sortedItems(const QSet<KFileItem>& items) const
1098 {
1099 KFileItemList itemList;
1100 if (items.isEmpty()) {
1101 return itemList;
1102 }
1103
1104 #ifdef KFILEITEMMODELROLESUPDATER_DEBUG
1105 QElapsedTimer timer;
1106 timer.start();
1107 #endif
1108
1109 QList<int> indexes;
1110 indexes.reserve(items.count());
1111
1112 QSetIterator<KFileItem> it(items);
1113 while (it.hasNext()) {
1114 const KFileItem item = it.next();
1115 const int index = m_model->index(item);
1116 if (index >= 0) {
1117 indexes.append(index);
1118 }
1119 }
1120 qSort(indexes);
1121
1122 itemList.reserve(items.count());
1123 foreach (int index, indexes) {
1124 itemList.append(m_model->fileItem(index));
1125 }
1126
1127 #ifdef KFILEITEMMODELROLESUPDATER_DEBUG
1128 kDebug() << "[TIME] Sorting of items:" << timer.elapsed();
1129 #endif
1130 return itemList;
1131 }
1132
1133 int KFileItemModelRolesUpdater::subItemsCount(const QString& path) const
1134 {
1135 const bool countHiddenFiles = m_model->showHiddenFiles();
1136 const bool showFoldersOnly = m_model->showDirectoriesOnly();
1137
1138 #ifdef Q_WS_WIN
1139 QDir dir(path);
1140 QDir::Filters filters = QDir::NoDotAndDotDot | QDir::System;
1141 if (countHiddenFiles) {
1142 filters |= QDir::Hidden;
1143 }
1144 if (showFoldersOnly) {
1145 filters |= QDir::Dirs;
1146 } else {
1147 filters |= QDir::AllEntries;
1148 }
1149 return dir.entryList(filters).count();
1150 #else
1151 // Taken from kdelibs/kio/kio/kdirmodel.cpp
1152 // Copyright (C) 2006 David Faure <faure@kde.org>
1153
1154 int count = -1;
1155 DIR* dir = ::opendir(QFile::encodeName(path));
1156 if (dir) { // krazy:exclude=syscalls
1157 count = 0;
1158 struct dirent *dirEntry = 0;
1159 while ((dirEntry = ::readdir(dir))) {
1160 if (dirEntry->d_name[0] == '.') {
1161 if (dirEntry->d_name[1] == '\0' || !countHiddenFiles) {
1162 // Skip "." or hidden files
1163 continue;
1164 }
1165 if (dirEntry->d_name[1] == '.' && dirEntry->d_name[2] == '\0') {
1166 // Skip ".."
1167 continue;
1168 }
1169 }
1170
1171 // If only directories are counted, consider an unknown file type and links also
1172 // as directory instead of trying to do an expensive stat()
1173 // (see bugs 292642 and 299997).
1174 const bool countEntry = !showFoldersOnly ||
1175 dirEntry->d_type == DT_DIR ||
1176 dirEntry->d_type == DT_LNK ||
1177 dirEntry->d_type == DT_UNKNOWN;
1178 if (countEntry) {
1179 ++count;
1180 }
1181 }
1182 ::closedir(dir);
1183 }
1184 return count;
1185 #endif
1186 }
1187
1188 void KFileItemModelRolesUpdater::updateAllPreviews()
1189 {
1190 if (m_paused) {
1191 m_previewChangedDuringPausing = true;
1192 } else {
1193 sortAndResolveAllRoles();
1194 }
1195 }
1196
1197 #include "kfileitemmodelrolesupdater.moc"