]> cloud.milkyroute.net Git - dolphin.git/blob - src/kitemviews/kfileitemmodelrolesupdater.cpp
Merge branch 'master' into kf6
[dolphin.git] / src / kitemviews / kfileitemmodelrolesupdater.cpp
1 /*
2 * SPDX-FileCopyrightText: 2011 Peter Penz <peter.penz19@gmail.com>
3 *
4 * SPDX-License-Identifier: GPL-2.0-or-later
5 */
6
7 #include "kfileitemmodelrolesupdater.h"
8
9 #include "dolphindebug.h"
10 #include "kfileitemmodel.h"
11 #include "private/kdirectorycontentscounter.h"
12 #include "private/kpixmapmodifier.h"
13
14 #include <KConfig>
15 #include <KConfigGroup>
16 #include <KIO/PreviewJob>
17 #include <KIconLoader>
18 #include <KJobWidgets>
19 #include <KOverlayIconPlugin>
20 #include <KPluginMetaData>
21 #include <KSharedConfig>
22
23 #include "dolphin_contentdisplaysettings.h"
24
25 #if HAVE_BALOO
26 #include "private/kbaloorolesprovider.h"
27 #include <Baloo/File>
28 #include <Baloo/FileMonitor>
29 #endif
30
31 #include <QApplication>
32 #include <QElapsedTimer>
33 #include <QFileInfo>
34 #include <QPainter>
35 #include <QPluginLoader>
36 #include <QTimer>
37 #include <chrono>
38
39 using namespace std::chrono_literals;
40
41 // #define KFILEITEMMODELROLESUPDATER_DEBUG
42
43 namespace
44 {
45 // Maximum time in ms that the KFileItemModelRolesUpdater
46 // may perform a blocking operation
47 const int MaxBlockTimeout = 200;
48
49 // If the number of items is smaller than ResolveAllItemsLimit,
50 // the roles of all items will be resolved.
51 const int ResolveAllItemsLimit = 500;
52
53 // Not only the visible area, but up to ReadAheadPages before and after
54 // this area will be resolved.
55 const int ReadAheadPages = 5;
56 }
57
58 KFileItemModelRolesUpdater::KFileItemModelRolesUpdater(KFileItemModel *model, QObject *parent)
59 : QObject(parent)
60 , m_state(Idle)
61 , m_previewChangedDuringPausing(false)
62 , m_iconSizeChangedDuringPausing(false)
63 , m_rolesChangedDuringPausing(false)
64 , m_previewShown(false)
65 , m_enlargeSmallPreviews(true)
66 , m_clearPreviews(false)
67 , m_finishedItems()
68 , m_model(model)
69 , m_iconSize()
70 , m_firstVisibleIndex(0)
71 , m_lastVisibleIndex(-1)
72 , m_maximumVisibleItems(50)
73 , m_roles()
74 , m_resolvableRoles()
75 , m_enabledPlugins()
76 , m_localFileSizePreviewLimit(0)
77 , m_pendingSortRoleItems()
78 , m_pendingIndexes()
79 , m_pendingPreviewItems()
80 , m_previewJob()
81 , m_hoverSequenceItem()
82 , m_hoverSequenceIndex(0)
83 , m_hoverSequencePreviewJob(nullptr)
84 , m_hoverSequenceNumSuccessiveFailures(0)
85 , m_recentlyChangedItemsTimer(nullptr)
86 , m_recentlyChangedItems()
87 , m_changedItems()
88 , m_directoryContentsCounter(nullptr)
89 #if HAVE_BALOO
90 , m_balooFileMonitor(nullptr)
91 #endif
92 {
93 Q_ASSERT(model);
94
95 const KConfigGroup globalConfig(KSharedConfig::openConfig(), "PreviewSettings");
96 m_enabledPlugins = globalConfig.readEntry("Plugins", KIO::PreviewJob::defaultPlugins());
97 m_localFileSizePreviewLimit = static_cast<qulonglong>(globalConfig.readEntry("MaximumSize", 0));
98
99 connect(m_model, &KFileItemModel::itemsInserted, this, &KFileItemModelRolesUpdater::slotItemsInserted);
100 connect(m_model, &KFileItemModel::itemsRemoved, this, &KFileItemModelRolesUpdater::slotItemsRemoved);
101 connect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged);
102 connect(m_model, &KFileItemModel::itemsMoved, this, &KFileItemModelRolesUpdater::slotItemsMoved);
103 connect(m_model, &KFileItemModel::sortRoleChanged, this, &KFileItemModelRolesUpdater::slotSortRoleChanged);
104
105 // Use a timer to prevent that each call of slotItemsChanged() results in a synchronous
106 // resolving of the roles. Postpone the resolving until no update has been done for 100 ms.
107 m_recentlyChangedItemsTimer = new QTimer(this);
108 m_recentlyChangedItemsTimer->setInterval(100ms);
109 m_recentlyChangedItemsTimer->setSingleShot(true);
110 connect(m_recentlyChangedItemsTimer, &QTimer::timeout, this, &KFileItemModelRolesUpdater::resolveRecentlyChangedItems);
111
112 m_resolvableRoles.insert("size");
113 m_resolvableRoles.insert("type");
114 m_resolvableRoles.insert("isExpandable");
115 #if HAVE_BALOO
116 m_resolvableRoles += KBalooRolesProvider::instance().roles();
117 #endif
118
119 m_directoryContentsCounter = new KDirectoryContentsCounter(m_model, this);
120 connect(m_directoryContentsCounter, &KDirectoryContentsCounter::result, this, &KFileItemModelRolesUpdater::slotDirectoryContentsCountReceived);
121
122 const QString pluginNamespace = QStringLiteral("kf" QT_STRINGIFY(QT_MAJOR_VERSION)) + QStringLiteral("/overlayicon");
123 const auto plugins = KPluginMetaData::findPlugins(pluginNamespace, {}, KPluginMetaData::AllowEmptyMetaData);
124 for (const KPluginMetaData &data : plugins) {
125 auto instance = QPluginLoader(data.fileName()).instance();
126 auto plugin = qobject_cast<KOverlayIconPlugin *>(instance);
127 if (plugin) {
128 m_overlayIconsPlugin.append(plugin);
129 connect(plugin, &KOverlayIconPlugin::overlaysChanged, this, &KFileItemModelRolesUpdater::slotOverlaysChanged);
130 } else {
131 // not our/valid plugin, so delete the created object
132 delete instance;
133 }
134 }
135 }
136
137 KFileItemModelRolesUpdater::~KFileItemModelRolesUpdater()
138 {
139 killPreviewJob();
140 }
141
142 void KFileItemModelRolesUpdater::setIconSize(const QSize &size)
143 {
144 if (size != m_iconSize) {
145 m_iconSize = size;
146 if (m_state == Paused) {
147 m_iconSizeChangedDuringPausing = true;
148 } else if (m_previewShown) {
149 // An icon size change requires the regenerating of
150 // all previews
151 m_finishedItems.clear();
152 startUpdating();
153 }
154 }
155 }
156
157 QSize KFileItemModelRolesUpdater::iconSize() const
158 {
159 return m_iconSize;
160 }
161
162 void KFileItemModelRolesUpdater::setVisibleIndexRange(int index, int count)
163 {
164 if (index < 0) {
165 index = 0;
166 }
167 if (count < 0) {
168 count = 0;
169 }
170
171 if (index == m_firstVisibleIndex && count == m_lastVisibleIndex - m_firstVisibleIndex + 1) {
172 // The range has not been changed
173 return;
174 }
175
176 m_firstVisibleIndex = index;
177 m_lastVisibleIndex = qMin(index + count - 1, m_model->count() - 1);
178
179 startUpdating();
180 }
181
182 void KFileItemModelRolesUpdater::setMaximumVisibleItems(int count)
183 {
184 m_maximumVisibleItems = count;
185 }
186
187 void KFileItemModelRolesUpdater::setPreviewsShown(bool show)
188 {
189 if (show == m_previewShown) {
190 return;
191 }
192
193 m_previewShown = show;
194 if (!show) {
195 m_clearPreviews = true;
196 }
197
198 updateAllPreviews();
199 }
200
201 bool KFileItemModelRolesUpdater::previewsShown() const
202 {
203 return m_previewShown;
204 }
205
206 void KFileItemModelRolesUpdater::setEnlargeSmallPreviews(bool enlarge)
207 {
208 if (enlarge != m_enlargeSmallPreviews) {
209 m_enlargeSmallPreviews = enlarge;
210 if (m_previewShown) {
211 updateAllPreviews();
212 }
213 }
214 }
215
216 bool KFileItemModelRolesUpdater::enlargeSmallPreviews() const
217 {
218 return m_enlargeSmallPreviews;
219 }
220
221 void KFileItemModelRolesUpdater::setEnabledPlugins(const QStringList &list)
222 {
223 if (m_enabledPlugins != list) {
224 m_enabledPlugins = list;
225 if (m_previewShown) {
226 updateAllPreviews();
227 }
228 }
229 }
230
231 void KFileItemModelRolesUpdater::setPaused(bool paused)
232 {
233 if (paused == (m_state == Paused)) {
234 return;
235 }
236
237 if (paused) {
238 m_state = Paused;
239 killPreviewJob();
240 } else {
241 const bool updatePreviews = (m_iconSizeChangedDuringPausing && m_previewShown) || m_previewChangedDuringPausing;
242 const bool resolveAll = updatePreviews || m_rolesChangedDuringPausing;
243 if (resolveAll) {
244 m_finishedItems.clear();
245 }
246
247 m_iconSizeChangedDuringPausing = false;
248 m_previewChangedDuringPausing = false;
249 m_rolesChangedDuringPausing = false;
250
251 if (!m_pendingSortRoleItems.isEmpty()) {
252 m_state = ResolvingSortRole;
253 resolveNextSortRole();
254 } else {
255 m_state = Idle;
256 }
257
258 startUpdating();
259 }
260 }
261
262 void KFileItemModelRolesUpdater::setRoles(const QSet<QByteArray> &roles)
263 {
264 if (m_roles != roles) {
265 m_roles = roles;
266
267 #if HAVE_BALOO
268 // Check whether there is at least one role that must be resolved
269 // with the help of Baloo. If this is the case, a (quite expensive)
270 // resolving will be done in KFileItemModelRolesUpdater::rolesData() and
271 // the role gets watched for changes.
272 const KBalooRolesProvider &rolesProvider = KBalooRolesProvider::instance();
273 bool hasBalooRole = false;
274 QSetIterator<QByteArray> it(roles);
275 while (it.hasNext()) {
276 const QByteArray &role = it.next();
277 if (rolesProvider.roles().contains(role)) {
278 hasBalooRole = true;
279 break;
280 }
281 }
282
283 if (hasBalooRole && m_balooConfig.fileIndexingEnabled() && !m_balooFileMonitor) {
284 m_balooFileMonitor = new Baloo::FileMonitor(this);
285 connect(m_balooFileMonitor, &Baloo::FileMonitor::fileMetaDataChanged, this, &KFileItemModelRolesUpdater::applyChangedBalooRoles);
286 } else if (!hasBalooRole && m_balooFileMonitor) {
287 delete m_balooFileMonitor;
288 m_balooFileMonitor = nullptr;
289 }
290 #endif
291
292 if (m_state == Paused) {
293 m_rolesChangedDuringPausing = true;
294 } else {
295 startUpdating();
296 }
297 }
298 }
299
300 QSet<QByteArray> KFileItemModelRolesUpdater::roles() const
301 {
302 return m_roles;
303 }
304
305 bool KFileItemModelRolesUpdater::isPaused() const
306 {
307 return m_state == Paused;
308 }
309
310 QStringList KFileItemModelRolesUpdater::enabledPlugins() const
311 {
312 return m_enabledPlugins;
313 }
314
315 void KFileItemModelRolesUpdater::setLocalFileSizePreviewLimit(const qlonglong size)
316 {
317 m_localFileSizePreviewLimit = size;
318 }
319
320 qlonglong KFileItemModelRolesUpdater::localFileSizePreviewLimit() const
321 {
322 return m_localFileSizePreviewLimit;
323 }
324
325 void KFileItemModelRolesUpdater::setHoverSequenceState(const QUrl &itemUrl, int seqIdx)
326 {
327 const KFileItem item = m_model->fileItem(itemUrl);
328
329 if (item != m_hoverSequenceItem) {
330 killHoverSequencePreviewJob();
331 }
332
333 m_hoverSequenceItem = item;
334 m_hoverSequenceIndex = seqIdx;
335
336 if (!m_previewShown) {
337 return;
338 }
339
340 m_hoverSequenceNumSuccessiveFailures = 0;
341
342 loadNextHoverSequencePreview();
343 }
344
345 void KFileItemModelRolesUpdater::slotItemsInserted(const KItemRangeList &itemRanges)
346 {
347 QElapsedTimer timer;
348 timer.start();
349
350 // Determine the sort role synchronously for as many items as possible.
351 if (m_resolvableRoles.contains(m_model->sortRole())) {
352 int insertedCount = 0;
353 for (const KItemRange &range : itemRanges) {
354 const int lastIndex = insertedCount + range.index + range.count - 1;
355 for (int i = insertedCount + range.index; i <= lastIndex; ++i) {
356 if (timer.elapsed() < MaxBlockTimeout) {
357 applySortRole(i);
358 } else {
359 m_pendingSortRoleItems.insert(m_model->fileItem(i));
360 }
361 }
362 insertedCount += range.count;
363 }
364
365 applySortProgressToModel();
366
367 // If there are still items whose sort role is unknown, check if the
368 // asynchronous determination of the sort role is already in progress,
369 // and start it if that is not the case.
370 if (!m_pendingSortRoleItems.isEmpty() && m_state != ResolvingSortRole) {
371 killPreviewJob();
372 m_state = ResolvingSortRole;
373 resolveNextSortRole();
374 }
375 }
376
377 startUpdating();
378 }
379
380 void KFileItemModelRolesUpdater::slotItemsRemoved(const KItemRangeList &itemRanges)
381 {
382 Q_UNUSED(itemRanges)
383
384 const bool allItemsRemoved = (m_model->count() == 0);
385
386 #if HAVE_BALOO
387 if (m_balooFileMonitor) {
388 // Don't let the FileWatcher watch for removed items
389 if (allItemsRemoved) {
390 m_balooFileMonitor->clear();
391 } else {
392 QStringList newFileList;
393 const QStringList oldFileList = m_balooFileMonitor->files();
394 for (const QString &file : oldFileList) {
395 if (m_model->index(QUrl::fromLocalFile(file)) >= 0) {
396 newFileList.append(file);
397 }
398 }
399 m_balooFileMonitor->setFiles(newFileList);
400 }
401 }
402 #endif
403
404 if (allItemsRemoved) {
405 m_state = Idle;
406
407 m_finishedItems.clear();
408 m_pendingSortRoleItems.clear();
409 m_pendingIndexes.clear();
410 m_pendingPreviewItems.clear();
411 m_recentlyChangedItems.clear();
412 m_recentlyChangedItemsTimer->stop();
413 m_changedItems.clear();
414 m_hoverSequenceLoadedItems.clear();
415
416 killPreviewJob();
417 if (!m_model->showDirectoriesOnly()) {
418 m_directoryContentsCounter->stopWorker();
419 }
420 } else {
421 // Only remove the items from m_finishedItems. They will be removed
422 // from the other sets later on.
423 QSet<KFileItem>::iterator it = m_finishedItems.begin();
424 while (it != m_finishedItems.end()) {
425 if (m_model->index(*it) < 0) {
426 it = m_finishedItems.erase(it);
427 } else {
428 ++it;
429 }
430 }
431
432 // Removed items won't have hover previews loaded anymore.
433 for (const KItemRange &itemRange : itemRanges) {
434 int index = itemRange.index;
435 for (int count = itemRange.count; count > 0; --count) {
436 const KFileItem item = m_model->fileItem(index);
437 m_hoverSequenceLoadedItems.remove(item);
438 ++index;
439 }
440 }
441
442 // The visible items might have changed.
443 startUpdating();
444 }
445 }
446
447 void KFileItemModelRolesUpdater::slotItemsMoved(KItemRange itemRange, const QList<int> &movedToIndexes)
448 {
449 Q_UNUSED(itemRange)
450 Q_UNUSED(movedToIndexes)
451
452 // The visible items might have changed.
453 startUpdating();
454 }
455
456 void KFileItemModelRolesUpdater::slotItemsChanged(const KItemRangeList &itemRanges, const QSet<QByteArray> &roles)
457 {
458 Q_UNUSED(roles)
459
460 // Find out if slotItemsChanged() has been done recently. If that is the
461 // case, resolving the roles is postponed until a timer has exceeded
462 // to prevent expensive repeated updates if files are updated frequently.
463 const bool itemsChangedRecently = m_recentlyChangedItemsTimer->isActive();
464
465 QSet<KFileItem> &targetSet = itemsChangedRecently ? m_recentlyChangedItems : m_changedItems;
466
467 for (const KItemRange &itemRange : itemRanges) {
468 int index = itemRange.index;
469 for (int count = itemRange.count; count > 0; --count) {
470 const KFileItem item = m_model->fileItem(index);
471 targetSet.insert(item);
472 ++index;
473 }
474 }
475
476 m_recentlyChangedItemsTimer->start();
477
478 if (!itemsChangedRecently) {
479 updateChangedItems();
480 }
481 }
482
483 void KFileItemModelRolesUpdater::slotSortRoleChanged(const QByteArray &current, const QByteArray &previous)
484 {
485 Q_UNUSED(current)
486 Q_UNUSED(previous)
487
488 if (m_resolvableRoles.contains(current)) {
489 m_pendingSortRoleItems.clear();
490 m_finishedItems.clear();
491
492 const int count = m_model->count();
493 QElapsedTimer timer;
494 timer.start();
495
496 // Determine the sort role synchronously for as many items as possible.
497 for (int index = 0; index < count; ++index) {
498 if (timer.elapsed() < MaxBlockTimeout) {
499 applySortRole(index);
500 } else {
501 m_pendingSortRoleItems.insert(m_model->fileItem(index));
502 }
503 }
504
505 applySortProgressToModel();
506
507 if (!m_pendingSortRoleItems.isEmpty()) {
508 // Trigger the asynchronous determination of the sort role.
509 killPreviewJob();
510 m_state = ResolvingSortRole;
511 resolveNextSortRole();
512 }
513 } else {
514 m_state = Idle;
515 m_pendingSortRoleItems.clear();
516 applySortProgressToModel();
517 }
518 }
519
520 void KFileItemModelRolesUpdater::slotGotPreview(const KFileItem &item, const QPixmap &pixmap)
521 {
522 if (m_state != PreviewJobRunning) {
523 return;
524 }
525
526 m_changedItems.remove(item);
527
528 const int index = m_model->index(item);
529 if (index < 0) {
530 return;
531 }
532
533 QPixmap scaledPixmap = transformPreviewPixmap(pixmap);
534
535 QHash<QByteArray, QVariant> data = rolesData(item, index);
536
537 const QStringList overlays = data["iconOverlays"].toStringList();
538 // Strangely KFileItem::overlays() returns empty string-values, so
539 // we need to check first whether an overlay must be drawn at all.
540 // It is more efficient to do it here, as KIconLoader::drawOverlays()
541 // assumes that an overlay will be drawn and has some additional
542 // setup time.
543 if (!scaledPixmap.isNull()) {
544 for (const QString &overlay : overlays) {
545 if (!overlay.isEmpty()) {
546 // There is at least one overlay, draw all overlays above m_pixmap
547 // and cancel the check
548 KIconLoader::global()->drawOverlays(overlays, scaledPixmap, KIconLoader::Desktop);
549 break;
550 }
551 }
552 }
553
554 data.insert("iconPixmap", scaledPixmap);
555
556 disconnect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged);
557 m_model->setData(index, data);
558 connect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged);
559
560 m_finishedItems.insert(item);
561 }
562
563 void KFileItemModelRolesUpdater::slotPreviewFailed(const KFileItem &item)
564 {
565 if (m_state != PreviewJobRunning) {
566 return;
567 }
568
569 m_changedItems.remove(item);
570
571 const int index = m_model->index(item);
572 if (index >= 0) {
573 QHash<QByteArray, QVariant> data;
574 data.insert("iconPixmap", QPixmap());
575
576 disconnect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged);
577 m_model->setData(index, data);
578 connect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged);
579
580 applyResolvedRoles(index, ResolveAll);
581 m_finishedItems.insert(item);
582 }
583 }
584
585 void KFileItemModelRolesUpdater::slotPreviewJobFinished()
586 {
587 m_previewJob = nullptr;
588
589 if (m_state != PreviewJobRunning) {
590 return;
591 }
592
593 m_state = Idle;
594
595 if (!m_pendingPreviewItems.isEmpty()) {
596 startPreviewJob();
597 } else {
598 if (!m_changedItems.isEmpty()) {
599 updateChangedItems();
600 }
601 }
602 }
603
604 void KFileItemModelRolesUpdater::slotHoverSequenceGotPreview(const KFileItem &item, const QPixmap &pixmap)
605 {
606 const int index = m_model->index(item);
607 if (index < 0) {
608 return;
609 }
610
611 QHash<QByteArray, QVariant> data = m_model->data(index);
612 QVector<QPixmap> pixmaps = data["hoverSequencePixmaps"].value<QVector<QPixmap>>();
613 const int loadedIndex = pixmaps.size();
614
615 float wap = m_hoverSequencePreviewJob->sequenceIndexWraparoundPoint();
616 if (!m_hoverSequencePreviewJob->handlesSequences()) {
617 wap = 1.0f;
618 }
619 if (wap >= 0.0f) {
620 data["hoverSequenceWraparoundPoint"] = wap;
621 m_model->setData(index, data);
622 }
623
624 // For hover sequence previews we never load index 0, because that's just the regular preview
625 // in "iconPixmap". But that means we'll load index 1 even for thumbnailers that don't support
626 // sequences, in which case we can just throw away the preview because it's the same as for
627 // index 0. Unfortunately we can't find it out earlier :(
628 if (wap < 0.0f || loadedIndex < static_cast<int>(wap)) {
629 // Add the preview to the model data
630
631 const QPixmap scaledPixmap = transformPreviewPixmap(pixmap);
632
633 pixmaps.append(scaledPixmap);
634 data["hoverSequencePixmaps"] = QVariant::fromValue(pixmaps);
635
636 m_model->setData(index, data);
637
638 const auto loadedIt = std::find(m_hoverSequenceLoadedItems.begin(), m_hoverSequenceLoadedItems.end(), item);
639 if (loadedIt == m_hoverSequenceLoadedItems.end()) {
640 m_hoverSequenceLoadedItems.push_back(item);
641 trimHoverSequenceLoadedItems();
642 }
643 }
644
645 m_hoverSequenceNumSuccessiveFailures = 0;
646 }
647
648 void KFileItemModelRolesUpdater::slotHoverSequencePreviewFailed(const KFileItem &item)
649 {
650 const int index = m_model->index(item);
651 if (index < 0) {
652 return;
653 }
654
655 static const int numRetries = 2;
656
657 QHash<QByteArray, QVariant> data = m_model->data(index);
658 QVector<QPixmap> pixmaps = data["hoverSequencePixmaps"].value<QVector<QPixmap>>();
659
660 qCDebug(DolphinDebug).nospace() << "Failed to generate hover sequence preview #" << pixmaps.size() << " for file " << item.url().toString() << " (attempt "
661 << (m_hoverSequenceNumSuccessiveFailures + 1) << "/" << (numRetries + 1) << ")";
662
663 if (m_hoverSequenceNumSuccessiveFailures >= numRetries) {
664 // Give up and simply duplicate the previous sequence image (if any)
665
666 pixmaps.append(pixmaps.empty() ? QPixmap() : pixmaps.last());
667 data["hoverSequencePixmaps"] = QVariant::fromValue(pixmaps);
668
669 if (!data.contains("hoverSequenceWraparoundPoint")) {
670 // hoverSequenceWraparoundPoint is only available when PreviewJob succeeds, so unless
671 // it has previously succeeded, it's best to assume that it just doesn't handle
672 // sequences instead of trying to load the next image indefinitely.
673 data["hoverSequenceWraparoundPoint"] = 1.0f;
674 }
675
676 m_model->setData(index, data);
677
678 m_hoverSequenceNumSuccessiveFailures = 0;
679 } else {
680 // Retry
681
682 m_hoverSequenceNumSuccessiveFailures++;
683 }
684
685 // Next image in the sequence (or same one if the retry limit wasn't reached yet) will be
686 // loaded automatically, because slotHoverSequencePreviewJobFinished() will be triggered
687 // even when PreviewJob fails.
688 }
689
690 void KFileItemModelRolesUpdater::slotHoverSequencePreviewJobFinished()
691 {
692 const int index = m_model->index(m_hoverSequenceItem);
693 if (index < 0) {
694 m_hoverSequencePreviewJob = nullptr;
695 return;
696 }
697
698 // Since a PreviewJob can only have one associated sequence index, we can only generate
699 // one sequence image per job, so we have to start another one for the next index.
700
701 // Load the next image in the sequence
702 m_hoverSequencePreviewJob = nullptr;
703 loadNextHoverSequencePreview();
704 }
705
706 void KFileItemModelRolesUpdater::resolveNextSortRole()
707 {
708 if (m_state != ResolvingSortRole) {
709 return;
710 }
711
712 QSet<KFileItem>::iterator it = m_pendingSortRoleItems.begin();
713 while (it != m_pendingSortRoleItems.end()) {
714 const KFileItem item = *it;
715 const int index = m_model->index(item);
716
717 // Continue if the sort role has already been determined for the
718 // item, and the item has not been changed recently.
719 if (!m_changedItems.contains(item) && m_model->data(index).contains(m_model->sortRole())) {
720 it = m_pendingSortRoleItems.erase(it);
721 continue;
722 }
723
724 applySortRole(index);
725 m_pendingSortRoleItems.erase(it);
726 break;
727 }
728
729 if (!m_pendingSortRoleItems.isEmpty()) {
730 applySortProgressToModel();
731 QTimer::singleShot(0, this, &KFileItemModelRolesUpdater::resolveNextSortRole);
732 } else {
733 m_state = Idle;
734
735 // Prevent that we try to update the items twice.
736 disconnect(m_model, &KFileItemModel::itemsMoved, this, &KFileItemModelRolesUpdater::slotItemsMoved);
737 applySortProgressToModel();
738 connect(m_model, &KFileItemModel::itemsMoved, this, &KFileItemModelRolesUpdater::slotItemsMoved);
739 startUpdating();
740 }
741 }
742
743 void KFileItemModelRolesUpdater::resolveNextPendingRoles()
744 {
745 if (m_state != ResolvingAllRoles) {
746 return;
747 }
748
749 while (!m_pendingIndexes.isEmpty()) {
750 const int index = m_pendingIndexes.takeFirst();
751 const KFileItem item = m_model->fileItem(index);
752
753 if (m_finishedItems.contains(item)) {
754 continue;
755 }
756
757 applyResolvedRoles(index, ResolveAll);
758 m_finishedItems.insert(item);
759 m_changedItems.remove(item);
760 break;
761 }
762
763 if (!m_pendingIndexes.isEmpty()) {
764 QTimer::singleShot(0, this, &KFileItemModelRolesUpdater::resolveNextPendingRoles);
765 } else {
766 m_state = Idle;
767
768 if (m_clearPreviews) {
769 // Only go through the list if there are items which might still have previews.
770 if (m_finishedItems.count() != m_model->count()) {
771 QHash<QByteArray, QVariant> data;
772 data.insert("iconPixmap", QPixmap());
773 data.insert("hoverSequencePixmaps", QVariant::fromValue(QVector<QPixmap>()));
774
775 disconnect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged);
776 for (int index = 0; index <= m_model->count(); ++index) {
777 if (m_model->data(index).contains("iconPixmap") || m_model->data(index).contains("hoverSequencePixmaps")) {
778 m_model->setData(index, data);
779 }
780 }
781 connect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged);
782 }
783 m_clearPreviews = false;
784 }
785
786 if (!m_changedItems.isEmpty()) {
787 updateChangedItems();
788 }
789 }
790 }
791
792 void KFileItemModelRolesUpdater::resolveRecentlyChangedItems()
793 {
794 m_changedItems += m_recentlyChangedItems;
795 m_recentlyChangedItems.clear();
796 updateChangedItems();
797 }
798
799 void KFileItemModelRolesUpdater::applyChangedBalooRoles(const QString &file)
800 {
801 #if HAVE_BALOO
802 const KFileItem item = m_model->fileItem(QUrl::fromLocalFile(file));
803
804 if (item.isNull()) {
805 // itemUrl is not in the model anymore, probably because
806 // the corresponding file has been deleted in the meantime.
807 return;
808 }
809 applyChangedBalooRolesForItem(item);
810 #else
811 Q_UNUSED(file)
812 #endif
813 }
814
815 void KFileItemModelRolesUpdater::applyChangedBalooRolesForItem(const KFileItem &item)
816 {
817 #if HAVE_BALOO
818 Baloo::File file(item.localPath());
819 file.load();
820
821 const KBalooRolesProvider &rolesProvider = KBalooRolesProvider::instance();
822 QHash<QByteArray, QVariant> data;
823
824 const auto roles = rolesProvider.roles();
825 for (const QByteArray &role : roles) {
826 // Overwrite all the role values with an empty QVariant, because the roles
827 // provider doesn't overwrite it when the property value list is empty.
828 // See bug 322348
829 data.insert(role, QVariant());
830 }
831
832 QHashIterator<QByteArray, QVariant> it(rolesProvider.roleValues(file, m_roles));
833 while (it.hasNext()) {
834 it.next();
835 data.insert(it.key(), it.value());
836 }
837
838 disconnect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged);
839 const int index = m_model->index(item);
840 m_model->setData(index, data);
841 connect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged);
842 #else
843 #ifndef Q_CC_MSVC
844 Q_UNUSED(item)
845 #endif
846 #endif
847 }
848
849 void KFileItemModelRolesUpdater::slotDirectoryContentsCountReceived(const QString &path, int count, long long size)
850 {
851 const bool getIsExpandableRole = m_roles.contains("isExpandable");
852 const bool getSizeRole = m_roles.contains("size");
853
854 if (getSizeRole || getIsExpandableRole) {
855 const int index = m_model->index(QUrl::fromLocalFile(path));
856 if (index >= 0) {
857 QHash<QByteArray, QVariant> data;
858
859 if (getSizeRole) {
860 data.insert("count", count);
861 data.insert("size", QVariant::fromValue(size));
862 }
863 if (getIsExpandableRole) {
864 data.insert("isExpandable", count > 0);
865 }
866
867 disconnect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged);
868 m_model->setData(index, data);
869 connect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged);
870 }
871 }
872 }
873
874 void KFileItemModelRolesUpdater::startUpdating()
875 {
876 if (m_state == Paused) {
877 return;
878 }
879
880 if (m_finishedItems.count() == m_model->count()) {
881 // All roles have been resolved already.
882 m_state = Idle;
883 return;
884 }
885
886 // Terminate all updates that are currently active.
887 killPreviewJob();
888 m_pendingIndexes.clear();
889
890 QElapsedTimer timer;
891 timer.start();
892
893 // Determine the icons for the visible items synchronously.
894 updateVisibleIcons();
895
896 // A detailed update of the items in and near the visible area
897 // only makes sense if sorting is finished.
898 if (m_state == ResolvingSortRole) {
899 return;
900 }
901
902 // Start the preview job or the asynchronous resolving of all roles.
903 QList<int> indexes = indexesToResolve();
904
905 if (m_previewShown) {
906 m_pendingPreviewItems.clear();
907 m_pendingPreviewItems.reserve(indexes.count());
908
909 for (int index : qAsConst(indexes)) {
910 const KFileItem item = m_model->fileItem(index);
911 if (!m_finishedItems.contains(item)) {
912 m_pendingPreviewItems.append(item);
913 }
914 }
915
916 startPreviewJob();
917 } else {
918 m_pendingIndexes = indexes;
919 // Trigger the asynchronous resolving of all roles.
920 m_state = ResolvingAllRoles;
921 QTimer::singleShot(0, this, &KFileItemModelRolesUpdater::resolveNextPendingRoles);
922 }
923 }
924
925 void KFileItemModelRolesUpdater::updateVisibleIcons()
926 {
927 int lastVisibleIndex = m_lastVisibleIndex;
928 if (lastVisibleIndex <= 0) {
929 // Guess a reasonable value for the last visible index if the view
930 // has not told us about the real value yet.
931 lastVisibleIndex = qMin(m_firstVisibleIndex + m_maximumVisibleItems, m_model->count() - 1);
932 if (lastVisibleIndex <= 0) {
933 lastVisibleIndex = qMin(200, m_model->count() - 1);
934 }
935 }
936
937 QElapsedTimer timer;
938 timer.start();
939
940 // Try to determine the final icons for all visible items.
941 int index;
942 for (index = m_firstVisibleIndex; index <= lastVisibleIndex && timer.elapsed() < MaxBlockTimeout; ++index) {
943 applyResolvedRoles(index, ResolveFast);
944 }
945
946 // KFileItemListView::initializeItemListWidget(KItemListWidget*) will load
947 // preliminary icons (i.e., without mime type determination) for the
948 // remaining items.
949 }
950
951 void KFileItemModelRolesUpdater::startPreviewJob()
952 {
953 m_state = PreviewJobRunning;
954
955 if (m_pendingPreviewItems.isEmpty()) {
956 QTimer::singleShot(0, this, &KFileItemModelRolesUpdater::slotPreviewJobFinished);
957 return;
958 }
959
960 // PreviewJob internally caches items always with the size of
961 // 128 x 128 pixels or 256 x 256 pixels. A (slow) downscaling is done
962 // by PreviewJob if a smaller size is requested. For images KFileItemModelRolesUpdater must
963 // do a downscaling anyhow because of the frame, so in this case only the provided
964 // cache sizes are requested.
965 const QSize cacheSize = (m_iconSize.width() > 128) || (m_iconSize.height() > 128) ? QSize(256, 256) : QSize(128, 128);
966
967 // KIO::filePreview() will request the MIME-type of all passed items, which (in the
968 // worst case) might block the application for several seconds. To prevent such
969 // a blocking, we only pass items with known mime type to the preview job.
970 const int count = m_pendingPreviewItems.count();
971 KFileItemList itemSubSet;
972 itemSubSet.reserve(count);
973
974 if (m_pendingPreviewItems.first().isMimeTypeKnown()) {
975 // Some mime types are known already, probably because they were
976 // determined when loading the icons for the visible items. Start
977 // a preview job for all items at the beginning of the list which
978 // have a known mime type.
979 do {
980 itemSubSet.append(m_pendingPreviewItems.takeFirst());
981 } while (!m_pendingPreviewItems.isEmpty() && m_pendingPreviewItems.first().isMimeTypeKnown());
982 } else {
983 // Determine mime types for MaxBlockTimeout ms, and start a preview
984 // job for the corresponding items.
985 QElapsedTimer timer;
986 timer.start();
987
988 do {
989 const KFileItem item = m_pendingPreviewItems.takeFirst();
990 item.determineMimeType();
991 itemSubSet.append(item);
992 } while (!m_pendingPreviewItems.isEmpty() && timer.elapsed() < MaxBlockTimeout);
993 }
994
995 KIO::PreviewJob *job = new KIO::PreviewJob(itemSubSet, cacheSize, &m_enabledPlugins);
996
997 job->setIgnoreMaximumSize(itemSubSet.first().isLocalFile() && !itemSubSet.first().isSlow() && m_localFileSizePreviewLimit <= 0);
998 if (job->uiDelegate()) {
999 KJobWidgets::setWindow(job, qApp->activeWindow());
1000 }
1001
1002 connect(job, &KIO::PreviewJob::gotPreview, this, &KFileItemModelRolesUpdater::slotGotPreview);
1003 connect(job, &KIO::PreviewJob::failed, this, &KFileItemModelRolesUpdater::slotPreviewFailed);
1004 connect(job, &KIO::PreviewJob::finished, this, &KFileItemModelRolesUpdater::slotPreviewJobFinished);
1005
1006 m_previewJob = job;
1007 }
1008
1009 QPixmap KFileItemModelRolesUpdater::transformPreviewPixmap(const QPixmap &pixmap)
1010 {
1011 QPixmap scaledPixmap = pixmap;
1012
1013 if (!pixmap.hasAlpha() && !pixmap.isNull() && m_iconSize.width() > KIconLoader::SizeSmallMedium && m_iconSize.height() > KIconLoader::SizeSmallMedium) {
1014 if (m_enlargeSmallPreviews) {
1015 KPixmapModifier::applyFrame(scaledPixmap, m_iconSize);
1016 } else {
1017 // Assure that small previews don't get enlarged. Instead they
1018 // should be shown centered within the frame.
1019 const QSize contentSize = KPixmapModifier::sizeInsideFrame(m_iconSize);
1020 const bool enlargingRequired = scaledPixmap.width() < contentSize.width() && scaledPixmap.height() < contentSize.height();
1021 if (enlargingRequired) {
1022 QSize frameSize = scaledPixmap.size() / scaledPixmap.devicePixelRatio();
1023 frameSize.scale(m_iconSize, Qt::KeepAspectRatio);
1024
1025 QPixmap largeFrame(frameSize);
1026 largeFrame.fill(Qt::transparent);
1027
1028 KPixmapModifier::applyFrame(largeFrame, frameSize);
1029
1030 QPainter painter(&largeFrame);
1031 painter.drawPixmap((largeFrame.width() - scaledPixmap.width() / scaledPixmap.devicePixelRatio()) / 2,
1032 (largeFrame.height() - scaledPixmap.height() / scaledPixmap.devicePixelRatio()) / 2,
1033 scaledPixmap);
1034 scaledPixmap = largeFrame;
1035 } else {
1036 // The image must be shrunk as it is too large to fit into
1037 // the available icon size
1038 KPixmapModifier::applyFrame(scaledPixmap, m_iconSize);
1039 }
1040 }
1041 } else if (!pixmap.isNull()) {
1042 KPixmapModifier::scale(scaledPixmap, m_iconSize * qApp->devicePixelRatio());
1043 scaledPixmap.setDevicePixelRatio(qApp->devicePixelRatio());
1044 }
1045
1046 return scaledPixmap;
1047 }
1048
1049 void KFileItemModelRolesUpdater::loadNextHoverSequencePreview()
1050 {
1051 if (m_hoverSequenceItem.isNull() || m_hoverSequencePreviewJob) {
1052 return;
1053 }
1054
1055 const int index = m_model->index(m_hoverSequenceItem);
1056 if (index < 0) {
1057 return;
1058 }
1059
1060 // We generate the next few sequence indices in advance (buffering)
1061 const int maxSeqIdx = m_hoverSequenceIndex + 5;
1062
1063 QHash<QByteArray, QVariant> data = m_model->data(index);
1064
1065 if (!data.contains("hoverSequencePixmaps")) {
1066 // The pixmap at index 0 isn't used ("iconPixmap" will be used instead)
1067 data.insert("hoverSequencePixmaps", QVariant::fromValue(QVector<QPixmap>() << QPixmap()));
1068 m_model->setData(index, data);
1069 }
1070
1071 const QVector<QPixmap> pixmaps = data["hoverSequencePixmaps"].value<QVector<QPixmap>>();
1072
1073 const int loadSeqIdx = pixmaps.size();
1074
1075 float wap = -1.0f;
1076 if (data.contains("hoverSequenceWraparoundPoint")) {
1077 wap = data["hoverSequenceWraparoundPoint"].toFloat();
1078 }
1079 if (wap >= 1.0f && loadSeqIdx >= static_cast<int>(wap)) {
1080 // Reached the wraparound point -> no more previews to load.
1081 return;
1082 }
1083
1084 if (loadSeqIdx > maxSeqIdx) {
1085 // Wait until setHoverSequenceState() is called with a higher sequence index.
1086 return;
1087 }
1088
1089 // PreviewJob internally caches items always with the size of
1090 // 128 x 128 pixels or 256 x 256 pixels. A (slow) downscaling is done
1091 // by PreviewJob if a smaller size is requested. For images KFileItemModelRolesUpdater must
1092 // do a downscaling anyhow because of the frame, so in this case only the provided
1093 // cache sizes are requested.
1094 const QSize cacheSize = (m_iconSize.width() > 128) || (m_iconSize.height() > 128) ? QSize(256, 256) : QSize(128, 128);
1095
1096 KIO::PreviewJob *job = new KIO::PreviewJob({m_hoverSequenceItem}, cacheSize, &m_enabledPlugins);
1097
1098 job->setSequenceIndex(loadSeqIdx);
1099 job->setIgnoreMaximumSize(m_hoverSequenceItem.isLocalFile() && !m_hoverSequenceItem.isSlow() && m_localFileSizePreviewLimit <= 0);
1100 if (job->uiDelegate()) {
1101 KJobWidgets::setWindow(job, qApp->activeWindow());
1102 }
1103
1104 connect(job, &KIO::PreviewJob::gotPreview, this, &KFileItemModelRolesUpdater::slotHoverSequenceGotPreview);
1105 connect(job, &KIO::PreviewJob::failed, this, &KFileItemModelRolesUpdater::slotHoverSequencePreviewFailed);
1106 connect(job, &KIO::PreviewJob::finished, this, &KFileItemModelRolesUpdater::slotHoverSequencePreviewJobFinished);
1107
1108 m_hoverSequencePreviewJob = job;
1109 }
1110
1111 void KFileItemModelRolesUpdater::killHoverSequencePreviewJob()
1112 {
1113 if (m_hoverSequencePreviewJob) {
1114 disconnect(m_hoverSequencePreviewJob, &KIO::PreviewJob::gotPreview, this, &KFileItemModelRolesUpdater::slotHoverSequenceGotPreview);
1115 disconnect(m_hoverSequencePreviewJob, &KIO::PreviewJob::failed, this, &KFileItemModelRolesUpdater::slotHoverSequencePreviewFailed);
1116 disconnect(m_hoverSequencePreviewJob, &KIO::PreviewJob::finished, this, &KFileItemModelRolesUpdater::slotHoverSequencePreviewJobFinished);
1117 m_hoverSequencePreviewJob->kill();
1118 m_hoverSequencePreviewJob = nullptr;
1119 }
1120 }
1121
1122 void KFileItemModelRolesUpdater::updateChangedItems()
1123 {
1124 if (m_state == Paused) {
1125 return;
1126 }
1127
1128 if (m_changedItems.isEmpty()) {
1129 return;
1130 }
1131
1132 m_finishedItems -= m_changedItems;
1133
1134 if (m_resolvableRoles.contains(m_model->sortRole())) {
1135 m_pendingSortRoleItems += m_changedItems;
1136
1137 if (m_state != ResolvingSortRole) {
1138 // Stop the preview job if necessary, and trigger the
1139 // asynchronous determination of the sort role.
1140 killPreviewJob();
1141 m_state = ResolvingSortRole;
1142 QTimer::singleShot(0, this, &KFileItemModelRolesUpdater::resolveNextSortRole);
1143 }
1144
1145 return;
1146 }
1147
1148 QList<int> visibleChangedIndexes;
1149 QList<int> invisibleChangedIndexes;
1150 visibleChangedIndexes.reserve(m_changedItems.size());
1151 invisibleChangedIndexes.reserve(m_changedItems.size());
1152
1153 auto changedItemsIt = m_changedItems.begin();
1154 while (changedItemsIt != m_changedItems.end()) {
1155 const auto &item = *changedItemsIt;
1156 const int index = m_model->index(item);
1157
1158 if (index < 0) {
1159 changedItemsIt = m_changedItems.erase(changedItemsIt);
1160 continue;
1161 }
1162 ++changedItemsIt;
1163
1164 if (index >= m_firstVisibleIndex && index <= m_lastVisibleIndex) {
1165 visibleChangedIndexes.append(index);
1166 } else {
1167 invisibleChangedIndexes.append(index);
1168 }
1169 }
1170
1171 std::sort(visibleChangedIndexes.begin(), visibleChangedIndexes.end());
1172
1173 if (m_previewShown) {
1174 for (int index : qAsConst(visibleChangedIndexes)) {
1175 m_pendingPreviewItems.append(m_model->fileItem(index));
1176 }
1177
1178 for (int index : qAsConst(invisibleChangedIndexes)) {
1179 m_pendingPreviewItems.append(m_model->fileItem(index));
1180 }
1181
1182 if (!m_previewJob) {
1183 startPreviewJob();
1184 }
1185 } else {
1186 const bool resolvingInProgress = !m_pendingIndexes.isEmpty();
1187 m_pendingIndexes = visibleChangedIndexes + m_pendingIndexes + invisibleChangedIndexes;
1188 if (!resolvingInProgress) {
1189 // Trigger the asynchronous resolving of the changed roles.
1190 m_state = ResolvingAllRoles;
1191 QTimer::singleShot(0, this, &KFileItemModelRolesUpdater::resolveNextPendingRoles);
1192 }
1193 }
1194 }
1195
1196 void KFileItemModelRolesUpdater::applySortRole(int index)
1197 {
1198 QHash<QByteArray, QVariant> data;
1199 const KFileItem item = m_model->fileItem(index);
1200
1201 if (m_model->sortRole() == "type") {
1202 if (!item.isMimeTypeKnown()) {
1203 item.determineMimeType();
1204 }
1205
1206 data.insert("type", item.mimeComment());
1207 } else if (m_model->sortRole() == "size" && item.isLocalFile() && item.isDir()) {
1208 startDirectorySizeCounting(item, index);
1209 return;
1210 } else {
1211 // Probably the sort role is a baloo role - just determine all roles.
1212 data = rolesData(item, index);
1213 }
1214
1215 disconnect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged);
1216 m_model->setData(index, data);
1217 connect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged);
1218 }
1219
1220 void KFileItemModelRolesUpdater::applySortProgressToModel()
1221 {
1222 // Inform the model about the progress of the resolved items,
1223 // so that it can give an indication when the sorting has been finished.
1224 const int resolvedCount = m_model->count() - m_pendingSortRoleItems.count();
1225 m_model->emitSortProgress(resolvedCount);
1226 }
1227
1228 bool KFileItemModelRolesUpdater::applyResolvedRoles(int index, ResolveHint hint)
1229 {
1230 const KFileItem item = m_model->fileItem(index);
1231 const bool resolveAll = (hint == ResolveAll);
1232
1233 bool iconChanged = false;
1234 if (!item.isMimeTypeKnown() || !item.isFinalIconKnown()) {
1235 item.determineMimeType();
1236 iconChanged = true;
1237 } else if (!m_model->data(index).contains("iconName")) {
1238 iconChanged = true;
1239 }
1240
1241 if (iconChanged || resolveAll || m_clearPreviews) {
1242 if (index < 0) {
1243 return false;
1244 }
1245
1246 QHash<QByteArray, QVariant> data;
1247 if (resolveAll) {
1248 data = rolesData(item, index);
1249 }
1250
1251 if (!item.iconName().isEmpty()) {
1252 data.insert("iconName", item.iconName());
1253 }
1254
1255 if (m_clearPreviews) {
1256 data.insert("iconPixmap", QPixmap());
1257 data.insert("hoverSequencePixmaps", QVariant::fromValue(QVector<QPixmap>()));
1258 }
1259
1260 disconnect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged);
1261 m_model->setData(index, data);
1262 connect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged);
1263 return true;
1264 }
1265
1266 return false;
1267 }
1268
1269 void KFileItemModelRolesUpdater::startDirectorySizeCounting(const KFileItem &item, int index)
1270 {
1271 if (ContentDisplaySettings::directorySizeCount() || item.isSlow() || !item.isLocalFile()) {
1272 // fastpath no recursion necessary
1273
1274 auto data = m_model->data(index);
1275 if (data.value("size") == -2) {
1276 // means job already started
1277 return;
1278 }
1279
1280 auto url = item.url();
1281 if (!item.localPath().isEmpty()) {
1282 // optimization for desktop:/, trash:/
1283 url = QUrl::fromLocalFile(item.localPath());
1284 }
1285
1286 data.insert("isExpandable", false);
1287 data.insert("count", 0);
1288 data.insert("size", -2); // invalid size, -1 means size unknown
1289
1290 disconnect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged);
1291 m_model->setData(index, data);
1292 connect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged);
1293
1294 auto listJob = KIO::listDir(url);
1295 QObject::connect(listJob, &KIO::ListJob::entries, this, [this, index](const KJob * /*job*/, const KIO::UDSEntryList &list) {
1296 auto data = m_model->data(index);
1297 int origCount = data.value("count").toInt();
1298 int entryCount = origCount;
1299
1300 for (const KIO::UDSEntry &entry : list) {
1301 const auto name = entry.stringValue(KIO::UDSEntry::UDS_NAME);
1302
1303 if (name == QStringLiteral("..") || name == QStringLiteral(".")) {
1304 continue;
1305 }
1306 if (!m_model->showHiddenFiles() && name.startsWith(QLatin1Char('.'))) {
1307 continue;
1308 }
1309 if (m_model->showDirectoriesOnly() && !entry.isDir()) {
1310 continue;
1311 }
1312 ++entryCount;
1313 }
1314
1315 // count has changed
1316 if (origCount < entryCount) {
1317 QHash<QByteArray, QVariant> data;
1318 data.insert("isExpandable", entryCount > 0);
1319 data.insert("count", entryCount);
1320
1321 disconnect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged);
1322 m_model->setData(index, data);
1323 connect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged);
1324 }
1325 });
1326 return;
1327 }
1328
1329 // Tell m_directoryContentsCounter that we want to count the items
1330 // inside the directory. The result will be received in slotDirectoryContentsCountReceived.
1331 const QString path = item.localPath();
1332 const auto priority = index >= m_firstVisibleIndex && index <= m_lastVisibleIndex ? KDirectoryContentsCounter::PathCountPriority::High
1333 : KDirectoryContentsCounter::PathCountPriority::Normal;
1334
1335 m_directoryContentsCounter->scanDirectory(path, priority);
1336 }
1337
1338 QHash<QByteArray, QVariant> KFileItemModelRolesUpdater::rolesData(const KFileItem &item, int index)
1339 {
1340 QHash<QByteArray, QVariant> data;
1341
1342 const bool getSizeRole = m_roles.contains("size");
1343 const bool getIsExpandableRole = m_roles.contains("isExpandable");
1344
1345 if ((getSizeRole || getIsExpandableRole) && item.isDir()) {
1346 startDirectorySizeCounting(item, index);
1347 }
1348
1349 if (m_roles.contains("extension")) {
1350 // TODO KF6 use KFileItem::suffix 464722
1351 data.insert("extension", QFileInfo(item.name()).suffix());
1352 }
1353
1354 if (m_roles.contains("type")) {
1355 data.insert("type", item.mimeComment());
1356 }
1357
1358 QStringList overlays = item.overlays();
1359 for (KOverlayIconPlugin *it : qAsConst(m_overlayIconsPlugin)) {
1360 overlays.append(it->getOverlays(item.url()));
1361 }
1362 if (!overlays.isEmpty()) {
1363 data.insert("iconOverlays", overlays);
1364 }
1365
1366 #if HAVE_BALOO
1367 if (m_balooFileMonitor) {
1368 m_balooFileMonitor->addFile(item.localPath());
1369 applyChangedBalooRolesForItem(item);
1370 }
1371 #endif
1372 return data;
1373 }
1374
1375 void KFileItemModelRolesUpdater::slotOverlaysChanged(const QUrl &url, const QStringList &)
1376 {
1377 const KFileItem item = m_model->fileItem(url);
1378 if (item.isNull()) {
1379 return;
1380 }
1381 const int index = m_model->index(item);
1382 QHash<QByteArray, QVariant> data = m_model->data(index);
1383 QStringList overlays = item.overlays();
1384 for (KOverlayIconPlugin *it : qAsConst(m_overlayIconsPlugin)) {
1385 overlays.append(it->getOverlays(url));
1386 }
1387 data.insert("iconOverlays", overlays);
1388 m_model->setData(index, data);
1389 }
1390
1391 void KFileItemModelRolesUpdater::updateAllPreviews()
1392 {
1393 if (m_state == Paused) {
1394 m_previewChangedDuringPausing = true;
1395 } else {
1396 m_finishedItems.clear();
1397 startUpdating();
1398 }
1399 }
1400
1401 void KFileItemModelRolesUpdater::killPreviewJob()
1402 {
1403 if (m_previewJob) {
1404 disconnect(m_previewJob, &KIO::PreviewJob::gotPreview, this, &KFileItemModelRolesUpdater::slotGotPreview);
1405 disconnect(m_previewJob, &KIO::PreviewJob::failed, this, &KFileItemModelRolesUpdater::slotPreviewFailed);
1406 disconnect(m_previewJob, &KIO::PreviewJob::finished, this, &KFileItemModelRolesUpdater::slotPreviewJobFinished);
1407 m_previewJob->kill();
1408 m_previewJob = nullptr;
1409 m_pendingPreviewItems.clear();
1410 }
1411 }
1412
1413 QList<int> KFileItemModelRolesUpdater::indexesToResolve() const
1414 {
1415 const int count = m_model->count();
1416
1417 QList<int> result;
1418 result.reserve(qMin(count, (m_lastVisibleIndex - m_firstVisibleIndex + 1) + ResolveAllItemsLimit + (2 * m_maximumVisibleItems)));
1419
1420 // Add visible items.
1421 // Resolve files first, their previews are quicker.
1422 QList<int> visibleDirs;
1423 for (int i = m_firstVisibleIndex; i <= m_lastVisibleIndex; ++i) {
1424 const KFileItem item = m_model->fileItem(i);
1425 if (item.isDir()) {
1426 visibleDirs.append(i);
1427 } else {
1428 result.append(i);
1429 }
1430 }
1431
1432 result.append(visibleDirs);
1433
1434 // We need a reasonable upper limit for number of items to resolve after
1435 // and before the visible range. m_maximumVisibleItems can be quite large
1436 // when using Compact View.
1437 const int readAheadItems = qMin(ReadAheadPages * m_maximumVisibleItems, ResolveAllItemsLimit / 2);
1438
1439 // Add items after the visible range.
1440 const int endExtendedVisibleRange = qMin(m_lastVisibleIndex + readAheadItems, count - 1);
1441 for (int i = m_lastVisibleIndex + 1; i <= endExtendedVisibleRange; ++i) {
1442 result.append(i);
1443 }
1444
1445 // Add items before the visible range in reverse order.
1446 const int beginExtendedVisibleRange = qMax(0, m_firstVisibleIndex - readAheadItems);
1447 for (int i = m_firstVisibleIndex - 1; i >= beginExtendedVisibleRange; --i) {
1448 result.append(i);
1449 }
1450
1451 // Add items on the last page.
1452 const int beginLastPage = qMax(endExtendedVisibleRange + 1, count - m_maximumVisibleItems);
1453 for (int i = beginLastPage; i < count; ++i) {
1454 result.append(i);
1455 }
1456
1457 // Add items on the first page.
1458 const int endFirstPage = qMin(beginExtendedVisibleRange, m_maximumVisibleItems);
1459 for (int i = 0; i < endFirstPage; ++i) {
1460 result.append(i);
1461 }
1462
1463 // Continue adding items until ResolveAllItemsLimit is reached.
1464 int remainingItems = ResolveAllItemsLimit - result.count();
1465
1466 for (int i = endExtendedVisibleRange + 1; i < beginLastPage && remainingItems > 0; ++i) {
1467 result.append(i);
1468 --remainingItems;
1469 }
1470
1471 for (int i = beginExtendedVisibleRange - 1; i >= endFirstPage && remainingItems > 0; --i) {
1472 result.append(i);
1473 --remainingItems;
1474 }
1475
1476 return result;
1477 }
1478
1479 void KFileItemModelRolesUpdater::trimHoverSequenceLoadedItems()
1480 {
1481 static const size_t maxLoadedItems = 20;
1482
1483 size_t loadedItems = m_hoverSequenceLoadedItems.size();
1484 while (loadedItems > maxLoadedItems) {
1485 const KFileItem item = m_hoverSequenceLoadedItems.front();
1486
1487 m_hoverSequenceLoadedItems.pop_front();
1488 loadedItems--;
1489
1490 const int index = m_model->index(item);
1491 if (index >= 0) {
1492 QHash<QByteArray, QVariant> data = m_model->data(index);
1493 data["hoverSequencePixmaps"] = QVariant::fromValue(QVector<QPixmap>() << QPixmap());
1494 m_model->setData(index, data);
1495 }
1496 }
1497 }