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