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