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