]> cloud.milkyroute.net Git - dolphin.git/blob - src/kitemviews/kfileitemmodelrolesupdater.cpp
11d01b2cf3694793345a9052eefbfbad9165c43b
[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 <KSharedConfig>
27 #include <KFileItem>
28 #include <KIconLoader>
29 #include <KJobWidgets>
30 #include <KIO/JobUiDelegate>
31 #include <KIO/PreviewJob>
32 #include <KPluginLoader>
33 #include <KOverlayIconPlugin>
34
35 #include "private/kpixmapmodifier.h"
36 #include "private/kdirectorycontentscounter.h"
37
38 #include <QApplication>
39 #include <QPainter>
40 #include <QPixmap>
41 #include <QElapsedTimer>
42 #include <QTimer>
43
44 #include <algorithm>
45
46 #ifdef HAVE_BALOO
47 #include "private/kbaloorolesprovider.h"
48 #include <Baloo/File>
49 #include <Baloo/FileMonitor>
50 #endif
51
52
53 // #define KFILEITEMMODELROLESUPDATER_DEBUG
54
55 namespace {
56 // Maximum time in ms that the KFileItemModelRolesUpdater
57 // may perform a blocking operation
58 const int MaxBlockTimeout = 200;
59
60 // If the number of items is smaller than ResolveAllItemsLimit,
61 // the roles of all items will be resolved.
62 const int ResolveAllItemsLimit = 500;
63
64 // Not only the visible area, but up to ReadAheadPages before and after
65 // this area will be resolved.
66 const int ReadAheadPages = 5;
67 }
68
69 KFileItemModelRolesUpdater::KFileItemModelRolesUpdater(KFileItemModel* model, QObject* parent) :
70 QObject(parent),
71 m_state(Idle),
72 m_previewChangedDuringPausing(false),
73 m_iconSizeChangedDuringPausing(false),
74 m_rolesChangedDuringPausing(false),
75 m_previewShown(false),
76 m_enlargeSmallPreviews(true),
77 m_clearPreviews(false),
78 m_finishedItems(),
79 m_model(model),
80 m_iconSize(),
81 m_firstVisibleIndex(0),
82 m_lastVisibleIndex(-1),
83 m_maximumVisibleItems(50),
84 m_roles(),
85 m_resolvableRoles(),
86 m_enabledPlugins(),
87 m_pendingSortRoleItems(),
88 m_pendingIndexes(),
89 m_pendingPreviewItems(),
90 m_previewJob(),
91 m_recentlyChangedItemsTimer(0),
92 m_recentlyChangedItems(),
93 m_changedItems(),
94 m_directoryContentsCounter(0)
95 #ifdef HAVE_BALOO
96 , m_balooFileMonitor(0)
97 #endif
98 {
99 Q_ASSERT(model);
100
101 const KConfigGroup globalConfig(KSharedConfig::openConfig(), "PreviewSettings");
102 m_enabledPlugins = globalConfig.readEntry("Plugins", KIO::PreviewJob::defaultPlugins());
103
104 connect(m_model, &KFileItemModel::itemsInserted,
105 this, &KFileItemModelRolesUpdater::slotItemsInserted);
106 connect(m_model, &KFileItemModel::itemsRemoved,
107 this, &KFileItemModelRolesUpdater::slotItemsRemoved);
108 connect(m_model, &KFileItemModel::itemsChanged,
109 this, &KFileItemModelRolesUpdater::slotItemsChanged);
110 connect(m_model, &KFileItemModel::itemsMoved,
111 this, &KFileItemModelRolesUpdater::slotItemsMoved);
112 connect(m_model, &KFileItemModel::sortRoleChanged,
113 this, &KFileItemModelRolesUpdater::slotSortRoleChanged);
114
115 // Use a timer to prevent that each call of slotItemsChanged() results in a synchronous
116 // resolving of the roles. Postpone the resolving until no update has been done for 1 second.
117 m_recentlyChangedItemsTimer = new QTimer(this);
118 m_recentlyChangedItemsTimer->setInterval(1000);
119 m_recentlyChangedItemsTimer->setSingleShot(true);
120 connect(m_recentlyChangedItemsTimer, &QTimer::timeout, this, &KFileItemModelRolesUpdater::resolveRecentlyChangedItems);
121
122 m_resolvableRoles.insert("size");
123 m_resolvableRoles.insert("type");
124 m_resolvableRoles.insert("isExpandable");
125 #ifdef HAVE_BALOO
126 m_resolvableRoles += KBalooRolesProvider::instance().roles();
127 #endif
128
129 m_directoryContentsCounter = new KDirectoryContentsCounter(m_model, this);
130 connect(m_directoryContentsCounter, &KDirectoryContentsCounter::result,
131 this, &KFileItemModelRolesUpdater::slotDirectoryContentsCountReceived);
132
133 auto plugins = KPluginLoader::instantiatePlugins(QStringLiteral("kf5/overlayicon"), nullptr, qApp);
134 foreach (QObject *it, plugins) {
135 auto plugin = qobject_cast<KOverlayIconPlugin*>(it);
136 if (plugin) {
137 m_overlayIconsPlugin.append(plugin);
138 connect(plugin, &KOverlayIconPlugin::overlaysChanged, this, &KFileItemModelRolesUpdater::slotOverlaysChanged);
139 } else {
140 // not our/valid plugin, so delete the created object
141 it->deleteLater();
142 }
143 }
144 }
145
146 KFileItemModelRolesUpdater::~KFileItemModelRolesUpdater()
147 {
148 killPreviewJob();
149 }
150
151 void KFileItemModelRolesUpdater::setIconSize(const QSize& size)
152 {
153 if (size != m_iconSize) {
154 m_iconSize = size;
155 if (m_state == Paused) {
156 m_iconSizeChangedDuringPausing = true;
157 } else if (m_previewShown) {
158 // An icon size change requires the regenerating of
159 // all previews
160 m_finishedItems.clear();
161 startUpdating();
162 }
163 }
164 }
165
166 QSize KFileItemModelRolesUpdater::iconSize() const
167 {
168 return m_iconSize;
169 }
170
171 void KFileItemModelRolesUpdater::setVisibleIndexRange(int index, int count)
172 {
173 if (index < 0) {
174 index = 0;
175 }
176 if (count < 0) {
177 count = 0;
178 }
179
180 if (index == m_firstVisibleIndex && count == m_lastVisibleIndex - m_firstVisibleIndex + 1) {
181 // The range has not been changed
182 return;
183 }
184
185 m_firstVisibleIndex = index;
186 m_lastVisibleIndex = qMin(index + count - 1, m_model->count() - 1);
187
188 startUpdating();
189 }
190
191 void KFileItemModelRolesUpdater::setMaximumVisibleItems(int count)
192 {
193 m_maximumVisibleItems = count;
194 }
195
196 void KFileItemModelRolesUpdater::setPreviewsShown(bool show)
197 {
198 if (show == m_previewShown) {
199 return;
200 }
201
202 m_previewShown = show;
203 if (!show) {
204 m_clearPreviews = true;
205 }
206
207 updateAllPreviews();
208 }
209
210 bool KFileItemModelRolesUpdater::previewsShown() const
211 {
212 return m_previewShown;
213 }
214
215 void KFileItemModelRolesUpdater::setEnlargeSmallPreviews(bool enlarge)
216 {
217 if (enlarge != m_enlargeSmallPreviews) {
218 m_enlargeSmallPreviews = enlarge;
219 if (m_previewShown) {
220 updateAllPreviews();
221 }
222 }
223 }
224
225 bool KFileItemModelRolesUpdater::enlargeSmallPreviews() const
226 {
227 return m_enlargeSmallPreviews;
228 }
229
230 void KFileItemModelRolesUpdater::setEnabledPlugins(const QStringList& list)
231 {
232 if (m_enabledPlugins != list) {
233 m_enabledPlugins = list;
234 if (m_previewShown) {
235 updateAllPreviews();
236 }
237 }
238 }
239
240 void KFileItemModelRolesUpdater::setPaused(bool paused)
241 {
242 if (paused == (m_state == Paused)) {
243 return;
244 }
245
246 if (paused) {
247 m_state = Paused;
248 killPreviewJob();
249 } else {
250 const bool updatePreviews = (m_iconSizeChangedDuringPausing && m_previewShown) ||
251 m_previewChangedDuringPausing;
252 const bool resolveAll = updatePreviews || m_rolesChangedDuringPausing;
253 if (resolveAll) {
254 m_finishedItems.clear();
255 }
256
257 m_iconSizeChangedDuringPausing = false;
258 m_previewChangedDuringPausing = false;
259 m_rolesChangedDuringPausing = false;
260
261 if (!m_pendingSortRoleItems.isEmpty()) {
262 m_state = ResolvingSortRole;
263 resolveNextSortRole();
264 } else {
265 m_state = Idle;
266 }
267
268 startUpdating();
269 }
270 }
271
272 void KFileItemModelRolesUpdater::setRoles(const QSet<QByteArray>& roles)
273 {
274 if (m_roles != roles) {
275 m_roles = roles;
276
277 #ifdef HAVE_BALOO
278 // Check whether there is at least one role that must be resolved
279 // with the help of Baloo. If this is the case, a (quite expensive)
280 // resolving will be done in KFileItemModelRolesUpdater::rolesData() and
281 // the role gets watched for changes.
282 const KBalooRolesProvider& rolesProvider = KBalooRolesProvider::instance();
283 bool hasBalooRole = false;
284 QSetIterator<QByteArray> it(roles);
285 while (it.hasNext()) {
286 const QByteArray& role = it.next();
287 if (rolesProvider.roles().contains(role)) {
288 hasBalooRole = true;
289 break;
290 }
291 }
292
293 if (hasBalooRole && m_balooConfig.fileIndexingEnabled() && !m_balooFileMonitor) {
294 m_balooFileMonitor = new Baloo::FileMonitor(this);
295 connect(m_balooFileMonitor, &Baloo::FileMonitor::fileMetaDataChanged,
296 this, &KFileItemModelRolesUpdater::applyChangedBalooRoles);
297 } else if (!hasBalooRole && m_balooFileMonitor) {
298 delete m_balooFileMonitor;
299 m_balooFileMonitor = 0;
300 }
301 #endif
302
303 if (m_state == Paused) {
304 m_rolesChangedDuringPausing = true;
305 } else {
306 startUpdating();
307 }
308 }
309 }
310
311 QSet<QByteArray> KFileItemModelRolesUpdater::roles() const
312 {
313 return m_roles;
314 }
315
316 bool KFileItemModelRolesUpdater::isPaused() const
317 {
318 return m_state == Paused;
319 }
320
321 QStringList KFileItemModelRolesUpdater::enabledPlugins() const
322 {
323 return m_enabledPlugins;
324 }
325
326 void KFileItemModelRolesUpdater::slotItemsInserted(const KItemRangeList& itemRanges)
327 {
328 QElapsedTimer timer;
329 timer.start();
330
331 // Determine the sort role synchronously for as many items as possible.
332 if (m_resolvableRoles.contains(m_model->sortRole())) {
333 int insertedCount = 0;
334 foreach (const KItemRange& range, itemRanges) {
335 const int lastIndex = insertedCount + range.index + range.count - 1;
336 for (int i = insertedCount + range.index; i <= lastIndex; ++i) {
337 if (timer.elapsed() < MaxBlockTimeout) {
338 applySortRole(i);
339 } else {
340 m_pendingSortRoleItems.insert(m_model->fileItem(i));
341 }
342 }
343 insertedCount += range.count;
344 }
345
346 applySortProgressToModel();
347
348 // If there are still items whose sort role is unknown, check if the
349 // asynchronous determination of the sort role is already in progress,
350 // and start it if that is not the case.
351 if (!m_pendingSortRoleItems.isEmpty() && m_state != ResolvingSortRole) {
352 killPreviewJob();
353 m_state = ResolvingSortRole;
354 resolveNextSortRole();
355 }
356 }
357
358 startUpdating();
359 }
360
361 void KFileItemModelRolesUpdater::slotItemsRemoved(const KItemRangeList& itemRanges)
362 {
363 Q_UNUSED(itemRanges);
364
365 const bool allItemsRemoved = (m_model->count() == 0);
366
367 #ifdef HAVE_BALOO
368 if (m_balooFileMonitor) {
369 // Don't let the FileWatcher watch for removed items
370 if (allItemsRemoved) {
371 m_balooFileMonitor->clear();
372 } else {
373 QStringList newFileList;
374 foreach (const QString& file, m_balooFileMonitor->files()) {
375 if (m_model->index(QUrl::fromLocalFile(file)) >= 0) {
376 newFileList.append(file);
377 }
378 }
379 m_balooFileMonitor->setFiles(newFileList);
380 }
381 }
382 #endif
383
384 if (allItemsRemoved) {
385 m_state = Idle;
386
387 m_finishedItems.clear();
388 m_pendingSortRoleItems.clear();
389 m_pendingIndexes.clear();
390 m_pendingPreviewItems.clear();
391 m_recentlyChangedItems.clear();
392 m_recentlyChangedItemsTimer->stop();
393 m_changedItems.clear();
394
395 killPreviewJob();
396 } else {
397 // Only remove the items from m_finishedItems. They will be removed
398 // from the other sets later on.
399 QSet<KFileItem>::iterator it = m_finishedItems.begin();
400 while (it != m_finishedItems.end()) {
401 if (m_model->index(*it) < 0) {
402 it = m_finishedItems.erase(it);
403 } else {
404 ++it;
405 }
406 }
407
408 // The visible items might have changed.
409 startUpdating();
410 }
411 }
412
413 void KFileItemModelRolesUpdater::slotItemsMoved(const KItemRange& itemRange, QList<int> movedToIndexes)
414 {
415 Q_UNUSED(itemRange);
416 Q_UNUSED(movedToIndexes);
417
418 // The visible items might have changed.
419 startUpdating();
420 }
421
422 void KFileItemModelRolesUpdater::slotItemsChanged(const KItemRangeList& itemRanges,
423 const QSet<QByteArray>& roles)
424 {
425 Q_UNUSED(roles);
426
427 // Find out if slotItemsChanged() has been done recently. If that is the
428 // case, resolving the roles is postponed until a timer has exceeded
429 // to prevent expensive repeated updates if files are updated frequently.
430 const bool itemsChangedRecently = m_recentlyChangedItemsTimer->isActive();
431
432 QSet<KFileItem>& targetSet = itemsChangedRecently ? m_recentlyChangedItems : m_changedItems;
433
434 foreach (const KItemRange& itemRange, itemRanges) {
435 int index = itemRange.index;
436 for (int count = itemRange.count; count > 0; --count) {
437 const KFileItem item = m_model->fileItem(index);
438 targetSet.insert(item);
439 ++index;
440 }
441 }
442
443 m_recentlyChangedItemsTimer->start();
444
445 if (!itemsChangedRecently) {
446 updateChangedItems();
447 }
448 }
449
450 void KFileItemModelRolesUpdater::slotSortRoleChanged(const QByteArray& current,
451 const QByteArray& previous)
452 {
453 Q_UNUSED(current);
454 Q_UNUSED(previous);
455
456 if (m_resolvableRoles.contains(current)) {
457 m_pendingSortRoleItems.clear();
458 m_finishedItems.clear();
459
460 const int count = m_model->count();
461 QElapsedTimer timer;
462 timer.start();
463
464 // Determine the sort role synchronously for as many items as possible.
465 for (int index = 0; index < count; ++index) {
466 if (timer.elapsed() < MaxBlockTimeout) {
467 applySortRole(index);
468 } else {
469 m_pendingSortRoleItems.insert(m_model->fileItem(index));
470 }
471 }
472
473 applySortProgressToModel();
474
475 if (!m_pendingSortRoleItems.isEmpty()) {
476 // Trigger the asynchronous determination of the sort role.
477 killPreviewJob();
478 m_state = ResolvingSortRole;
479 resolveNextSortRole();
480 }
481 } else {
482 m_state = Idle;
483 m_pendingSortRoleItems.clear();
484 applySortProgressToModel();
485 }
486 }
487
488 void KFileItemModelRolesUpdater::slotGotPreview(const KFileItem& item, const QPixmap& pixmap)
489 {
490 if (m_state != PreviewJobRunning) {
491 return;
492 }
493
494 m_changedItems.remove(item);
495
496 const int index = m_model->index(item);
497 if (index < 0) {
498 return;
499 }
500
501 QPixmap scaledPixmap = pixmap;
502
503 const QString mimeType = item.mimetype();
504 const int slashIndex = mimeType.indexOf(QLatin1Char('/'));
505 const bool isFontPreview = mimeType.right(slashIndex).contains(QLatin1String("font"));
506 const bool isFolderPreview = item.isDir();
507 const bool isWindowsExePreview = mimeType == QLatin1String("application/x-ms-dos-executable") ||
508 mimeType == QLatin1String("application/x-msdownload");
509
510 if (!isFolderPreview && !isFontPreview && !isWindowsExePreview) {
511 if (m_enlargeSmallPreviews) {
512 KPixmapModifier::applyFrame(scaledPixmap, m_iconSize);
513 } else {
514 // Assure that small previews don't get enlarged. Instead they
515 // should be shown centered within the frame.
516 const QSize contentSize = KPixmapModifier::sizeInsideFrame(m_iconSize);
517 const bool enlargingRequired = scaledPixmap.width() < contentSize.width() &&
518 scaledPixmap.height() < contentSize.height();
519 if (enlargingRequired) {
520 QSize frameSize = scaledPixmap.size() / scaledPixmap.devicePixelRatio();
521 frameSize.scale(m_iconSize, Qt::KeepAspectRatio);
522
523 QPixmap largeFrame(frameSize);
524 largeFrame.fill(Qt::transparent);
525
526 KPixmapModifier::applyFrame(largeFrame, frameSize);
527
528 QPainter painter(&largeFrame);
529 painter.drawPixmap((largeFrame.width() - scaledPixmap.width() / scaledPixmap.devicePixelRatio()) / 2,
530 (largeFrame.height() - scaledPixmap.height() / scaledPixmap.devicePixelRatio()) / 2,
531 scaledPixmap);
532 scaledPixmap = largeFrame;
533 } else {
534 // The image must be shrinked as it is too large to fit into
535 // the available icon size
536 KPixmapModifier::applyFrame(scaledPixmap, m_iconSize);
537 }
538 }
539 } else {
540 KPixmapModifier::scale(scaledPixmap, m_iconSize);
541 }
542
543 QHash<QByteArray, QVariant> data = rolesData(item);
544
545 const QStringList overlays = data["iconOverlays"].toStringList();
546 // Strangely KFileItem::overlays() returns empty string-values, so
547 // we need to check first whether an overlay must be drawn at all.
548 // It is more efficient to do it here, as KIconLoader::drawOverlays()
549 // assumes that an overlay will be drawn and has some additional
550 // setup time.
551 foreach (const QString& overlay, overlays) {
552 if (!overlay.isEmpty()) {
553 // There is at least one overlay, draw all overlays above m_pixmap
554 // and cancel the check
555 KIconLoader::global()->drawOverlays(overlays, scaledPixmap, KIconLoader::Desktop);
556 break;
557 }
558 }
559
560 data.insert("iconPixmap", scaledPixmap);
561
562 disconnect(m_model, &KFileItemModel::itemsChanged,
563 this, &KFileItemModelRolesUpdater::slotItemsChanged);
564 m_model->setData(index, data);
565 connect(m_model, &KFileItemModel::itemsChanged,
566 this, &KFileItemModelRolesUpdater::slotItemsChanged);
567
568 m_finishedItems.insert(item);
569 }
570
571 void KFileItemModelRolesUpdater::slotPreviewFailed(const KFileItem& item)
572 {
573 if (m_state != PreviewJobRunning) {
574 return;
575 }
576
577 m_changedItems.remove(item);
578
579 const int index = m_model->index(item);
580 if (index >= 0) {
581 QHash<QByteArray, QVariant> data;
582 data.insert("iconPixmap", QPixmap());
583
584 disconnect(m_model, &KFileItemModel::itemsChanged,
585 this, &KFileItemModelRolesUpdater::slotItemsChanged);
586 m_model->setData(index, data);
587 connect(m_model, &KFileItemModel::itemsChanged,
588 this, &KFileItemModelRolesUpdater::slotItemsChanged);
589
590 applyResolvedRoles(index, ResolveAll);
591 m_finishedItems.insert(item);
592 }
593 }
594
595 void KFileItemModelRolesUpdater::slotPreviewJobFinished()
596 {
597 m_previewJob = 0;
598
599 if (m_state != PreviewJobRunning) {
600 return;
601 }
602
603 m_state = Idle;
604
605 if (!m_pendingPreviewItems.isEmpty()) {
606 startPreviewJob();
607 } else {
608 if (!m_changedItems.isEmpty()) {
609 updateChangedItems();
610 }
611 }
612 }
613
614 void KFileItemModelRolesUpdater::resolveNextSortRole()
615 {
616 if (m_state != ResolvingSortRole) {
617 return;
618 }
619
620 QSet<KFileItem>::iterator it = m_pendingSortRoleItems.begin();
621 while (it != m_pendingSortRoleItems.end()) {
622 const KFileItem item = *it;
623 const int index = m_model->index(item);
624
625 // Continue if the sort role has already been determined for the
626 // item, and the item has not been changed recently.
627 if (!m_changedItems.contains(item) && m_model->data(index).contains(m_model->sortRole())) {
628 it = m_pendingSortRoleItems.erase(it);
629 continue;
630 }
631
632 applySortRole(index);
633 m_pendingSortRoleItems.erase(it);
634 break;
635 }
636
637 if (!m_pendingSortRoleItems.isEmpty()) {
638 applySortProgressToModel();
639 QTimer::singleShot(0, this, &KFileItemModelRolesUpdater::resolveNextSortRole);
640 } else {
641 m_state = Idle;
642
643 // Prevent that we try to update the items twice.
644 disconnect(m_model, &KFileItemModel::itemsMoved,
645 this, &KFileItemModelRolesUpdater::slotItemsMoved);
646 applySortProgressToModel();
647 connect(m_model, &KFileItemModel::itemsMoved,
648 this, &KFileItemModelRolesUpdater::slotItemsMoved);
649 startUpdating();
650 }
651 }
652
653 void KFileItemModelRolesUpdater::resolveNextPendingRoles()
654 {
655 if (m_state != ResolvingAllRoles) {
656 return;
657 }
658
659 while (!m_pendingIndexes.isEmpty()) {
660 const int index = m_pendingIndexes.takeFirst();
661 const KFileItem item = m_model->fileItem(index);
662
663 if (m_finishedItems.contains(item)) {
664 continue;
665 }
666
667 applyResolvedRoles(index, ResolveAll);
668 m_finishedItems.insert(item);
669 m_changedItems.remove(item);
670 break;
671 }
672
673 if (!m_pendingIndexes.isEmpty()) {
674 QTimer::singleShot(0, this, &KFileItemModelRolesUpdater::resolveNextPendingRoles);
675 } else {
676 m_state = Idle;
677
678 if (m_clearPreviews) {
679 // Only go through the list if there are items which might still have previews.
680 if (m_finishedItems.count() != m_model->count()) {
681 QHash<QByteArray, QVariant> data;
682 data.insert("iconPixmap", QPixmap());
683
684 disconnect(m_model, &KFileItemModel::itemsChanged,
685 this, &KFileItemModelRolesUpdater::slotItemsChanged);
686 for (int index = 0; index <= m_model->count(); ++index) {
687 if (m_model->data(index).contains("iconPixmap")) {
688 m_model->setData(index, data);
689 }
690 }
691 connect(m_model, &KFileItemModel::itemsChanged,
692 this, &KFileItemModelRolesUpdater::slotItemsChanged);
693
694 }
695 m_clearPreviews = false;
696 }
697
698 if (!m_changedItems.isEmpty()) {
699 updateChangedItems();
700 }
701 }
702 }
703
704 void KFileItemModelRolesUpdater::resolveRecentlyChangedItems()
705 {
706 m_changedItems += m_recentlyChangedItems;
707 m_recentlyChangedItems.clear();
708 updateChangedItems();
709 }
710
711 void KFileItemModelRolesUpdater::applyChangedBalooRoles(const QString& file)
712 {
713 #ifdef HAVE_BALOO
714 const KFileItem item = m_model->fileItem(QUrl::fromLocalFile(file));
715
716 if (item.isNull()) {
717 // itemUrl is not in the model anymore, probably because
718 // the corresponding file has been deleted in the meantime.
719 return;
720 }
721 applyChangedBalooRolesForItem(item);
722 #endif
723 }
724
725 void KFileItemModelRolesUpdater::applyChangedBalooRolesForItem(const KFileItem &item)
726 {
727 #ifdef HAVE_BALOO
728 Baloo::File file(item.localPath());
729 file.load();
730
731 const KBalooRolesProvider& rolesProvider = KBalooRolesProvider::instance();
732 QHash<QByteArray, QVariant> data;
733
734 foreach (const QByteArray& role, rolesProvider.roles()) {
735 // Overwrite all the role values with an empty QVariant, because the roles
736 // provider doesn't overwrite it when the property value list is empty.
737 // See bug 322348
738 data.insert(role, QVariant());
739 }
740
741 QHashIterator<QByteArray, QVariant> it(rolesProvider.roleValues(file, m_roles));
742 while (it.hasNext()) {
743 it.next();
744 data.insert(it.key(), it.value());
745 }
746
747 disconnect(m_model, &KFileItemModel::itemsChanged,
748 this, &KFileItemModelRolesUpdater::slotItemsChanged);
749 const int index = m_model->index(item);
750 m_model->setData(index, data);
751 connect(m_model, &KFileItemModel::itemsChanged,
752 this, &KFileItemModelRolesUpdater::slotItemsChanged);
753 #else
754 #ifndef Q_CC_MSVC
755 Q_UNUSED(item);
756 #endif
757 #endif
758 }
759
760 void KFileItemModelRolesUpdater::slotDirectoryContentsCountReceived(const QString& path, int count)
761 {
762 const bool getSizeRole = m_roles.contains("size");
763 const bool getIsExpandableRole = m_roles.contains("isExpandable");
764
765 if (getSizeRole || getIsExpandableRole) {
766 const int index = m_model->index(QUrl::fromLocalFile(path));
767 if (index >= 0) {
768 QHash<QByteArray, QVariant> data;
769
770 if (getSizeRole) {
771 data.insert("size", count);
772 }
773 if (getIsExpandableRole) {
774 data.insert("isExpandable", count > 0);
775 }
776
777 disconnect(m_model, &KFileItemModel::itemsChanged,
778 this, &KFileItemModelRolesUpdater::slotItemsChanged);
779 m_model->setData(index, data);
780 connect(m_model, &KFileItemModel::itemsChanged,
781 this, &KFileItemModelRolesUpdater::slotItemsChanged);
782 }
783 }
784 }
785
786 void KFileItemModelRolesUpdater::startUpdating()
787 {
788 if (m_state == Paused) {
789 return;
790 }
791
792 if (m_finishedItems.count() == m_model->count()) {
793 // All roles have been resolved already.
794 m_state = Idle;
795 return;
796 }
797
798 // Terminate all updates that are currently active.
799 killPreviewJob();
800 m_pendingIndexes.clear();
801
802 QElapsedTimer timer;
803 timer.start();
804
805 // Determine the icons for the visible items synchronously.
806 updateVisibleIcons();
807
808 // A detailed update of the items in and near the visible area
809 // only makes sense if sorting is finished.
810 if (m_state == ResolvingSortRole) {
811 return;
812 }
813
814 // Start the preview job or the asynchronous resolving of all roles.
815 QList<int> indexes = indexesToResolve();
816
817 if (m_previewShown) {
818 m_pendingPreviewItems.clear();
819 m_pendingPreviewItems.reserve(indexes.count());
820
821 foreach (int index, indexes) {
822 const KFileItem item = m_model->fileItem(index);
823 if (!m_finishedItems.contains(item)) {
824 m_pendingPreviewItems.append(item);
825 }
826 }
827
828 startPreviewJob();
829 } else {
830 m_pendingIndexes = indexes;
831 // Trigger the asynchronous resolving of all roles.
832 m_state = ResolvingAllRoles;
833 QTimer::singleShot(0, this, &KFileItemModelRolesUpdater::resolveNextPendingRoles);
834 }
835 }
836
837 void KFileItemModelRolesUpdater::updateVisibleIcons()
838 {
839 int lastVisibleIndex = m_lastVisibleIndex;
840 if (lastVisibleIndex <= 0) {
841 // Guess a reasonable value for the last visible index if the view
842 // has not told us about the real value yet.
843 lastVisibleIndex = qMin(m_firstVisibleIndex + m_maximumVisibleItems, m_model->count() - 1);
844 if (lastVisibleIndex <= 0) {
845 lastVisibleIndex = qMin(200, m_model->count() - 1);
846 }
847 }
848
849 QElapsedTimer timer;
850 timer.start();
851
852 // Try to determine the final icons for all visible items.
853 int index;
854 for (index = m_firstVisibleIndex; index <= lastVisibleIndex && timer.elapsed() < MaxBlockTimeout; ++index) {
855 applyResolvedRoles(index, ResolveFast);
856 }
857
858 // KFileItemListView::initializeItemListWidget(KItemListWidget*) will load
859 // preliminary icons (i.e., without mime type determination) for the
860 // remaining items.
861 }
862
863 void KFileItemModelRolesUpdater::startPreviewJob()
864 {
865 m_state = PreviewJobRunning;
866
867 if (m_pendingPreviewItems.isEmpty()) {
868 QTimer::singleShot(0, this, &KFileItemModelRolesUpdater::slotPreviewJobFinished);
869 return;
870 }
871
872 // PreviewJob internally caches items always with the size of
873 // 128 x 128 pixels or 256 x 256 pixels. A (slow) downscaling is done
874 // by PreviewJob if a smaller size is requested. For images KFileItemModelRolesUpdater must
875 // do a downscaling anyhow because of the frame, so in this case only the provided
876 // cache sizes are requested.
877 const QSize cacheSize = (m_iconSize.width() > 128) || (m_iconSize.height() > 128)
878 ? QSize(256, 256) : QSize(128, 128);
879
880 // KIO::filePreview() will request the MIME-type of all passed items, which (in the
881 // worst case) might block the application for several seconds. To prevent such
882 // a blocking, we only pass items with known mime type to the preview job.
883 const int count = m_pendingPreviewItems.count();
884 KFileItemList itemSubSet;
885 itemSubSet.reserve(count);
886
887 if (m_pendingPreviewItems.first().isMimeTypeKnown()) {
888 // Some mime types are known already, probably because they were
889 // determined when loading the icons for the visible items. Start
890 // a preview job for all items at the beginning of the list which
891 // have a known mime type.
892 do {
893 itemSubSet.append(m_pendingPreviewItems.takeFirst());
894 } while (!m_pendingPreviewItems.isEmpty() && m_pendingPreviewItems.first().isMimeTypeKnown());
895 } else {
896 // Determine mime types for MaxBlockTimeout ms, and start a preview
897 // job for the corresponding items.
898 QElapsedTimer timer;
899 timer.start();
900
901 do {
902 const KFileItem item = m_pendingPreviewItems.takeFirst();
903 item.determineMimeType();
904 itemSubSet.append(item);
905 } while (!m_pendingPreviewItems.isEmpty() && timer.elapsed() < MaxBlockTimeout);
906 }
907
908 KIO::PreviewJob* job = new KIO::PreviewJob(itemSubSet, cacheSize, &m_enabledPlugins);
909
910 job->setIgnoreMaximumSize(itemSubSet.first().isLocalFile());
911 if (job->uiDelegate()) {
912 KJobWidgets::setWindow(job, qApp->activeWindow());
913 }
914
915 connect(job, &KIO::PreviewJob::gotPreview,
916 this, &KFileItemModelRolesUpdater::slotGotPreview);
917 connect(job, &KIO::PreviewJob::failed,
918 this, &KFileItemModelRolesUpdater::slotPreviewFailed);
919 connect(job, &KIO::PreviewJob::finished,
920 this, &KFileItemModelRolesUpdater::slotPreviewJobFinished);
921
922 m_previewJob = job;
923 }
924
925 void KFileItemModelRolesUpdater::updateChangedItems()
926 {
927 if (m_state == Paused) {
928 return;
929 }
930
931 if (m_changedItems.isEmpty()) {
932 return;
933 }
934
935 m_finishedItems -= m_changedItems;
936
937 if (m_resolvableRoles.contains(m_model->sortRole())) {
938 m_pendingSortRoleItems += m_changedItems;
939
940 if (m_state != ResolvingSortRole) {
941 // Stop the preview job if necessary, and trigger the
942 // asynchronous determination of the sort role.
943 killPreviewJob();
944 m_state = ResolvingSortRole;
945 QTimer::singleShot(0, this, &KFileItemModelRolesUpdater::resolveNextSortRole);
946 }
947
948 return;
949 }
950
951 QList<int> visibleChangedIndexes;
952 QList<int> invisibleChangedIndexes;
953
954 foreach (const KFileItem& item, m_changedItems) {
955 const int index = m_model->index(item);
956
957 if (index < 0) {
958 m_changedItems.remove(item);
959 continue;
960 }
961
962 if (index >= m_firstVisibleIndex && index <= m_lastVisibleIndex) {
963 visibleChangedIndexes.append(index);
964 } else {
965 invisibleChangedIndexes.append(index);
966 }
967 }
968
969 std::sort(visibleChangedIndexes.begin(), visibleChangedIndexes.end());
970
971 if (m_previewShown) {
972 foreach (int index, visibleChangedIndexes) {
973 m_pendingPreviewItems.append(m_model->fileItem(index));
974 }
975
976 foreach (int index, invisibleChangedIndexes) {
977 m_pendingPreviewItems.append(m_model->fileItem(index));
978 }
979
980 if (!m_previewJob) {
981 startPreviewJob();
982 }
983 } else {
984 const bool resolvingInProgress = !m_pendingIndexes.isEmpty();
985 m_pendingIndexes = visibleChangedIndexes + m_pendingIndexes + invisibleChangedIndexes;
986 if (!resolvingInProgress) {
987 // Trigger the asynchronous resolving of the changed roles.
988 m_state = ResolvingAllRoles;
989 QTimer::singleShot(0, this, &KFileItemModelRolesUpdater::resolveNextPendingRoles);
990 }
991 }
992 }
993
994 void KFileItemModelRolesUpdater::applySortRole(int index)
995 {
996 QHash<QByteArray, QVariant> data;
997 const KFileItem item = m_model->fileItem(index);
998
999 if (m_model->sortRole() == "type") {
1000 if (!item.isMimeTypeKnown()) {
1001 item.determineMimeType();
1002 }
1003
1004 data.insert("type", item.mimeComment());
1005 } else if (m_model->sortRole() == "size" && item.isLocalFile() && item.isDir()) {
1006 const QString path = item.localPath();
1007 data.insert("size", m_directoryContentsCounter->countDirectoryContentsSynchronously(path));
1008 } else {
1009 // Probably the sort role is a baloo role - just determine all roles.
1010 data = rolesData(item);
1011 }
1012
1013 disconnect(m_model, &KFileItemModel::itemsChanged,
1014 this, &KFileItemModelRolesUpdater::slotItemsChanged);
1015 m_model->setData(index, data);
1016 connect(m_model, &KFileItemModel::itemsChanged,
1017 this, &KFileItemModelRolesUpdater::slotItemsChanged);
1018 }
1019
1020 void KFileItemModelRolesUpdater::applySortProgressToModel()
1021 {
1022 // Inform the model about the progress of the resolved items,
1023 // so that it can give an indication when the sorting has been finished.
1024 const int resolvedCount = m_model->count() - m_pendingSortRoleItems.count();
1025 m_model->emitSortProgress(resolvedCount);
1026 }
1027
1028 bool KFileItemModelRolesUpdater::applyResolvedRoles(int index, ResolveHint hint)
1029 {
1030 const KFileItem item = m_model->fileItem(index);
1031 const bool resolveAll = (hint == ResolveAll);
1032
1033 bool iconChanged = false;
1034 if (!item.isMimeTypeKnown() || !item.isFinalIconKnown()) {
1035 item.determineMimeType();
1036 iconChanged = true;
1037 } else if (!m_model->data(index).contains("iconName")) {
1038 iconChanged = true;
1039 }
1040
1041 if (iconChanged || resolveAll || m_clearPreviews) {
1042 if (index < 0) {
1043 return false;
1044 }
1045
1046 QHash<QByteArray, QVariant> data;
1047 if (resolveAll) {
1048 data = rolesData(item);
1049 }
1050
1051 data.insert("iconName", item.iconName());
1052
1053 if (m_clearPreviews) {
1054 data.insert("iconPixmap", QPixmap());
1055 }
1056
1057 disconnect(m_model, &KFileItemModel::itemsChanged,
1058 this, &KFileItemModelRolesUpdater::slotItemsChanged);
1059 m_model->setData(index, data);
1060 connect(m_model, &KFileItemModel::itemsChanged,
1061 this, &KFileItemModelRolesUpdater::slotItemsChanged);
1062 return true;
1063 }
1064
1065 return false;
1066 }
1067
1068 QHash<QByteArray, QVariant> KFileItemModelRolesUpdater::rolesData(const KFileItem& item)
1069 {
1070 QHash<QByteArray, QVariant> data;
1071
1072 const bool getSizeRole = m_roles.contains("size");
1073 const bool getIsExpandableRole = m_roles.contains("isExpandable");
1074
1075 if ((getSizeRole || getIsExpandableRole) && item.isDir()) {
1076 if (item.isLocalFile()) {
1077 // Tell m_directoryContentsCounter that we want to count the items
1078 // inside the directory. The result will be received in slotDirectoryContentsCountReceived.
1079 const QString path = item.localPath();
1080 m_directoryContentsCounter->addDirectory(path);
1081 } else if (getSizeRole) {
1082 data.insert("size", -1); // -1 indicates an unknown number of items
1083 }
1084 }
1085
1086 if (m_roles.contains("type")) {
1087 data.insert("type", item.mimeComment());
1088 }
1089
1090 QStringList overlays = item.overlays();
1091 foreach(KOverlayIconPlugin *it, m_overlayIconsPlugin) {
1092 overlays.append(it->getOverlays(item.url()));
1093 }
1094 data.insert("iconOverlays", overlays);
1095
1096 #ifdef HAVE_BALOO
1097 if (m_balooFileMonitor) {
1098 m_balooFileMonitor->addFile(item.localPath());
1099 applyChangedBalooRolesForItem(item);
1100 }
1101 #endif
1102 return data;
1103 }
1104
1105 void KFileItemModelRolesUpdater::slotOverlaysChanged(const QUrl& url, const QStringList &)
1106 {
1107 const KFileItem item = m_model->fileItem(url);
1108 if (item.isNull()) {
1109 return;
1110 }
1111 const int index = m_model->index(item);
1112 QHash<QByteArray, QVariant> data = m_model->data(index);
1113 QStringList overlays = item.overlays();
1114 foreach (KOverlayIconPlugin *it, m_overlayIconsPlugin) {
1115 overlays.append(it->getOverlays(url));
1116 }
1117 data.insert("iconOverlays", overlays);
1118 m_model->setData(index, data);
1119 }
1120
1121 void KFileItemModelRolesUpdater::updateAllPreviews()
1122 {
1123 if (m_state == Paused) {
1124 m_previewChangedDuringPausing = true;
1125 } else {
1126 m_finishedItems.clear();
1127 startUpdating();
1128 }
1129 }
1130
1131 void KFileItemModelRolesUpdater::killPreviewJob()
1132 {
1133 if (m_previewJob) {
1134 disconnect(m_previewJob, &KIO::PreviewJob::gotPreview,
1135 this, &KFileItemModelRolesUpdater::slotGotPreview);
1136 disconnect(m_previewJob, &KIO::PreviewJob::failed,
1137 this, &KFileItemModelRolesUpdater::slotPreviewFailed);
1138 disconnect(m_previewJob, &KIO::PreviewJob::finished,
1139 this, &KFileItemModelRolesUpdater::slotPreviewJobFinished);
1140 m_previewJob->kill();
1141 m_previewJob = 0;
1142 m_pendingPreviewItems.clear();
1143 }
1144 }
1145
1146 QList<int> KFileItemModelRolesUpdater::indexesToResolve() const
1147 {
1148 const int count = m_model->count();
1149
1150 QList<int> result;
1151 result.reserve(ResolveAllItemsLimit);
1152
1153 // Add visible items.
1154 for (int i = m_firstVisibleIndex; i <= m_lastVisibleIndex; ++i) {
1155 result.append(i);
1156 }
1157
1158 // We need a reasonable upper limit for number of items to resolve after
1159 // and before the visible range. m_maximumVisibleItems can be quite large
1160 // when using Compace View.
1161 const int readAheadItems = qMin(ReadAheadPages * m_maximumVisibleItems, ResolveAllItemsLimit / 2);
1162
1163 // Add items after the visible range.
1164 const int endExtendedVisibleRange = qMin(m_lastVisibleIndex + readAheadItems, count - 1);
1165 for (int i = m_lastVisibleIndex + 1; i <= endExtendedVisibleRange; ++i) {
1166 result.append(i);
1167 }
1168
1169 // Add items before the visible range in reverse order.
1170 const int beginExtendedVisibleRange = qMax(0, m_firstVisibleIndex - readAheadItems);
1171 for (int i = m_firstVisibleIndex - 1; i >= beginExtendedVisibleRange; --i) {
1172 result.append(i);
1173 }
1174
1175 // Add items on the last page.
1176 const int beginLastPage = qMax(qMin(endExtendedVisibleRange + 1, count - 1), count - m_maximumVisibleItems);
1177 for (int i = beginLastPage; i < count; ++i) {
1178 result.append(i);
1179 }
1180
1181 // Add items on the first page.
1182 const int endFirstPage = qMin(qMax(beginExtendedVisibleRange - 1, 0), m_maximumVisibleItems);
1183 for (int i = 0; i <= endFirstPage; ++i) {
1184 result.append(i);
1185 }
1186
1187 // Continue adding items until ResolveAllItemsLimit is reached.
1188 int remainingItems = ResolveAllItemsLimit - result.count();
1189
1190 for (int i = endExtendedVisibleRange + 1; i < beginLastPage && remainingItems > 0; ++i) {
1191 result.append(i);
1192 --remainingItems;
1193 }
1194
1195 for (int i = beginExtendedVisibleRange - 1; i > endFirstPage && remainingItems > 0; --i) {
1196 result.append(i);
1197 --remainingItems;
1198 }
1199
1200 return result;
1201 }
1202