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