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