]> cloud.milkyroute.net Git - dolphin.git/blob - src/kitemviews/kfileitemmodelrolesupdater.cpp
GIT_SILENT Update Appstream for new release
[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 QList<QUrl> dirsWithAddedItems;
374
375 int insertedCount = 0;
376 for (const KItemRange &range : itemRanges) {
377 const int lastIndex = insertedCount + range.index + range.count - 1;
378 for (int i = insertedCount + range.index; i <= lastIndex; ++i) {
379 const auto fileItem = m_model->fileItem(i);
380 const auto fileItemParentFolderUrl = fileItem.url().adjusted(QUrl::RemoveFilename);
381 if (!dirsWithAddedItems.contains(fileItemParentFolderUrl)) {
382 dirsWithAddedItems.append(fileItemParentFolderUrl);
383 }
384 if (timer.elapsed() < MaxBlockTimeout) {
385 applySortRole(i);
386 } else {
387 m_pendingSortRoleItems.insert(fileItem);
388 }
389 }
390 insertedCount += range.count;
391 }
392
393 recountDirectoryItems(dirsWithAddedItems);
394
395 applySortProgressToModel();
396
397 // If there are still items whose sort role is unknown, check if the
398 // asynchronous determination of the sort role is already in progress,
399 // and start it if that is not the case.
400 if (!m_pendingSortRoleItems.isEmpty() && m_state != ResolvingSortRole) {
401 killPreviewJob();
402 m_state = ResolvingSortRole;
403 resolveNextSortRole();
404 }
405 }
406
407 startUpdating();
408 }
409
410 void KFileItemModelRolesUpdater::slotItemsRemoved(const KItemRangeList &itemRanges)
411 {
412 Q_UNUSED(itemRanges)
413
414 const bool allItemsRemoved = (m_model->count() == 0);
415
416 #if HAVE_BALOO
417 if (m_balooFileMonitor) {
418 // Don't let the FileWatcher watch for removed items
419 if (allItemsRemoved) {
420 m_balooFileMonitor->clear();
421 } else {
422 QStringList newFileList;
423 const QStringList oldFileList = m_balooFileMonitor->files();
424 for (const QString &file : oldFileList) {
425 if (m_model->index(QUrl::fromLocalFile(file)) >= 0) {
426 newFileList.append(file);
427 }
428 }
429 m_balooFileMonitor->setFiles(newFileList);
430 }
431 }
432 #endif
433
434 if (allItemsRemoved) {
435 m_state = Idle;
436
437 m_finishedItems.clear();
438 m_pendingSortRoleItems.clear();
439 m_pendingIndexes.clear();
440 m_pendingPreviewItems.clear();
441 m_recentlyChangedItems.clear();
442 m_recentlyChangedItemsTimer->stop();
443 m_changedItems.clear();
444 m_hoverSequenceLoadedItems.clear();
445
446 killPreviewJob();
447 if (!m_model->showDirectoriesOnly()) {
448 m_directoryContentsCounter->stopWorker();
449 }
450 } else {
451 QList<QUrl> dirsWithDeletedItems;
452 // Only remove the items from m_finishedItems. They will be removed
453 // from the other sets later on.
454 QSet<KFileItem>::iterator it = m_finishedItems.begin();
455 while (it != m_finishedItems.end()) {
456 if (m_model->index(*it) < 0) {
457 // Get the folder path of the file.
458 const auto folderUrl = it->url().adjusted(QUrl::RemoveFilename);
459 if (!dirsWithDeletedItems.contains(folderUrl)) {
460 dirsWithDeletedItems.append(folderUrl);
461 }
462 it = m_finishedItems.erase(it);
463 } else {
464 ++it;
465 }
466 }
467
468 recountDirectoryItems(dirsWithDeletedItems);
469
470 // Removed items won't have hover previews loaded anymore.
471 for (const KItemRange &itemRange : itemRanges) {
472 int index = itemRange.index;
473 for (int count = itemRange.count; count > 0; --count) {
474 const KFileItem item = m_model->fileItem(index);
475 m_hoverSequenceLoadedItems.remove(item);
476 ++index;
477 }
478 }
479
480 // The visible items might have changed.
481 startUpdating();
482 }
483 }
484
485 void KFileItemModelRolesUpdater::slotItemsMoved(KItemRange itemRange, const QList<int> &movedToIndexes)
486 {
487 Q_UNUSED(itemRange)
488 Q_UNUSED(movedToIndexes)
489
490 // The visible items might have changed.
491 startUpdating();
492 }
493
494 void KFileItemModelRolesUpdater::slotItemsChanged(const KItemRangeList &itemRanges, const QSet<QByteArray> &roles)
495 {
496 Q_UNUSED(roles)
497
498 // Find out if slotItemsChanged() has been done recently. If that is the
499 // case, resolving the roles is postponed until a timer has exceeded
500 // to prevent expensive repeated updates if files are updated frequently.
501 const bool itemsChangedRecently = m_recentlyChangedItemsTimer->isActive();
502
503 QSet<KFileItem> &targetSet = itemsChangedRecently ? m_recentlyChangedItems : m_changedItems;
504
505 for (const KItemRange &itemRange : itemRanges) {
506 int index = itemRange.index;
507 for (int count = itemRange.count; count > 0; --count) {
508 const KFileItem item = m_model->fileItem(index);
509 targetSet.insert(item);
510 ++index;
511 }
512 }
513
514 m_recentlyChangedItemsTimer->start();
515
516 if (!itemsChangedRecently) {
517 updateChangedItems();
518 }
519 }
520
521 void KFileItemModelRolesUpdater::slotSortRoleChanged(const QByteArray &current, const QByteArray &previous)
522 {
523 Q_UNUSED(current)
524 Q_UNUSED(previous)
525
526 if (m_resolvableRoles.contains(current)) {
527 m_pendingSortRoleItems.clear();
528 m_finishedItems.clear();
529
530 const int count = m_model->count();
531 QElapsedTimer timer;
532 timer.start();
533
534 // Determine the sort role synchronously for as many items as possible.
535 for (int index = 0; index < count; ++index) {
536 if (timer.elapsed() < MaxBlockTimeout) {
537 applySortRole(index);
538 } else {
539 m_pendingSortRoleItems.insert(m_model->fileItem(index));
540 }
541 }
542
543 applySortProgressToModel();
544
545 if (!m_pendingSortRoleItems.isEmpty()) {
546 // Trigger the asynchronous determination of the sort role.
547 killPreviewJob();
548 m_state = ResolvingSortRole;
549 resolveNextSortRole();
550 }
551 } else {
552 m_state = Idle;
553 m_pendingSortRoleItems.clear();
554 applySortProgressToModel();
555 }
556 }
557
558 void KFileItemModelRolesUpdater::slotGotPreview(const KFileItem &item, const QPixmap &pixmap)
559 {
560 if (m_state != PreviewJobRunning) {
561 return;
562 }
563
564 m_changedItems.remove(item);
565
566 const int index = m_model->index(item);
567 if (index < 0) {
568 return;
569 }
570
571 QHash<QByteArray, QVariant> data = rolesData(item, index);
572 data.insert("iconPixmap", transformPreviewPixmap(pixmap));
573 data.insert("supportsSequencing", m_previewJob->handlesSequences());
574
575 disconnect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged);
576 m_model->setData(index, data);
577 connect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged);
578 Q_EMIT previewJobFinished(); // For unit testing
579
580 m_finishedItems.insert(item);
581 }
582
583 void KFileItemModelRolesUpdater::slotPreviewFailed(const KFileItem &item)
584 {
585 if (m_state != PreviewJobRunning) {
586 return;
587 }
588
589 m_changedItems.remove(item);
590
591 const int index = m_model->index(item);
592 if (index >= 0) {
593 QHash<QByteArray, QVariant> data;
594 data.insert("iconPixmap", QPixmap());
595
596 disconnect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged);
597 m_model->setData(index, data);
598 connect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged);
599
600 applyResolvedRoles(index, ResolveAll);
601 m_finishedItems.insert(item);
602 }
603 }
604
605 void KFileItemModelRolesUpdater::slotPreviewJobFinished()
606 {
607 m_previewJob = nullptr;
608
609 if (m_state != PreviewJobRunning) {
610 return;
611 }
612
613 m_state = Idle;
614
615 if (!m_pendingPreviewItems.isEmpty()) {
616 startPreviewJob();
617 } else {
618 if (!m_changedItems.isEmpty()) {
619 updateChangedItems();
620 }
621 }
622 }
623
624 void KFileItemModelRolesUpdater::slotHoverSequenceGotPreview(const KFileItem &item, const QPixmap &pixmap)
625 {
626 const int index = m_model->index(item);
627 if (index < 0) {
628 return;
629 }
630
631 QHash<QByteArray, QVariant> data = m_model->data(index);
632 QVector<QPixmap> pixmaps = data["hoverSequencePixmaps"].value<QVector<QPixmap>>();
633 const int loadedIndex = pixmaps.size();
634
635 float wap = m_hoverSequencePreviewJob->sequenceIndexWraparoundPoint();
636 if (!m_hoverSequencePreviewJob->handlesSequences()) {
637 wap = 1.0f;
638 }
639 if (wap >= 0.0f) {
640 data["hoverSequenceWraparoundPoint"] = wap;
641 m_model->setData(index, data);
642 }
643
644 // For hover sequence previews we never load index 0, because that's just the regular preview
645 // in "iconPixmap". But that means we'll load index 1 even for thumbnailers that don't support
646 // sequences, in which case we can just throw away the preview because it's the same as for
647 // index 0. Unfortunately we can't find it out earlier :(
648 if (wap < 0.0f || loadedIndex < static_cast<int>(wap)) {
649 // Add the preview to the model data
650
651 const QPixmap scaledPixmap = transformPreviewPixmap(pixmap);
652
653 pixmaps.append(scaledPixmap);
654 data["hoverSequencePixmaps"] = QVariant::fromValue(pixmaps);
655
656 m_model->setData(index, data);
657
658 const auto loadedIt = std::find(m_hoverSequenceLoadedItems.begin(), m_hoverSequenceLoadedItems.end(), item);
659 if (loadedIt == m_hoverSequenceLoadedItems.end()) {
660 m_hoverSequenceLoadedItems.push_back(item);
661 trimHoverSequenceLoadedItems();
662 }
663 }
664
665 m_hoverSequenceNumSuccessiveFailures = 0;
666 }
667
668 void KFileItemModelRolesUpdater::slotHoverSequencePreviewFailed(const KFileItem &item)
669 {
670 const int index = m_model->index(item);
671 if (index < 0) {
672 return;
673 }
674
675 static const int numRetries = 2;
676
677 QHash<QByteArray, QVariant> data = m_model->data(index);
678 QVector<QPixmap> pixmaps = data["hoverSequencePixmaps"].value<QVector<QPixmap>>();
679
680 qCDebug(DolphinDebug).nospace() << "Failed to generate hover sequence preview #" << pixmaps.size() << " for file " << item.url().toString() << " (attempt "
681 << (m_hoverSequenceNumSuccessiveFailures + 1) << "/" << (numRetries + 1) << ")";
682
683 if (m_hoverSequenceNumSuccessiveFailures >= numRetries) {
684 // Give up and simply duplicate the previous sequence image (if any)
685
686 pixmaps.append(pixmaps.empty() ? QPixmap() : pixmaps.last());
687 data["hoverSequencePixmaps"] = QVariant::fromValue(pixmaps);
688
689 if (!data.contains("hoverSequenceWraparoundPoint")) {
690 // hoverSequenceWraparoundPoint is only available when PreviewJob succeeds, so unless
691 // it has previously succeeded, it's best to assume that it just doesn't handle
692 // sequences instead of trying to load the next image indefinitely.
693 data["hoverSequenceWraparoundPoint"] = 1.0f;
694 }
695
696 m_model->setData(index, data);
697
698 m_hoverSequenceNumSuccessiveFailures = 0;
699 } else {
700 // Retry
701
702 m_hoverSequenceNumSuccessiveFailures++;
703 }
704
705 // Next image in the sequence (or same one if the retry limit wasn't reached yet) will be
706 // loaded automatically, because slotHoverSequencePreviewJobFinished() will be triggered
707 // even when PreviewJob fails.
708 }
709
710 void KFileItemModelRolesUpdater::slotHoverSequencePreviewJobFinished()
711 {
712 const int index = m_model->index(m_hoverSequenceItem);
713 if (index < 0) {
714 m_hoverSequencePreviewJob = nullptr;
715 return;
716 }
717
718 // Since a PreviewJob can only have one associated sequence index, we can only generate
719 // one sequence image per job, so we have to start another one for the next index.
720
721 // Load the next image in the sequence
722 m_hoverSequencePreviewJob = nullptr;
723 loadNextHoverSequencePreview();
724 }
725
726 void KFileItemModelRolesUpdater::resolveNextSortRole()
727 {
728 if (m_state != ResolvingSortRole) {
729 return;
730 }
731
732 QSet<KFileItem>::iterator it = m_pendingSortRoleItems.begin();
733 while (it != m_pendingSortRoleItems.end()) {
734 const KFileItem item = *it;
735 const int index = m_model->index(item);
736
737 // Continue if the sort role has already been determined for the
738 // item, and the item has not been changed recently.
739 if (!m_changedItems.contains(item) && m_model->data(index).contains(m_model->sortRole())) {
740 it = m_pendingSortRoleItems.erase(it);
741 continue;
742 }
743
744 applySortRole(index);
745 m_pendingSortRoleItems.erase(it);
746 break;
747 }
748
749 if (!m_pendingSortRoleItems.isEmpty()) {
750 applySortProgressToModel();
751 QTimer::singleShot(0, this, &KFileItemModelRolesUpdater::resolveNextSortRole);
752 } else {
753 m_state = Idle;
754
755 // Prevent that we try to update the items twice.
756 disconnect(m_model, &KFileItemModel::itemsMoved, this, &KFileItemModelRolesUpdater::slotItemsMoved);
757 applySortProgressToModel();
758 connect(m_model, &KFileItemModel::itemsMoved, this, &KFileItemModelRolesUpdater::slotItemsMoved);
759 startUpdating();
760 }
761 }
762
763 void KFileItemModelRolesUpdater::resolveNextPendingRoles()
764 {
765 if (m_state != ResolvingAllRoles) {
766 return;
767 }
768
769 while (!m_pendingIndexes.isEmpty()) {
770 const int index = m_pendingIndexes.takeFirst();
771 const KFileItem item = m_model->fileItem(index);
772
773 if (m_finishedItems.contains(item)) {
774 continue;
775 }
776
777 applyResolvedRoles(index, ResolveAll);
778 m_finishedItems.insert(item);
779 m_changedItems.remove(item);
780 break;
781 }
782
783 if (!m_pendingIndexes.isEmpty()) {
784 QTimer::singleShot(0, this, &KFileItemModelRolesUpdater::resolveNextPendingRoles);
785 } else {
786 m_state = Idle;
787
788 if (m_clearPreviews) {
789 // Only go through the list if there are items which might still have previews.
790 if (m_finishedItems.count() != m_model->count()) {
791 QHash<QByteArray, QVariant> data;
792 data.insert("iconPixmap", QPixmap());
793 data.insert("hoverSequencePixmaps", QVariant::fromValue(QVector<QPixmap>()));
794
795 disconnect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged);
796 for (int index = 0; index <= m_model->count(); ++index) {
797 if (m_model->data(index).contains("iconPixmap") || m_model->data(index).contains("hoverSequencePixmaps")) {
798 m_model->setData(index, data);
799 }
800 }
801 connect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged);
802 }
803 m_clearPreviews = false;
804 }
805
806 if (!m_changedItems.isEmpty()) {
807 updateChangedItems();
808 }
809 }
810 }
811
812 void KFileItemModelRolesUpdater::resolveRecentlyChangedItems()
813 {
814 m_changedItems += m_recentlyChangedItems;
815 m_recentlyChangedItems.clear();
816 updateChangedItems();
817 }
818
819 void KFileItemModelRolesUpdater::applyChangedBalooRoles(const QString &file)
820 {
821 #if HAVE_BALOO
822 const KFileItem item = m_model->fileItem(QUrl::fromLocalFile(file));
823
824 if (item.isNull()) {
825 // itemUrl is not in the model anymore, probably because
826 // the corresponding file has been deleted in the meantime.
827 return;
828 }
829 applyChangedBalooRolesForItem(item);
830 #else
831 Q_UNUSED(file)
832 #endif
833 }
834
835 void KFileItemModelRolesUpdater::applyChangedBalooRolesForItem(const KFileItem &item)
836 {
837 #if HAVE_BALOO
838 Baloo::File file(item.localPath());
839 file.load();
840
841 const KBalooRolesProvider &rolesProvider = KBalooRolesProvider::instance();
842 QHash<QByteArray, QVariant> data;
843
844 const auto roles = rolesProvider.roles();
845 for (const QByteArray &role : roles) {
846 // Overwrite all the role values with an empty QVariant, because the roles
847 // provider doesn't overwrite it when the property value list is empty.
848 // See bug 322348
849 data.insert(role, QVariant());
850 }
851
852 QHashIterator<QByteArray, QVariant> it(rolesProvider.roleValues(file, m_roles));
853 while (it.hasNext()) {
854 it.next();
855 data.insert(it.key(), it.value());
856 }
857
858 disconnect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged);
859 const int index = m_model->index(item);
860 m_model->setData(index, data);
861 connect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged);
862 #else
863 #ifndef Q_CC_MSVC
864 Q_UNUSED(item)
865 #endif
866 #endif
867 }
868
869 void KFileItemModelRolesUpdater::slotDirectoryContentsCountReceived(const QString &path, int count, long long size)
870 {
871 const bool getIsExpandableRole = m_roles.contains("isExpandable");
872 const bool getSizeRole = m_roles.contains("size");
873
874 if (getSizeRole || getIsExpandableRole) {
875 const int index = m_model->index(QUrl::fromLocalFile(path));
876 if (index >= 0) {
877 QHash<QByteArray, QVariant> data;
878
879 if (getSizeRole) {
880 data.insert("count", count);
881 data.insert("size", QVariant::fromValue(size));
882 }
883 if (getIsExpandableRole) {
884 data.insert("isExpandable", count > 0);
885 }
886
887 disconnect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged);
888 m_model->setData(index, data);
889 connect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged);
890 }
891 }
892 }
893
894 void KFileItemModelRolesUpdater::startUpdating()
895 {
896 if (m_state == Paused) {
897 return;
898 }
899
900 if (m_finishedItems.count() == m_model->count()) {
901 // All roles have been resolved already.
902 m_state = Idle;
903 return;
904 }
905
906 // Terminate all updates that are currently active.
907 killPreviewJob();
908 m_pendingIndexes.clear();
909
910 QElapsedTimer timer;
911 timer.start();
912
913 // Determine the icons for the visible items synchronously.
914 updateVisibleIcons();
915
916 // A detailed update of the items in and near the visible area
917 // only makes sense if sorting is finished.
918 if (m_state == ResolvingSortRole) {
919 return;
920 }
921
922 // Start the preview job or the asynchronous resolving of all roles.
923 QList<int> indexes = indexesToResolve();
924
925 if (m_previewShown) {
926 m_pendingPreviewItems.clear();
927 m_pendingPreviewItems.reserve(indexes.count());
928
929 for (int index : std::as_const(indexes)) {
930 const KFileItem item = m_model->fileItem(index);
931 if (!m_finishedItems.contains(item)) {
932 m_pendingPreviewItems.append(item);
933 }
934 }
935
936 startPreviewJob();
937 } else {
938 m_pendingIndexes = indexes;
939 // Trigger the asynchronous resolving of all roles.
940 m_state = ResolvingAllRoles;
941 QTimer::singleShot(0, this, &KFileItemModelRolesUpdater::resolveNextPendingRoles);
942 }
943 }
944
945 void KFileItemModelRolesUpdater::updateVisibleIcons()
946 {
947 int lastVisibleIndex = m_lastVisibleIndex;
948 if (lastVisibleIndex <= 0) {
949 // Guess a reasonable value for the last visible index if the view
950 // has not told us about the real value yet.
951 lastVisibleIndex = qMin(m_firstVisibleIndex + m_maximumVisibleItems, m_model->count() - 1);
952 if (lastVisibleIndex <= 0) {
953 lastVisibleIndex = qMin(200, m_model->count() - 1);
954 }
955 }
956
957 QElapsedTimer timer;
958 timer.start();
959
960 // Try to determine the final icons for all visible items.
961 int index;
962 for (index = m_firstVisibleIndex; index <= lastVisibleIndex && timer.elapsed() < MaxBlockTimeout; ++index) {
963 applyResolvedRoles(index, ResolveFast);
964 }
965
966 // KFileItemListView::initializeItemListWidget(KItemListWidget*) will load
967 // preliminary icons (i.e., without mime type determination) for the
968 // remaining items.
969 }
970
971 void KFileItemModelRolesUpdater::startPreviewJob()
972 {
973 m_state = PreviewJobRunning;
974
975 if (m_pendingPreviewItems.isEmpty()) {
976 QTimer::singleShot(0, this, &KFileItemModelRolesUpdater::slotPreviewJobFinished);
977 return;
978 }
979
980 // KIO::filePreview() will request the MIME-type of all passed items, which (in the
981 // worst case) might block the application for several seconds. To prevent such
982 // a blocking, we only pass items with known mime type to the preview job.
983 const int count = m_pendingPreviewItems.count();
984 KFileItemList itemSubSet;
985 itemSubSet.reserve(count);
986
987 if (m_pendingPreviewItems.first().isMimeTypeKnown()) {
988 // Some mime types are known already, probably because they were
989 // determined when loading the icons for the visible items. Start
990 // a preview job for all items at the beginning of the list which
991 // have a known mime type.
992 do {
993 itemSubSet.append(m_pendingPreviewItems.takeFirst());
994 } while (!m_pendingPreviewItems.isEmpty() && m_pendingPreviewItems.first().isMimeTypeKnown());
995 } else {
996 // Determine mime types for MaxBlockTimeout ms, and start a preview
997 // job for the corresponding items.
998 QElapsedTimer timer;
999 timer.start();
1000
1001 do {
1002 const KFileItem item = m_pendingPreviewItems.takeFirst();
1003 item.determineMimeType();
1004 itemSubSet.append(item);
1005 } while (!m_pendingPreviewItems.isEmpty() && timer.elapsed() < MaxBlockTimeout);
1006 }
1007
1008 KIO::PreviewJob *job = new KIO::PreviewJob(itemSubSet, cacheSize(), &m_enabledPlugins);
1009 job->setDevicePixelRatio(m_devicePixelRatio);
1010 job->setIgnoreMaximumSize(itemSubSet.first().isLocalFile() && !itemSubSet.first().isSlow() && m_localFileSizePreviewLimit <= 0);
1011 if (job->uiDelegate()) {
1012 KJobWidgets::setWindow(job, qApp->activeWindow());
1013 }
1014
1015 connect(job, &KIO::PreviewJob::gotPreview, this, &KFileItemModelRolesUpdater::slotGotPreview);
1016 connect(job, &KIO::PreviewJob::failed, this, &KFileItemModelRolesUpdater::slotPreviewFailed);
1017 connect(job, &KIO::PreviewJob::finished, this, &KFileItemModelRolesUpdater::slotPreviewJobFinished);
1018
1019 m_previewJob = job;
1020 }
1021
1022 QPixmap KFileItemModelRolesUpdater::transformPreviewPixmap(const QPixmap &pixmap)
1023 {
1024 QPixmap scaledPixmap = pixmap;
1025
1026 if (pixmap.isNull()) {
1027 return scaledPixmap;
1028 }
1029
1030 if (!pixmap.hasAlpha() && m_iconSize.width() > KIconLoader::SizeSmallMedium && m_iconSize.height() > KIconLoader::SizeSmallMedium) {
1031 if (m_enlargeSmallPreviews) {
1032 KPixmapModifier::applyFrame(scaledPixmap, m_iconSize);
1033 } else {
1034 // Assure that small previews don't get enlarged. Instead they
1035 // should be shown centered within the frame.
1036 const QSize contentSize = KPixmapModifier::sizeInsideFrame(m_iconSize);
1037 const bool enlargingRequired = scaledPixmap.width() < contentSize.width() && scaledPixmap.height() < contentSize.height();
1038 if (enlargingRequired) {
1039 QSize frameSize = scaledPixmap.size() / scaledPixmap.devicePixelRatio();
1040 frameSize.scale(m_iconSize, Qt::KeepAspectRatio);
1041
1042 QPixmap largeFrame(frameSize);
1043 largeFrame.fill(Qt::transparent);
1044
1045 KPixmapModifier::applyFrame(largeFrame, frameSize);
1046
1047 QPainter painter(&largeFrame);
1048 painter.drawPixmap((largeFrame.width() - scaledPixmap.width() / scaledPixmap.devicePixelRatio()) / 2,
1049 (largeFrame.height() - scaledPixmap.height() / scaledPixmap.devicePixelRatio()) / 2,
1050 scaledPixmap);
1051 scaledPixmap = largeFrame;
1052 } else {
1053 // The image must be shrunk as it is too large to fit into
1054 // the available icon size
1055 KPixmapModifier::applyFrame(scaledPixmap, m_iconSize);
1056 }
1057 }
1058 } else {
1059 KPixmapModifier::scale(scaledPixmap, m_iconSize * m_devicePixelRatio);
1060 scaledPixmap.setDevicePixelRatio(m_devicePixelRatio);
1061 }
1062
1063 return scaledPixmap;
1064 }
1065
1066 QSize KFileItemModelRolesUpdater::cacheSize()
1067 {
1068 // PreviewJob internally caches items always with the size of
1069 // 128 x 128 pixels or 256 x 256 pixels. A (slow) downscaling is done
1070 // by PreviewJob if a smaller size is requested. For images KFileItemModelRolesUpdater must
1071 // do a downscaling anyhow because of the frame, so in this case only the provided
1072 // cache sizes are requested.
1073 return (m_iconSize.width() > 128) || (m_iconSize.height() > 128) ? QSize(256, 256) : QSize(128, 128);
1074 }
1075
1076 void KFileItemModelRolesUpdater::loadNextHoverSequencePreview()
1077 {
1078 if (m_hoverSequenceItem.isNull() || m_hoverSequencePreviewJob) {
1079 return;
1080 }
1081
1082 const int index = m_model->index(m_hoverSequenceItem);
1083 if (index < 0) {
1084 return;
1085 }
1086
1087 // We generate the next few sequence indices in advance (buffering)
1088 const int maxSeqIdx = m_hoverSequenceIndex + 5;
1089
1090 QHash<QByteArray, QVariant> data = m_model->data(index);
1091
1092 if (!data.contains("hoverSequencePixmaps")) {
1093 // The pixmap at index 0 isn't used ("iconPixmap" will be used instead)
1094 data.insert("hoverSequencePixmaps", QVariant::fromValue(QVector<QPixmap>() << QPixmap()));
1095 m_model->setData(index, data);
1096 }
1097
1098 const QVector<QPixmap> pixmaps = data["hoverSequencePixmaps"].value<QVector<QPixmap>>();
1099
1100 const int loadSeqIdx = pixmaps.size();
1101
1102 float wap = -1.0f;
1103 if (data.contains("hoverSequenceWraparoundPoint")) {
1104 wap = data["hoverSequenceWraparoundPoint"].toFloat();
1105 }
1106 if (wap >= 1.0f && loadSeqIdx >= static_cast<int>(wap)) {
1107 // Reached the wraparound point -> no more previews to load.
1108 return;
1109 }
1110
1111 if (loadSeqIdx > maxSeqIdx) {
1112 // Wait until setHoverSequenceState() is called with a higher sequence index.
1113 return;
1114 }
1115
1116 KIO::PreviewJob *job = new KIO::PreviewJob({m_hoverSequenceItem}, cacheSize(), &m_enabledPlugins);
1117
1118 job->setSequenceIndex(loadSeqIdx);
1119 job->setIgnoreMaximumSize(m_hoverSequenceItem.isLocalFile() && !m_hoverSequenceItem.isSlow() && m_localFileSizePreviewLimit <= 0);
1120 if (job->uiDelegate()) {
1121 KJobWidgets::setWindow(job, qApp->activeWindow());
1122 }
1123
1124 connect(job, &KIO::PreviewJob::gotPreview, this, &KFileItemModelRolesUpdater::slotHoverSequenceGotPreview);
1125 connect(job, &KIO::PreviewJob::failed, this, &KFileItemModelRolesUpdater::slotHoverSequencePreviewFailed);
1126 connect(job, &KIO::PreviewJob::finished, this, &KFileItemModelRolesUpdater::slotHoverSequencePreviewJobFinished);
1127
1128 m_hoverSequencePreviewJob = job;
1129 }
1130
1131 void KFileItemModelRolesUpdater::killHoverSequencePreviewJob()
1132 {
1133 if (m_hoverSequencePreviewJob) {
1134 disconnect(m_hoverSequencePreviewJob, &KIO::PreviewJob::gotPreview, this, &KFileItemModelRolesUpdater::slotHoverSequenceGotPreview);
1135 disconnect(m_hoverSequencePreviewJob, &KIO::PreviewJob::failed, this, &KFileItemModelRolesUpdater::slotHoverSequencePreviewFailed);
1136 disconnect(m_hoverSequencePreviewJob, &KIO::PreviewJob::finished, this, &KFileItemModelRolesUpdater::slotHoverSequencePreviewJobFinished);
1137 m_hoverSequencePreviewJob->kill();
1138 m_hoverSequencePreviewJob = nullptr;
1139 }
1140 }
1141
1142 void KFileItemModelRolesUpdater::updateChangedItems()
1143 {
1144 if (m_state == Paused) {
1145 return;
1146 }
1147
1148 if (m_changedItems.isEmpty()) {
1149 return;
1150 }
1151
1152 m_finishedItems -= m_changedItems;
1153
1154 if (m_resolvableRoles.contains(m_model->sortRole())) {
1155 m_pendingSortRoleItems += m_changedItems;
1156
1157 if (m_state != ResolvingSortRole) {
1158 // Stop the preview job if necessary, and trigger the
1159 // asynchronous determination of the sort role.
1160 killPreviewJob();
1161 m_state = ResolvingSortRole;
1162 QTimer::singleShot(0, this, &KFileItemModelRolesUpdater::resolveNextSortRole);
1163 }
1164
1165 return;
1166 }
1167
1168 QList<int> visibleChangedIndexes;
1169 QList<int> invisibleChangedIndexes;
1170 visibleChangedIndexes.reserve(m_changedItems.size());
1171 invisibleChangedIndexes.reserve(m_changedItems.size());
1172
1173 auto changedItemsIt = m_changedItems.begin();
1174 while (changedItemsIt != m_changedItems.end()) {
1175 const auto &item = *changedItemsIt;
1176 const int index = m_model->index(item);
1177
1178 if (index < 0) {
1179 changedItemsIt = m_changedItems.erase(changedItemsIt);
1180 continue;
1181 }
1182 ++changedItemsIt;
1183
1184 if (index >= m_firstVisibleIndex && index <= m_lastVisibleIndex) {
1185 visibleChangedIndexes.append(index);
1186 } else {
1187 invisibleChangedIndexes.append(index);
1188 }
1189 }
1190
1191 std::sort(visibleChangedIndexes.begin(), visibleChangedIndexes.end());
1192
1193 if (m_previewShown) {
1194 for (int index : std::as_const(visibleChangedIndexes)) {
1195 m_pendingPreviewItems.append(m_model->fileItem(index));
1196 }
1197
1198 for (int index : std::as_const(invisibleChangedIndexes)) {
1199 m_pendingPreviewItems.append(m_model->fileItem(index));
1200 }
1201
1202 if (!m_previewJob) {
1203 startPreviewJob();
1204 }
1205 } else {
1206 const bool resolvingInProgress = !m_pendingIndexes.isEmpty();
1207 m_pendingIndexes = visibleChangedIndexes + m_pendingIndexes + invisibleChangedIndexes;
1208 if (!resolvingInProgress) {
1209 // Trigger the asynchronous resolving of the changed roles.
1210 m_state = ResolvingAllRoles;
1211 QTimer::singleShot(0, this, &KFileItemModelRolesUpdater::resolveNextPendingRoles);
1212 }
1213 }
1214 }
1215
1216 void KFileItemModelRolesUpdater::applySortRole(int index)
1217 {
1218 QHash<QByteArray, QVariant> data;
1219 const KFileItem item = m_model->fileItem(index);
1220
1221 if (m_model->sortRole() == "type") {
1222 if (!item.isMimeTypeKnown()) {
1223 item.determineMimeType();
1224 }
1225
1226 data.insert("type", item.mimeComment());
1227 } else if (m_model->sortRole() == "size" && item.isLocalFile() && item.isDir()) {
1228 startDirectorySizeCounting(item, index);
1229 return;
1230 } else {
1231 // Probably the sort role is a baloo role - just determine all roles.
1232 data = rolesData(item, index);
1233 }
1234
1235 disconnect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged);
1236 m_model->setData(index, data);
1237 connect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged);
1238 }
1239
1240 void KFileItemModelRolesUpdater::applySortProgressToModel()
1241 {
1242 // Inform the model about the progress of the resolved items,
1243 // so that it can give an indication when the sorting has been finished.
1244 const int resolvedCount = m_model->count() - m_pendingSortRoleItems.count();
1245 m_model->emitSortProgress(resolvedCount);
1246 }
1247
1248 bool KFileItemModelRolesUpdater::applyResolvedRoles(int index, ResolveHint hint)
1249 {
1250 const KFileItem item = m_model->fileItem(index);
1251 const bool resolveAll = (hint == ResolveAll);
1252
1253 bool iconChanged = false;
1254 if (!item.isMimeTypeKnown() || !item.isFinalIconKnown()) {
1255 item.determineMimeType();
1256 iconChanged = true;
1257 } else if (!m_model->data(index).contains("iconName")) {
1258 iconChanged = true;
1259 }
1260
1261 if (iconChanged || resolveAll || m_clearPreviews) {
1262 if (index < 0) {
1263 return false;
1264 }
1265
1266 QHash<QByteArray, QVariant> data;
1267 if (resolveAll) {
1268 data = rolesData(item, index);
1269 }
1270
1271 if (!item.iconName().isEmpty()) {
1272 data.insert("iconName", item.iconName());
1273 }
1274
1275 if (m_clearPreviews) {
1276 data.insert("iconPixmap", QPixmap());
1277 data.insert("hoverSequencePixmaps", QVariant::fromValue(QVector<QPixmap>()));
1278 }
1279
1280 disconnect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged);
1281 m_model->setData(index, data);
1282 connect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged);
1283 return true;
1284 }
1285
1286 return false;
1287 }
1288
1289 void KFileItemModelRolesUpdater::startDirectorySizeCounting(const KFileItem &item, int index)
1290 {
1291 if (ContentDisplaySettings::directorySizeMode() == ContentDisplaySettings::EnumDirectorySizeMode::None) {
1292 return;
1293 }
1294
1295 // Set any remote files to unknown size (-1).
1296 if (!item.isLocalFile()) {
1297 resetSizeData(index, -1);
1298 return;
1299 } else {
1300 resetSizeData(index);
1301 }
1302
1303 if (ContentDisplaySettings::directorySizeMode() == ContentDisplaySettings::EnumDirectorySizeMode::ContentCount || item.isSlow()) {
1304 // fastpath no recursion necessary
1305 auto data = m_model->data(index);
1306 if (data.value("size") == -2) {
1307 // means job already started
1308 return;
1309 }
1310
1311 auto url = item.url();
1312 if (!item.localPath().isEmpty()) {
1313 // optimization for desktop:/, trash:/
1314 url = QUrl::fromLocalFile(item.localPath());
1315 }
1316
1317 data.insert("size", -2); // invalid size, -1 means size unknown
1318
1319 disconnect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged);
1320 m_model->setData(index, data);
1321 connect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged);
1322
1323 auto listJob = KIO::listDir(url, KIO::HideProgressInfo);
1324
1325 QObject::connect(listJob, &KIO::ListJob::entries, this, [this, item](const KJob *job, const KIO::UDSEntryList &list) {
1326 int index = m_model->index(item);
1327 if (index < 0) {
1328 return;
1329 }
1330 auto data = m_model->data(index);
1331 int origCount = data.value("count").toInt();
1332 // Get the amount of processed items...
1333 int entryCount = job->processedAmount(KJob::Bytes);
1334
1335 // ...and then remove the unwanted items from the amount.
1336 for (const KIO::UDSEntry &entry : list) {
1337 const auto name = entry.stringValue(KIO::UDSEntry::UDS_NAME);
1338
1339 if (name == QStringLiteral("..") || name == QStringLiteral(".")) {
1340 --entryCount;
1341 continue;
1342 }
1343 if (!m_model->showHiddenFiles() && name.startsWith(QLatin1Char('.'))) {
1344 --entryCount;
1345 continue;
1346 }
1347 if (m_model->showDirectoriesOnly() && !entry.isDir()) {
1348 --entryCount;
1349 continue;
1350 }
1351 }
1352
1353 QHash<QByteArray, QVariant> newData;
1354 QVariant expandable = data.value("isExpandable");
1355 if (expandable.isNull() || expandable.toBool() != (entryCount > 0)) {
1356 // if expandable has changed
1357 newData.insert("isExpandable", entryCount > 0);
1358 }
1359
1360 if (origCount != entryCount) {
1361 newData.insert("count", entryCount);
1362 }
1363
1364 if (!newData.isEmpty()) {
1365 disconnect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged);
1366 m_model->setData(index, newData);
1367 connect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged);
1368 }
1369 });
1370 return;
1371 }
1372
1373 // Tell m_directoryContentsCounter that we want to count the items
1374 // inside the directory. The result will be received in slotDirectoryContentsCountReceived.
1375 const QString path = item.localPath();
1376 const auto priority = index >= m_firstVisibleIndex && index <= m_lastVisibleIndex ? KDirectoryContentsCounter::PathCountPriority::High
1377 : KDirectoryContentsCounter::PathCountPriority::Normal;
1378
1379 m_directoryContentsCounter->scanDirectory(path, priority);
1380 }
1381
1382 QHash<QByteArray, QVariant> KFileItemModelRolesUpdater::rolesData(const KFileItem &item, int index)
1383 {
1384 QHash<QByteArray, QVariant> data;
1385
1386 const bool getSizeRole = m_roles.contains("size");
1387 const bool getIsExpandableRole = m_roles.contains("isExpandable");
1388
1389 if ((getSizeRole || getIsExpandableRole) && item.isDir()) {
1390 startDirectorySizeCounting(item, index);
1391 }
1392
1393 if (m_roles.contains("extension")) {
1394 // TODO KF6 use KFileItem::suffix 464722
1395 data.insert("extension", QFileInfo(item.name()).suffix());
1396 }
1397
1398 if (m_roles.contains("type")) {
1399 data.insert("type", item.mimeComment());
1400 }
1401
1402 QStringList overlays = item.overlays();
1403 for (KOverlayIconPlugin *it : std::as_const(m_overlayIconsPlugin)) {
1404 overlays.append(it->getOverlays(item.url()));
1405 }
1406 if (!overlays.isEmpty()) {
1407 data.insert("iconOverlays", overlays);
1408 }
1409
1410 #if HAVE_BALOO
1411 if (m_balooFileMonitor) {
1412 m_balooFileMonitor->addFile(item.localPath());
1413 applyChangedBalooRolesForItem(item);
1414 }
1415 #endif
1416 return data;
1417 }
1418
1419 void KFileItemModelRolesUpdater::slotOverlaysChanged(const QUrl &url, const QStringList &)
1420 {
1421 const KFileItem item = m_model->fileItem(url);
1422 if (item.isNull()) {
1423 return;
1424 }
1425 const int index = m_model->index(item);
1426 QHash<QByteArray, QVariant> data = m_model->data(index);
1427 QStringList overlays = item.overlays();
1428 for (KOverlayIconPlugin *it : std::as_const(m_overlayIconsPlugin)) {
1429 overlays.append(it->getOverlays(url));
1430 }
1431 data.insert("iconOverlays", overlays);
1432 m_model->setData(index, data);
1433 }
1434
1435 void KFileItemModelRolesUpdater::updateAllPreviews()
1436 {
1437 if (m_state == Paused) {
1438 m_previewChangedDuringPausing = true;
1439 } else {
1440 m_finishedItems.clear();
1441 startUpdating();
1442 }
1443 }
1444
1445 void KFileItemModelRolesUpdater::killPreviewJob()
1446 {
1447 if (m_previewJob) {
1448 disconnect(m_previewJob, &KIO::PreviewJob::gotPreview, this, &KFileItemModelRolesUpdater::slotGotPreview);
1449 disconnect(m_previewJob, &KIO::PreviewJob::failed, this, &KFileItemModelRolesUpdater::slotPreviewFailed);
1450 disconnect(m_previewJob, &KIO::PreviewJob::finished, this, &KFileItemModelRolesUpdater::slotPreviewJobFinished);
1451 m_previewJob->kill();
1452 m_previewJob = nullptr;
1453 m_pendingPreviewItems.clear();
1454 }
1455 }
1456
1457 QList<int> KFileItemModelRolesUpdater::indexesToResolve() const
1458 {
1459 const int count = m_model->count();
1460
1461 QList<int> result;
1462 result.reserve(qMin(count, (m_lastVisibleIndex - m_firstVisibleIndex + 1) + ResolveAllItemsLimit + (2 * m_maximumVisibleItems)));
1463
1464 // Add visible items.
1465 // Resolve files first, their previews are quicker.
1466 QList<int> visibleDirs;
1467 for (int i = m_firstVisibleIndex; i <= m_lastVisibleIndex; ++i) {
1468 const KFileItem item = m_model->fileItem(i);
1469 if (item.isDir()) {
1470 visibleDirs.append(i);
1471 } else {
1472 result.append(i);
1473 }
1474 }
1475
1476 result.append(visibleDirs);
1477
1478 // We need a reasonable upper limit for number of items to resolve after
1479 // and before the visible range. m_maximumVisibleItems can be quite large
1480 // when using Compact View.
1481 const int readAheadItems = qMin(ReadAheadPages * m_maximumVisibleItems, ResolveAllItemsLimit / 2);
1482
1483 // Add items after the visible range.
1484 const int endExtendedVisibleRange = qMin(m_lastVisibleIndex + readAheadItems, count - 1);
1485 for (int i = m_lastVisibleIndex + 1; i <= endExtendedVisibleRange; ++i) {
1486 result.append(i);
1487 }
1488
1489 // Add items before the visible range in reverse order.
1490 const int beginExtendedVisibleRange = qMax(0, m_firstVisibleIndex - readAheadItems);
1491 for (int i = m_firstVisibleIndex - 1; i >= beginExtendedVisibleRange; --i) {
1492 result.append(i);
1493 }
1494
1495 // Add items on the last page.
1496 const int beginLastPage = qMax(endExtendedVisibleRange + 1, count - m_maximumVisibleItems);
1497 for (int i = beginLastPage; i < count; ++i) {
1498 result.append(i);
1499 }
1500
1501 // Add items on the first page.
1502 const int endFirstPage = qMin(beginExtendedVisibleRange, m_maximumVisibleItems);
1503 for (int i = 0; i < endFirstPage; ++i) {
1504 result.append(i);
1505 }
1506
1507 // Continue adding items until ResolveAllItemsLimit is reached.
1508 int remainingItems = ResolveAllItemsLimit - result.count();
1509
1510 for (int i = endExtendedVisibleRange + 1; i < beginLastPage && remainingItems > 0; ++i) {
1511 result.append(i);
1512 --remainingItems;
1513 }
1514
1515 for (int i = beginExtendedVisibleRange - 1; i >= endFirstPage && remainingItems > 0; --i) {
1516 result.append(i);
1517 --remainingItems;
1518 }
1519
1520 return result;
1521 }
1522
1523 void KFileItemModelRolesUpdater::trimHoverSequenceLoadedItems()
1524 {
1525 static const size_t maxLoadedItems = 20;
1526
1527 size_t loadedItems = m_hoverSequenceLoadedItems.size();
1528 while (loadedItems > maxLoadedItems) {
1529 const KFileItem item = m_hoverSequenceLoadedItems.front();
1530
1531 m_hoverSequenceLoadedItems.pop_front();
1532 loadedItems--;
1533
1534 const int index = m_model->index(item);
1535 if (index >= 0) {
1536 QHash<QByteArray, QVariant> data = m_model->data(index);
1537 data["hoverSequencePixmaps"] = QVariant::fromValue(QVector<QPixmap>() << QPixmap());
1538 m_model->setData(index, data);
1539 }
1540 }
1541 }
1542
1543 void KFileItemModelRolesUpdater::resetSizeData(const int index, const int size)
1544 {
1545 disconnect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged);
1546 auto data = m_model->data(index);
1547 data.insert("size", size);
1548 m_model->setData(index, data);
1549 connect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged);
1550 }
1551
1552 void KFileItemModelRolesUpdater::recountDirectoryItems(const QList<QUrl> directories)
1553 {
1554 for (const auto &dir : directories) {
1555 auto index = m_model->index(dir);
1556 if (index < 0) {
1557 continue;
1558 }
1559 auto item = m_model->fileItem(index);
1560 if (item.isDir()) {
1561 startDirectorySizeCounting(item, index);
1562 }
1563 }
1564 }
1565
1566 #include "moc_kfileitemmodelrolesupdater.cpp"