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