1 /***************************************************************************
2 * Copyright (C) 2011 by Peter Penz <peter.penz19@gmail.com> *
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. *
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. *
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 ***************************************************************************/
20 #include "kfileitemmodelrolesupdater.h"
22 #include "kfileitemmodel.h"
23 #include "kpixmapmodifier_p.h"
26 #include <KConfigGroup>
30 #include <KIO/PreviewJob>
33 #include <QElapsedTimer>
36 // Required includes for subDirectoriesCount():
44 #define KFILEITEMMODELROLESUPDATER_DEBUG
47 // Maximum time in ms that the KFileItemModelRolesUpdater
48 // may perform a blocking operation
49 const int MaxBlockTimeout
= 200;
51 // Maximum number of items that will get resolved synchronously.
52 // The value should roughly represent the number of maximum visible
53 // items, as it does not make sense to resolve more items synchronously
54 // and probably reach the MaxBlockTimeout because of invisible items.
55 const int MaxResolveItemsCount
= 100;
58 KFileItemModelRolesUpdater::KFileItemModelRolesUpdater(KFileItemModel
* model
, QObject
* parent
) :
61 m_previewChangedDuringPausing(false),
62 m_iconSizeChangedDuringPausing(false),
63 m_rolesChangedDuringPausing(false),
64 m_previewShown(false),
65 m_clearPreviews(false),
68 m_firstVisibleIndex(0),
69 m_lastVisibleIndex(-1),
72 m_pendingVisibleItems(),
73 m_pendingInvisibleItems(),
75 m_resolvePendingRolesTimer(0)
79 const KConfigGroup
globalConfig(KGlobal::config(), "PreviewSettings");
80 m_enabledPlugins
= globalConfig
.readEntry("Plugins", QStringList()
81 << "directorythumbnail"
85 connect(m_model
, SIGNAL(itemsInserted(KItemRangeList
)),
86 this, SLOT(slotItemsInserted(KItemRangeList
)));
87 connect(m_model
, SIGNAL(itemsRemoved(KItemRangeList
)),
88 this, SLOT(slotItemsRemoved(KItemRangeList
)));
89 connect(m_model
, SIGNAL(itemsChanged(KItemRangeList
,QSet
<QByteArray
>)),
90 this, SLOT(slotItemsChanged(KItemRangeList
,QSet
<QByteArray
>)));
92 // A timer with a minimal timeout is used to merge several triggerPendingRolesResolving() calls
93 // to only one call of resolvePendingRoles().
94 m_resolvePendingRolesTimer
= new QTimer(this);
95 m_resolvePendingRolesTimer
->setInterval(1);
96 m_resolvePendingRolesTimer
->setSingleShot(true);
97 connect(m_resolvePendingRolesTimer
, SIGNAL(timeout()), this, SLOT(resolvePendingRoles()));
100 KFileItemModelRolesUpdater::~KFileItemModelRolesUpdater()
104 void KFileItemModelRolesUpdater::setIconSize(const QSize
& size
)
106 if (size
!= m_iconSize
) {
109 m_iconSizeChangedDuringPausing
= true;
110 } else if (m_previewShown
) {
111 // An icon size change requires the regenerating of
113 sortAndResolveAllRoles();
115 sortAndResolvePendingRoles();
120 QSize
KFileItemModelRolesUpdater::iconSize() const
125 void KFileItemModelRolesUpdater::setVisibleIndexRange(int index
, int count
)
134 if (index
== m_firstVisibleIndex
&& count
== m_lastVisibleIndex
- m_firstVisibleIndex
+ 1) {
135 // The range has not been changed
139 m_firstVisibleIndex
= index
;
140 m_lastVisibleIndex
= qMin(index
+ count
- 1, m_model
->count() - 1);
142 if (hasPendingRoles() && !m_paused
) {
143 sortAndResolvePendingRoles();
147 void KFileItemModelRolesUpdater::setPreviewShown(bool show
)
149 if (show
== m_previewShown
) {
153 m_previewShown
= show
;
155 m_clearPreviews
= true;
159 m_previewChangedDuringPausing
= true;
161 sortAndResolveAllRoles();
165 bool KFileItemModelRolesUpdater::isPreviewShown() const
167 return m_previewShown
;
170 void KFileItemModelRolesUpdater::setEnabledPlugins(const QStringList
& list
)
172 m_enabledPlugins
= list
;
175 void KFileItemModelRolesUpdater::setPaused(bool paused
)
177 if (paused
== m_paused
) {
183 if (hasPendingRoles()) {
184 foreach (KJob
* job
, m_previewJobs
) {
187 Q_ASSERT(m_previewJobs
.isEmpty());
190 const bool resolveAll
= (m_iconSizeChangedDuringPausing
&& m_previewShown
) ||
191 m_previewChangedDuringPausing
||
192 m_rolesChangedDuringPausing
;
194 sortAndResolveAllRoles();
196 sortAndResolvePendingRoles();
199 m_iconSizeChangedDuringPausing
= false;
200 m_previewChangedDuringPausing
= false;
201 m_rolesChangedDuringPausing
= false;
205 void KFileItemModelRolesUpdater::setRoles(const QSet
<QByteArray
>& roles
)
207 if (roles
.count() == m_roles
.count()) {
209 foreach (const QByteArray
& role
, roles
) {
210 if (!m_roles
.contains(role
)) {
223 m_rolesChangedDuringPausing
= true;
225 sortAndResolveAllRoles();
229 QSet
<QByteArray
> KFileItemModelRolesUpdater::roles() const
234 bool KFileItemModelRolesUpdater::isPaused() const
239 QStringList
KFileItemModelRolesUpdater::enabledPlugins() const
241 return m_enabledPlugins
;
244 void KFileItemModelRolesUpdater::slotItemsInserted(const KItemRangeList
& itemRanges
)
246 // If no valid index range is given assume that all items are visible.
247 // A cleanup will be done later as soon as the index range has been set.
248 const bool hasValidIndexRange
= (m_lastVisibleIndex
>= 0);
250 if (hasValidIndexRange
) {
251 // Move all current pending visible items that are not visible anymore
252 // to the pending invisible items.
253 QSetIterator
<KFileItem
> it(m_pendingVisibleItems
);
254 while (it
.hasNext()) {
255 const KFileItem item
= it
.next();
256 const int index
= m_model
->index(item
);
257 if (index
< m_firstVisibleIndex
|| index
> m_lastVisibleIndex
) {
258 m_pendingVisibleItems
.remove(item
);
259 m_pendingInvisibleItems
.insert(item
);
266 foreach (const KItemRange
& range
, itemRanges
) {
267 rangesCount
+= range
.count
;
269 // Add the inserted items to the pending visible and invisible items
270 const int lastIndex
= range
.index
+ range
.count
- 1;
271 for (int i
= range
.index
; i
<= lastIndex
; ++i
) {
272 const KFileItem item
= m_model
->fileItem(i
);
273 if (!hasValidIndexRange
|| (i
>= m_firstVisibleIndex
&& i
<= m_lastVisibleIndex
)) {
274 m_pendingVisibleItems
.insert(item
);
276 m_pendingInvisibleItems
.insert(item
);
281 triggerPendingRolesResolving(rangesCount
);
284 void KFileItemModelRolesUpdater::slotItemsRemoved(const KItemRangeList
& itemRanges
)
286 Q_UNUSED(itemRanges
);
287 m_firstVisibleIndex
= 0;
288 m_lastVisibleIndex
= -1;
289 if (!hasPendingRoles()) {
293 if (m_model
->count() == 0) {
294 // Most probably a directory change is done. Clear all pending items
295 // and also kill all ongoing preview-jobs.
298 // Remove all items from m_pendingVisibleItems and m_pendingInvisibleItems
299 // that are not part of the model anymore.
300 for (int i
= 0; i
<= 1; ++i
) {
301 QSet
<KFileItem
>& pendingItems
= (i
== 0) ? m_pendingVisibleItems
: m_pendingInvisibleItems
;
302 QMutableSetIterator
<KFileItem
> it(pendingItems
);
303 while (it
.hasNext()) {
304 const KFileItem item
= it
.next();
305 if (m_model
->index(item
) < 0) {
306 pendingItems
.remove(item
);
313 void KFileItemModelRolesUpdater::slotItemsChanged(const KItemRangeList
& itemRanges
,
314 const QSet
<QByteArray
>& roles
)
316 Q_UNUSED(itemRanges
);
321 void KFileItemModelRolesUpdater::slotGotPreview(const KFileItem
& item
, const QPixmap
& pixmap
)
323 m_pendingVisibleItems
.remove(item
);
324 m_pendingInvisibleItems
.remove(item
);
326 const int index
= m_model
->index(item
);
331 QPixmap scaledPixmap
= pixmap
;
333 const QString mimeType
= item
.mimetype();
334 const int slashIndex
= mimeType
.indexOf(QLatin1Char('/'));
335 const QString mimeTypeGroup
= mimeType
.left(slashIndex
);
336 if (mimeTypeGroup
== QLatin1String("image")) {
337 KPixmapModifier::applyFrame(scaledPixmap
, m_iconSize
);
339 KPixmapModifier::scale(scaledPixmap
, m_iconSize
);
342 QHash
<QByteArray
, QVariant
> data
= rolesData(item
);
343 data
.insert("iconPixmap", scaledPixmap
);
345 disconnect(m_model
, SIGNAL(itemsChanged(KItemRangeList
,QSet
<QByteArray
>)),
346 this, SLOT(slotItemsChanged(KItemRangeList
,QSet
<QByteArray
>)));
347 m_model
->setData(index
, data
);
348 connect(m_model
, SIGNAL(itemsChanged(KItemRangeList
,QSet
<QByteArray
>)),
349 this, SLOT(slotItemsChanged(KItemRangeList
,QSet
<QByteArray
>)));
352 void KFileItemModelRolesUpdater::slotPreviewFailed(const KFileItem
& item
)
354 m_pendingVisibleItems
.remove(item
);
355 m_pendingInvisibleItems
.remove(item
);
357 const bool clearPreviews
= m_clearPreviews
;
358 m_clearPreviews
= true;
359 applyResolvedRoles(item
, ResolveAll
);
360 m_clearPreviews
= clearPreviews
;
363 void KFileItemModelRolesUpdater::slotPreviewJobFinished(KJob
* job
)
365 #ifdef KFILEITEMMODELROLESUPDATER_DEBUG
366 kDebug() << "Preview job finished. Pending visible:" << m_pendingVisibleItems
.count() << "invisible:" << m_pendingInvisibleItems
.count();
369 m_previewJobs
.removeOne(job
);
370 if (!m_previewJobs
.isEmpty() || !hasPendingRoles()) {
374 const KFileItemList visibleItems
= sortedItems(m_pendingVisibleItems
);
375 startPreviewJob(visibleItems
+ m_pendingInvisibleItems
.toList());
378 void KFileItemModelRolesUpdater::resolvePendingRoles()
380 int resolvedCount
= 0;
382 const bool hasSlowRoles
= m_previewShown
383 || m_roles
.contains("size")
384 || m_roles
.contains("type");
385 const ResolveHint resolveHint
= hasSlowRoles
? ResolveFast
: ResolveAll
;
387 // Resolving the MIME type can be expensive. Assure that not more than MaxBlockTimeout ms are
388 // spend for resolving them synchronously. Usually this is more than enough to determine
389 // all visible items, but there are corner cases where this limit gets easily exceeded.
393 // Resolve the MIME type of all visible items
394 QSetIterator
<KFileItem
> visibleIt(m_pendingVisibleItems
);
395 while (visibleIt
.hasNext()) {
396 const KFileItem item
= visibleIt
.next();
397 applyResolvedRoles(item
, resolveHint
);
399 Q_ASSERT(!m_pendingInvisibleItems
.contains(item
));
400 // All roles have been resolved already by applyResolvedRoles()
401 m_pendingVisibleItems
.remove(item
);
405 if (timer
.elapsed() > MaxBlockTimeout
) {
410 // Resolve the MIME type of the invisible items at least until the timeout
411 // has been exceeded or the maximum number of items has been reached
412 KFileItemList invisibleItems
;
413 if (m_lastVisibleIndex
>= 0) {
414 // The visible range is valid, don't care about the order how the MIME
415 // type of invisible items get resolved
416 invisibleItems
= m_pendingInvisibleItems
.toList();
418 // The visible range is temporary invalid (e.g. happens when loading
419 // a directory) so take care to sort the currently invisible items where
420 // a part will get visible later
421 invisibleItems
= sortedItems(m_pendingInvisibleItems
);
425 while (resolvedCount
< MaxResolveItemsCount
&& index
< invisibleItems
.count() && timer
.elapsed() <= MaxBlockTimeout
) {
426 const KFileItem item
= invisibleItems
.at(index
);
427 applyResolvedRoles(item
, resolveHint
);
430 // All roles have been resolved already by applyResolvedRoles()
431 m_pendingInvisibleItems
.remove(item
);
437 if (m_previewShown
) {
438 KFileItemList items
= sortedItems(m_pendingVisibleItems
);
439 items
+= invisibleItems
;
440 startPreviewJob(items
);
442 QTimer::singleShot(0, this, SLOT(resolveNextPendingRoles()));
445 #ifdef KFILEITEMMODELROLESUPDATER_DEBUG
446 if (timer
.elapsed() > MaxBlockTimeout
) {
447 kDebug() << "Maximum time of" << MaxBlockTimeout
448 << "ms exceeded, skipping items... Remaining visible:" << m_pendingVisibleItems
.count()
449 << "invisible:" << m_pendingInvisibleItems
.count();
451 kDebug() << "[TIME] Resolved pending roles:" << timer
.elapsed();
455 void KFileItemModelRolesUpdater::resolveNextPendingRoles()
461 if (m_previewShown
) {
462 // The preview has been turned on since the last run. Skip
463 // resolving further pending roles as this is done as soon
464 // as a preview has been received.
468 int resolvedCount
= 0;
469 bool changed
= false;
470 for (int i
= 0; i
<= 1; ++i
) {
471 QSet
<KFileItem
>& pendingItems
= (i
== 0) ? m_pendingVisibleItems
: m_pendingInvisibleItems
;
472 QSetIterator
<KFileItem
> it(pendingItems
);
473 while (it
.hasNext() && !changed
&& resolvedCount
< MaxResolveItemsCount
) {
474 const KFileItem item
= it
.next();
475 pendingItems
.remove(item
);
476 changed
= applyResolvedRoles(item
, ResolveAll
);
481 if (hasPendingRoles()) {
482 QTimer::singleShot(0, this, SLOT(resolveNextPendingRoles()));
484 m_clearPreviews
= false;
487 #ifdef KFILEITEMMODELROLESUPDATER_DEBUG
488 static int callCount
= 0;
490 if (callCount
% 100 == 0) {
491 kDebug() << "Remaining visible roles to resolve:" << m_pendingVisibleItems
.count()
492 << "invisible:" << m_pendingInvisibleItems
.count();
497 void KFileItemModelRolesUpdater::startPreviewJob(const KFileItemList
& items
)
499 if (items
.isEmpty() || m_paused
) {
503 // PreviewJob internally caches items always with the size of
504 // 128 x 128 pixels or 256 x 256 pixels. A (slow) downscaling is done
505 // by PreviewJob if a smaller size is requested. For images KFileItemModelRolesUpdater must
506 // do a downscaling anyhow because of the frame, so in this case only the provided
507 // cache sizes are requested.
508 const QSize cacheSize
= (m_iconSize
.width() > 128) || (m_iconSize
.height() > 128)
509 ? QSize(256, 256) : QSize(128, 128);
511 // KIO::filePreview() will request the MIME-type of all passed items, which (in the
512 // worst case) might block the application for several seconds. To prevent such
513 // a blocking the MIME-type of the items will determined until the MaxBlockTimeout
514 // has been reached and only those items will get passed. As soon as the MIME-type
515 // has been resolved once KIO::filePreview() can already access the resolved
516 // MIME-type in a fast way.
519 KFileItemList itemSubSet
;
520 for (int i
= 0; i
< items
.count(); ++i
) {
521 KFileItem item
= items
.at(i
);
522 item
.determineMimeType();
523 itemSubSet
.append(items
.at(i
));
524 if (timer
.elapsed() > MaxBlockTimeout
) {
525 #ifdef KFILEITEMMODELROLESUPDATER_DEBUG
526 kDebug() << "Maximum time of" << MaxBlockTimeout
<< "ms exceeded, creating only previews for"
527 << (i
+ 1) << "items," << (items
.count() - (i
+ 1)) << "will be resolved later";
532 KJob
* job
= KIO::filePreview(itemSubSet
, cacheSize
, &m_enabledPlugins
);
534 connect(job
, SIGNAL(gotPreview(KFileItem
,QPixmap
)),
535 this, SLOT(slotGotPreview(KFileItem
,QPixmap
)));
536 connect(job
, SIGNAL(failed(KFileItem
)),
537 this, SLOT(slotPreviewFailed(KFileItem
)));
538 connect(job
, SIGNAL(finished(KJob
*)),
539 this, SLOT(slotPreviewJobFinished(KJob
*)));
541 m_previewJobs
.append(job
);
545 bool KFileItemModelRolesUpdater::hasPendingRoles() const
547 return !m_pendingVisibleItems
.isEmpty() || !m_pendingInvisibleItems
.isEmpty();
550 void KFileItemModelRolesUpdater::resetPendingRoles()
552 m_pendingVisibleItems
.clear();
553 m_pendingInvisibleItems
.clear();
555 foreach (KJob
* job
, m_previewJobs
) {
558 Q_ASSERT(m_previewJobs
.isEmpty());
561 void KFileItemModelRolesUpdater::triggerPendingRolesResolving(int count
)
563 if (count
== m_model
->count()) {
564 // When initially loading a directory a synchronous resolving prevents a minor
565 // flickering when opening directories. This is also fine from a performance point
566 // of view as it is assured in resolvePendingRoles() to never block the event-loop
567 // for more than 200 ms.
568 resolvePendingRoles();
570 // Items have been added. This can be done in several small steps within one loop
571 // because of the sorting and hence may not trigger any expensive operation.
572 m_resolvePendingRolesTimer
->start();
576 void KFileItemModelRolesUpdater::sortAndResolveAllRoles()
583 Q_ASSERT(m_pendingVisibleItems
.isEmpty());
584 Q_ASSERT(m_pendingInvisibleItems
.isEmpty());
586 if (m_model
->count() == 0) {
590 // Determine all visible items
591 Q_ASSERT(m_firstVisibleIndex
>= 0);
592 for (int i
= m_firstVisibleIndex
; i
<= m_lastVisibleIndex
; ++i
) {
593 const KFileItem item
= m_model
->fileItem(i
);
594 if (!item
.isNull()) {
595 m_pendingVisibleItems
.insert(item
);
599 // Determine all invisible items
600 for (int i
= 0; i
< m_firstVisibleIndex
; ++i
) {
601 const KFileItem item
= m_model
->fileItem(i
);
602 if (!item
.isNull()) {
603 m_pendingInvisibleItems
.insert(item
);
606 for (int i
= m_lastVisibleIndex
+ 1; i
< m_model
->count(); ++i
) {
607 const KFileItem item
= m_model
->fileItem(i
);
608 if (!item
.isNull()) {
609 m_pendingInvisibleItems
.insert(item
);
613 triggerPendingRolesResolving(m_pendingVisibleItems
.count() +
614 m_pendingInvisibleItems
.count());
617 void KFileItemModelRolesUpdater::sortAndResolvePendingRoles()
620 if (m_model
->count() == 0) {
624 // If no valid index range is given assume that all items are visible.
625 // A cleanup will be done later as soon as the index range has been set.
626 const bool hasValidIndexRange
= (m_lastVisibleIndex
>= 0);
628 // Trigger a preview generation of all pending items. Assure that the visible
629 // pending items get generated first.
630 QSet
<KFileItem
> pendingItems
;
631 pendingItems
+= m_pendingVisibleItems
;
632 pendingItems
+= m_pendingInvisibleItems
;
635 Q_ASSERT(m_pendingVisibleItems
.isEmpty());
636 Q_ASSERT(m_pendingInvisibleItems
.isEmpty());
638 QSetIterator
<KFileItem
> it(pendingItems
);
639 while (it
.hasNext()) {
640 const KFileItem item
= it
.next();
645 const int index
= m_model
->index(item
);
646 if (!hasValidIndexRange
|| (index
>= m_firstVisibleIndex
&& index
<= m_lastVisibleIndex
)) {
647 m_pendingVisibleItems
.insert(item
);
649 m_pendingInvisibleItems
.insert(item
);
653 triggerPendingRolesResolving(m_pendingVisibleItems
.count() +
654 m_pendingInvisibleItems
.count());
657 bool KFileItemModelRolesUpdater::applyResolvedRoles(const KFileItem
& item
, ResolveHint hint
)
663 const bool resolveAll
= (hint
== ResolveAll
);
665 bool mimeTypeChanged
= false;
666 if (!item
.isMimeTypeKnown()) {
667 item
.determineMimeType();
668 mimeTypeChanged
= true;
671 if (mimeTypeChanged
|| resolveAll
|| m_clearPreviews
) {
672 const int index
= m_model
->index(item
);
677 QHash
<QByteArray
, QVariant
> data
;
679 data
= rolesData(item
);
682 data
.insert("iconName", item
.iconName());
684 if (m_clearPreviews
) {
685 data
.insert("iconPixmap", QString());
688 disconnect(m_model
, SIGNAL(itemsChanged(KItemRangeList
,QSet
<QByteArray
>)),
689 this, SLOT(slotItemsChanged(KItemRangeList
,QSet
<QByteArray
>)));
690 m_model
->setData(index
, data
);
691 connect(m_model
, SIGNAL(itemsChanged(KItemRangeList
,QSet
<QByteArray
>)),
692 this, SLOT(slotItemsChanged(KItemRangeList
,QSet
<QByteArray
>)));
699 QHash
<QByteArray
, QVariant
> KFileItemModelRolesUpdater::rolesData(const KFileItem
& item
) const
701 QHash
<QByteArray
, QVariant
> data
;
703 if (m_roles
.contains("size")) {
704 if (item
.isDir() && item
.isLocalFile()) {
705 const QString path
= item
.localPath();
706 const int count
= subDirectoriesCount(path
);
708 data
.insert("size", KIO::filesize_t(count
));
713 if (m_roles
.contains("type")) {
714 data
.insert("type", item
.mimeComment());
720 KFileItemList
KFileItemModelRolesUpdater::sortedItems(const QSet
<KFileItem
>& items
) const
722 KFileItemList itemList
;
723 if (items
.isEmpty()) {
727 #ifdef KFILEITEMMODELROLESUPDATER_DEBUG
733 indexes
.reserve(items
.count());
735 QSetIterator
<KFileItem
> it(items
);
736 while (it
.hasNext()) {
737 const KFileItem item
= it
.next();
738 const int index
= m_model
->index(item
);
740 indexes
.append(index
);
745 itemList
.reserve(items
.count());
746 foreach (int index
, indexes
) {
747 itemList
.append(m_model
->fileItem(index
));
750 #ifdef KFILEITEMMODELROLESUPDATER_DEBUG
751 kDebug() << "[TIME] Sorting of items:" << timer
.elapsed();
756 int KFileItemModelRolesUpdater::subDirectoriesCount(const QString
& path
)
760 return dir
.entryList(QDir::AllEntries
|QDir::NoDotAndDotDot
|QDir::System
).count();
762 // Taken from kdelibs/kio/kio/kdirmodel.cpp
763 // Copyright (C) 2006 David Faure <faure@kde.org>
766 DIR* dir
= ::opendir(QFile::encodeName(path
));
769 struct dirent
*dirEntry
= 0;
770 while ((dirEntry
= ::readdir(dir
))) { // krazy:exclude=syscalls
771 if (dirEntry
->d_name
[0] == '.') {
772 if (dirEntry
->d_name
[1] == '\0') {
776 if (dirEntry
->d_name
[1] == '.' && dirEntry
->d_name
[2] == '\0') {
789 #include "kfileitemmodelrolesupdater.moc"