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