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
&& !m_previewShown
) ||
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() && m_model
->count() <= 0) {
294 void KFileItemModelRolesUpdater::slotItemsChanged(const KItemRangeList
& itemRanges
,
295 const QSet
<QByteArray
>& roles
)
297 Q_UNUSED(itemRanges
);
302 void KFileItemModelRolesUpdater::slotGotPreview(const KFileItem
& item
, const QPixmap
& pixmap
)
304 m_pendingVisibleItems
.remove(item
);
305 m_pendingInvisibleItems
.remove(item
);
307 const int index
= m_model
->index(item
);
312 QPixmap scaledPixmap
= pixmap
;
314 const QString mimeType
= item
.mimetype();
315 const int slashIndex
= mimeType
.indexOf(QLatin1Char('/'));
316 const QString mimeTypeGroup
= mimeType
.left(slashIndex
);
317 if (mimeTypeGroup
== QLatin1String("image")) {
318 KPixmapModifier::applyFrame(scaledPixmap
, m_iconSize
);
320 KPixmapModifier::scale(scaledPixmap
, m_iconSize
);
323 QHash
<QByteArray
, QVariant
> data
= rolesData(item
);
324 data
.insert("iconPixmap", scaledPixmap
);
326 disconnect(m_model
, SIGNAL(itemsChanged(KItemRangeList
,QSet
<QByteArray
>)),
327 this, SLOT(slotItemsChanged(KItemRangeList
,QSet
<QByteArray
>)));
328 m_model
->setData(index
, data
);
329 connect(m_model
, SIGNAL(itemsChanged(KItemRangeList
,QSet
<QByteArray
>)),
330 this, SLOT(slotItemsChanged(KItemRangeList
,QSet
<QByteArray
>)));
333 void KFileItemModelRolesUpdater::slotPreviewFailed(const KFileItem
& item
)
335 m_pendingVisibleItems
.remove(item
);
336 m_pendingInvisibleItems
.remove(item
);
338 const bool clearPreviews
= m_clearPreviews
;
339 m_clearPreviews
= true;
340 applyResolvedRoles(item
, ResolveAll
);
341 m_clearPreviews
= clearPreviews
;
344 void KFileItemModelRolesUpdater::slotPreviewJobFinished(KJob
* job
)
346 #ifdef KFILEITEMMODELROLESUPDATER_DEBUG
347 kDebug() << "Preview job finished. Pending visible:" << m_pendingVisibleItems
.count() << "invisible:" << m_pendingInvisibleItems
.count();
350 m_previewJobs
.removeOne(job
);
351 if (!m_previewJobs
.isEmpty() || !hasPendingRoles()) {
355 const KFileItemList visibleItems
= sortedItems(m_pendingVisibleItems
);
356 startPreviewJob(visibleItems
+ m_pendingInvisibleItems
.toList());
359 void KFileItemModelRolesUpdater::resolvePendingRoles()
361 int resolvedCount
= 0;
363 const bool hasSlowRoles
= m_previewShown
364 || m_roles
.contains("size")
365 || m_roles
.contains("type");
366 const ResolveHint resolveHint
= hasSlowRoles
? ResolveFast
: ResolveAll
;
368 // Resolving the MIME type can be expensive. Assure that not more than MaxBlockTimeout ms are
369 // spend for resolving them synchronously. Usually this is more than enough to determine
370 // all visible items, but there are corner cases where this limit gets easily exceeded.
374 // Resolve the MIME type of all visible items
375 QSetIterator
<KFileItem
> visibleIt(m_pendingVisibleItems
);
376 while (visibleIt
.hasNext()) {
377 const KFileItem item
= visibleIt
.next();
378 applyResolvedRoles(item
, resolveHint
);
380 Q_ASSERT(!m_pendingInvisibleItems
.contains(item
));
381 // All roles have been resolved already by applyResolvedRoles()
382 m_pendingVisibleItems
.remove(item
);
386 if (timer
.elapsed() > MaxBlockTimeout
) {
391 // Resolve the MIME type of the invisible items at least until the timeout
392 // has been exceeded or the maximum number of items has been reached
393 KFileItemList invisibleItems
;
394 if (m_lastVisibleIndex
>= 0) {
395 // The visible range is valid, don't care about the order how the MIME
396 // type of invisible items get resolved
397 invisibleItems
= m_pendingInvisibleItems
.toList();
399 // The visible range is temporary invalid (e.g. happens when loading
400 // a directory) so take care to sort the currently invisible items where
401 // a part will get visible later
402 invisibleItems
= sortedItems(m_pendingInvisibleItems
);
406 while (resolvedCount
< MaxResolveItemsCount
&& index
< invisibleItems
.count() && timer
.elapsed() <= MaxBlockTimeout
) {
407 const KFileItem item
= invisibleItems
.at(index
);
408 applyResolvedRoles(item
, resolveHint
);
411 // All roles have been resolved already by applyResolvedRoles()
412 m_pendingInvisibleItems
.remove(item
);
418 if (m_previewShown
) {
419 KFileItemList items
= sortedItems(m_pendingVisibleItems
);
420 items
+= invisibleItems
;
421 startPreviewJob(items
);
423 QTimer::singleShot(0, this, SLOT(resolveNextPendingRoles()));
426 #ifdef KFILEITEMMODELROLESUPDATER_DEBUG
427 if (timer
.elapsed() > MaxBlockTimeout
) {
428 kDebug() << "Maximum time of" << MaxBlockTimeout
429 << "ms exceeded, skipping items... Remaining visible:" << m_pendingVisibleItems
.count()
430 << "invisible:" << m_pendingInvisibleItems
.count();
432 kDebug() << "[TIME] Resolved pending roles:" << timer
.elapsed();
436 void KFileItemModelRolesUpdater::resolveNextPendingRoles()
442 if (m_previewShown
) {
443 // The preview has been turned on since the last run. Skip
444 // resolving further pending roles as this is done as soon
445 // as a preview has been received.
449 int resolvedCount
= 0;
450 bool changed
= false;
451 for (int i
= 0; i
<= 1; ++i
) {
452 QSet
<KFileItem
>& pendingItems
= (i
== 0) ? m_pendingVisibleItems
: m_pendingInvisibleItems
;
453 QSetIterator
<KFileItem
> it(pendingItems
);
454 while (it
.hasNext() && !changed
&& resolvedCount
< MaxResolveItemsCount
) {
455 const KFileItem item
= it
.next();
456 pendingItems
.remove(item
);
457 changed
= applyResolvedRoles(item
, ResolveAll
);
462 if (hasPendingRoles()) {
463 QTimer::singleShot(0, this, SLOT(resolveNextPendingRoles()));
465 m_clearPreviews
= false;
468 #ifdef KFILEITEMMODELROLESUPDATER_DEBUG
469 static int callCount
= 0;
471 if (callCount
% 100 == 0) {
472 kDebug() << "Remaining visible roles to resolve:" << m_pendingVisibleItems
.count()
473 << "invisible:" << m_pendingInvisibleItems
.count();
478 void KFileItemModelRolesUpdater::startPreviewJob(const KFileItemList
& items
)
480 if (items
.count() <= 0 || m_paused
) {
484 // PreviewJob internally caches items always with the size of
485 // 128 x 128 pixels or 256 x 256 pixels. A (slow) downscaling is done
486 // by PreviewJob if a smaller size is requested. For images KFileItemModelRolesUpdater must
487 // do a downscaling anyhow because of the frame, so in this case only the provided
488 // cache sizes are requested.
489 const QSize cacheSize
= (m_iconSize
.width() > 128) || (m_iconSize
.height() > 128)
490 ? QSize(256, 256) : QSize(128, 128);
492 // KIO::filePreview() will request the MIME-type of all passed items, which (in the
493 // worst case) might block the application for several seconds. To prevent such
494 // a blocking the MIME-type of the items will determined until the MaxBlockTimeout
495 // has been reached and only those items will get passed. As soon as the MIME-type
496 // has been resolved once KIO::filePreview() can already access the resolved
497 // MIME-type in a fast way.
500 KFileItemList itemSubSet
;
501 for (int i
= 0; i
< items
.count(); ++i
) {
502 KFileItem item
= items
.at(i
);
503 item
.determineMimeType();
504 itemSubSet
.append(items
.at(i
));
505 if (timer
.elapsed() > MaxBlockTimeout
) {
506 #ifdef KFILEITEMMODELROLESUPDATER_DEBUG
507 kDebug() << "Maximum time of" << MaxBlockTimeout
<< "ms exceeded, creating only previews for"
508 << (i
+ 1) << "items," << (items
.count() - (i
+ 1)) << "will be resolved later";
513 KJob
* job
= KIO::filePreview(itemSubSet
, cacheSize
, &m_enabledPlugins
);
515 connect(job
, SIGNAL(gotPreview(KFileItem
,QPixmap
)),
516 this, SLOT(slotGotPreview(KFileItem
,QPixmap
)));
517 connect(job
, SIGNAL(failed(KFileItem
)),
518 this, SLOT(slotPreviewFailed(KFileItem
)));
519 connect(job
, SIGNAL(finished(KJob
*)),
520 this, SLOT(slotPreviewJobFinished(KJob
*)));
522 m_previewJobs
.append(job
);
526 bool KFileItemModelRolesUpdater::hasPendingRoles() const
528 return !m_pendingVisibleItems
.isEmpty() || !m_pendingInvisibleItems
.isEmpty();
531 void KFileItemModelRolesUpdater::resetPendingRoles()
533 m_pendingVisibleItems
.clear();
534 m_pendingInvisibleItems
.clear();
536 foreach (KJob
* job
, m_previewJobs
) {
539 Q_ASSERT(m_previewJobs
.isEmpty());
542 void KFileItemModelRolesUpdater::triggerPendingRolesResolving(int count
)
544 Q_ASSERT(count
<= m_model
->count());
545 if (count
== m_model
->count()) {
546 // When initially loading a directory a synchronous resolving prevents a minor
547 // flickering when opening directories. This is also fine from a performance point
548 // of view as it is assured in resolvePendingRoles() to never block the event-loop
549 // for more than 200 ms.
550 resolvePendingRoles();
552 // Items have been added. This can be done in several small steps within one loop
553 // because of the sorting and hence may not trigger any expensive operation.
554 m_resolvePendingRolesTimer
->start();
558 void KFileItemModelRolesUpdater::sortAndResolveAllRoles()
565 Q_ASSERT(m_pendingVisibleItems
.isEmpty());
566 Q_ASSERT(m_pendingInvisibleItems
.isEmpty());
568 if (m_model
->count() <= 0) {
572 // Determine all visible items
573 Q_ASSERT(m_firstVisibleIndex
>= 0);
574 for (int i
= m_firstVisibleIndex
; i
<= m_lastVisibleIndex
; ++i
) {
575 const KFileItem item
= m_model
->fileItem(i
);
576 if (!item
.isNull()) {
577 m_pendingVisibleItems
.insert(item
);
581 // Determine all invisible items
582 for (int i
= 0; i
< m_firstVisibleIndex
; ++i
) {
583 const KFileItem item
= m_model
->fileItem(i
);
584 if (!item
.isNull()) {
585 m_pendingInvisibleItems
.insert(item
);
588 for (int i
= m_lastVisibleIndex
+ 1; i
< m_model
->count(); ++i
) {
589 const KFileItem item
= m_model
->fileItem(i
);
590 if (!item
.isNull()) {
591 m_pendingInvisibleItems
.insert(item
);
595 triggerPendingRolesResolving(m_pendingVisibleItems
.count() +
596 m_pendingInvisibleItems
.count());
599 void KFileItemModelRolesUpdater::sortAndResolvePendingRoles()
602 if (m_model
->count() <= 0) {
606 // If no valid index range is given assume that all items are visible.
607 // A cleanup will be done later as soon as the index range has been set.
608 const bool hasValidIndexRange
= (m_lastVisibleIndex
>= 0);
610 // Trigger a preview generation of all pending items. Assure that the visible
611 // pending items get generated first.
612 QSet
<KFileItem
> pendingItems
;
613 pendingItems
+= m_pendingVisibleItems
;
614 pendingItems
+= m_pendingInvisibleItems
;
617 Q_ASSERT(m_pendingVisibleItems
.isEmpty());
618 Q_ASSERT(m_pendingInvisibleItems
.isEmpty());
620 QSetIterator
<KFileItem
> it(pendingItems
);
621 while (it
.hasNext()) {
622 const KFileItem item
= it
.next();
627 const int index
= m_model
->index(item
);
628 if (!hasValidIndexRange
|| (index
>= m_firstVisibleIndex
&& index
<= m_lastVisibleIndex
)) {
629 m_pendingVisibleItems
.insert(item
);
631 m_pendingInvisibleItems
.insert(item
);
635 triggerPendingRolesResolving(m_pendingVisibleItems
.count() +
636 m_pendingInvisibleItems
.count());
639 bool KFileItemModelRolesUpdater::applyResolvedRoles(const KFileItem
& item
, ResolveHint hint
)
641 const bool resolveAll
= (hint
== ResolveAll
);
643 bool mimeTypeChanged
= false;
644 if (!item
.isMimeTypeKnown()) {
645 item
.determineMimeType();
646 mimeTypeChanged
= true;
649 if (mimeTypeChanged
|| resolveAll
|| m_clearPreviews
) {
650 const int index
= m_model
->index(item
);
655 QHash
<QByteArray
, QVariant
> data
;
657 data
= rolesData(item
);
660 if (mimeTypeChanged
|| m_clearPreviews
) {
661 data
.insert("iconName", item
.iconName());
663 if (m_clearPreviews
) {
664 data
.insert("iconPixmap", QString());
667 disconnect(m_model
, SIGNAL(itemsChanged(KItemRangeList
,QSet
<QByteArray
>)),
668 this, SLOT(slotItemsChanged(KItemRangeList
,QSet
<QByteArray
>)));
669 m_model
->setData(index
, data
);
670 connect(m_model
, SIGNAL(itemsChanged(KItemRangeList
,QSet
<QByteArray
>)),
671 this, SLOT(slotItemsChanged(KItemRangeList
,QSet
<QByteArray
>)));
678 QHash
<QByteArray
, QVariant
> KFileItemModelRolesUpdater::rolesData(const KFileItem
& item
) const
680 QHash
<QByteArray
, QVariant
> data
;
682 if (m_roles
.contains("size")) {
683 if (item
.isDir() && item
.isLocalFile()) {
684 const QString path
= item
.localPath();
685 const int count
= subDirectoriesCount(path
);
687 data
.insert("size", KIO::filesize_t(count
));
692 if (m_roles
.contains("type")) {
693 data
.insert("type", item
.mimeComment());
699 KFileItemList
KFileItemModelRolesUpdater::sortedItems(const QSet
<KFileItem
>& items
) const
701 KFileItemList itemList
;
702 if (items
.isEmpty()) {
706 #ifdef KFILEITEMMODELROLESUPDATER_DEBUG
712 indexes
.reserve(items
.count());
714 QSetIterator
<KFileItem
> it(items
);
715 while (it
.hasNext()) {
716 const KFileItem item
= it
.next();
717 const int index
= m_model
->index(item
);
718 indexes
.append(index
);
722 itemList
.reserve(items
.count());
723 foreach (int index
, indexes
) {
724 itemList
.append(m_model
->fileItem(index
));
727 #ifdef KFILEITEMMODELROLESUPDATER_DEBUG
728 kDebug() << "[TIME] Sorting of items:" << timer
.elapsed();
733 int KFileItemModelRolesUpdater::subDirectoriesCount(const QString
& path
)
737 return dir
.entryList(QDir::AllEntries
|QDir::NoDotAndDotDot
|QDir::System
).count();
739 // Taken from kdelibs/kio/kio/kdirmodel.cpp
740 // Copyright (C) 2006 David Faure <faure@kde.org>
743 DIR* dir
= ::opendir(QFile::encodeName(path
));
746 struct dirent
*dirEntry
= 0;
747 while ((dirEntry
= ::readdir(dir
))) { // krazy:exclude=syscalls
748 if (dirEntry
->d_name
[0] == '.') {
749 if (dirEntry
->d_name
[1] == '\0') {
753 if (dirEntry
->d_name
[1] == '.' && dirEntry
->d_name
[2] == '\0') {
766 #include "kfileitemmodelrolesupdater.moc"