kfileitemmodelrolesupdater.cpp 38.8 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/***************************************************************************
 *   Copyright (C) 2011 by Peter Penz <peter.penz19@gmail.com>             *
 *                                                                         *
 *   This program is free software; you can redistribute it and/or modify  *
 *   it under the terms of the GNU General Public License as published by  *
 *   the Free Software Foundation; either version 2 of the License, or     *
 *   (at your option) any later version.                                   *
 *                                                                         *
 *   This program is distributed in the hope that it will be useful,       *
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
 *   GNU General Public License for more details.                          *
 *                                                                         *
 *   You should have received a copy of the GNU General Public License     *
 *   along with this program; if not, write to the                         *
 *   Free Software Foundation, Inc.,                                       *
 *   51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA            *
 ***************************************************************************/

#include "kfileitemmodelrolesupdater.h"

#include "kfileitemmodel.h"

#include <KConfig>
#include <KConfigGroup>
#include <KDebug>
27
#include <KDirWatch>
28
29
#include <KFileItem>
#include <KGlobal>
30
#include <KIO/JobUiDelegate>
31
#include <KIO/PreviewJob>
32
33
34

#include "private/kpixmapmodifier.h"

35
#include <QApplication>
36
37
38
39
40
#include <QPainter>
#include <QPixmap>
#include <QElapsedTimer>
#include <QTimer>

41
#ifdef HAVE_NEPOMUK
42
    #include "private/knepomukrolesprovider.h"
43
    #include <Nepomuk2/ResourceWatcher>
44
45
#endif

46
// Required includes for subItemsCount():
47
48
49
50
51
52
53
#ifdef Q_WS_WIN
    #include <QDir>
#else
    #include <dirent.h>
    #include <QFile>
#endif

54
// #define KFILEITEMMODELROLESUPDATER_DEBUG
55
56

namespace {
57
58
59
60
61
62
63
64
    // Maximum time in ms that the KFileItemModelRolesUpdater
    // may perform a blocking operation
    const int MaxBlockTimeout = 200;

    // Maximum number of items that will get resolved synchronously.
    // The value should roughly represent the number of maximum visible
    // items, as it does not make sense to resolve more items synchronously
    // and probably reach the MaxBlockTimeout because of invisible items.
65
66
67
68
69
70
71
72
73
74
    const int MaxResolveItemsCount = 100;
}

KFileItemModelRolesUpdater::KFileItemModelRolesUpdater(KFileItemModel* model, QObject* parent) :
    QObject(parent),
    m_paused(false),
    m_previewChangedDuringPausing(false),
    m_iconSizeChangedDuringPausing(false),
    m_rolesChangedDuringPausing(false),
    m_previewShown(false),
75
    m_enlargeSmallPreviews(true),
76
    m_clearPreviews(false),
77
    m_sortingProgress(-1),
78
79
80
81
82
83
84
85
86
    m_model(model),
    m_iconSize(),
    m_firstVisibleIndex(0),
    m_lastVisibleIndex(-1),
    m_roles(),
    m_enabledPlugins(),
    m_pendingVisibleItems(),
    m_pendingInvisibleItems(),
    m_previewJobs(),
87
    m_changedItemsTimer(0),
88
89
90
    m_changedItems(),
    m_dirWatcher(0),
    m_watchedDirs()
91
  #ifdef HAVE_NEPOMUK
92
93
  , m_nepomukResourceWatcher(0),
    m_nepomukUriItems()
94
  #endif
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
{
    Q_ASSERT(model);

    const KConfigGroup globalConfig(KGlobal::config(), "PreviewSettings");
    m_enabledPlugins = globalConfig.readEntry("Plugins", QStringList()
                                                         << "directorythumbnail"
                                                         << "imagethumbnail"
                                                         << "jpegthumbnail");

    connect(m_model, SIGNAL(itemsInserted(KItemRangeList)),
            this,    SLOT(slotItemsInserted(KItemRangeList)));
    connect(m_model, SIGNAL(itemsRemoved(KItemRangeList)),
            this,    SLOT(slotItemsRemoved(KItemRangeList)));
    connect(m_model, SIGNAL(itemsChanged(KItemRangeList,QSet<QByteArray>)),
            this,    SLOT(slotItemsChanged(KItemRangeList,QSet<QByteArray>)));
110
111
    connect(m_model, SIGNAL(sortRoleChanged(QByteArray,QByteArray)),
            this,    SLOT(slotSortRoleChanged(QByteArray,QByteArray)));
112

113
    // Use a timer to prevent that each call of slotItemsChanged() results in a synchronous
114
    // resolving of the roles. Postpone the resolving until no update has been done for 1 second.
115
    m_changedItemsTimer = new QTimer(this);
116
    m_changedItemsTimer->setInterval(1000);
117
118
    m_changedItemsTimer->setSingleShot(true);
    connect(m_changedItemsTimer, SIGNAL(timeout()), this, SLOT(resolveChangedItems()));
119
120
121
122
123
124
125

    m_resolvableRoles.insert("size");
    m_resolvableRoles.insert("type");
    m_resolvableRoles.insert("isExpandable");
#ifdef HAVE_NEPOMUK
    m_resolvableRoles += KNepomukRolesProvider::instance().roles();
#endif
126
127
128
129
130

    // When folders are expandable or the item-count is shown for folders, it is necessary
    // to watch the number of items of the sub-folder to be able to react on changes.
    m_dirWatcher = new KDirWatch(this);
    connect(m_dirWatcher, SIGNAL(dirty(QString)), this, SLOT(slotDirWatchDirty(QString)));
131
132
133
134
}

KFileItemModelRolesUpdater::~KFileItemModelRolesUpdater()
{
135
    resetPendingRoles();
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
}

void KFileItemModelRolesUpdater::setIconSize(const QSize& size)
{
    if (size != m_iconSize) {
        m_iconSize = size;
        if (m_paused) {
            m_iconSizeChangedDuringPausing = true;
        } else if (m_previewShown) {
            // An icon size change requires the regenerating of
            // all previews
            sortAndResolveAllRoles();
        } else {
            sortAndResolvePendingRoles();
        }
    }
}

QSize KFileItemModelRolesUpdater::iconSize() const
{
    return m_iconSize;
}

void KFileItemModelRolesUpdater::setVisibleIndexRange(int index, int count)
{
    if (index < 0) {
        index = 0;
    }
    if (count < 0) {
        count = 0;
    }

    if (index == m_firstVisibleIndex && count == m_lastVisibleIndex - m_firstVisibleIndex + 1) {
        // The range has not been changed
        return;
    }

    m_firstVisibleIndex = index;
    m_lastVisibleIndex = qMin(index + count - 1, m_model->count() - 1);

    if (hasPendingRoles() && !m_paused) {
        sortAndResolvePendingRoles();
    }
}

181
void KFileItemModelRolesUpdater::setPreviewsShown(bool show)
182
183
184
185
186
187
188
189
190
191
{
    if (show == m_previewShown) {
        return;
    }

    m_previewShown = show;
    if (!show) {
        m_clearPreviews = true;
    }

192
    updateAllPreviews();
193
194
}

195
bool KFileItemModelRolesUpdater::previewsShown() const
196
197
198
199
{
    return m_previewShown;
}

200
void KFileItemModelRolesUpdater::setEnlargeSmallPreviews(bool enlarge)
201
{
202
203
204
205
206
    if (enlarge != m_enlargeSmallPreviews) {
        m_enlargeSmallPreviews = enlarge;
        if (m_previewShown) {
            updateAllPreviews();
        }
207
    }
208
}
209

210
211
212
213
214
215
216
bool KFileItemModelRolesUpdater::enlargeSmallPreviews() const
{
    return m_enlargeSmallPreviews;
}

void KFileItemModelRolesUpdater::setEnabledPlugins(const QStringList& list)
{
217
    if (m_enabledPlugins != list) {
218
219
220
        m_enabledPlugins = list;
        if (m_previewShown) {
            updateAllPreviews();
221
222
        }
    }
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
}

void KFileItemModelRolesUpdater::setPaused(bool paused)
{
    if (paused == m_paused) {
        return;
    }

    m_paused = paused;
    if (paused) {
        if (hasPendingRoles()) {
            foreach (KJob* job, m_previewJobs) {
                job->kill();
            }
            Q_ASSERT(m_previewJobs.isEmpty());
        }
    } else {
        const bool resolveAll = (m_iconSizeChangedDuringPausing && m_previewShown) ||
241
                                m_previewChangedDuringPausing ||
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
                                m_rolesChangedDuringPausing;
        if (resolveAll) {
            sortAndResolveAllRoles();
        } else {
            sortAndResolvePendingRoles();
        }

        m_iconSizeChangedDuringPausing = false;
        m_previewChangedDuringPausing = false;
        m_rolesChangedDuringPausing = false;
    }
}

void KFileItemModelRolesUpdater::setRoles(const QSet<QByteArray>& roles)
{
257
258
    if (m_roles != roles) {
        m_roles = roles;
259

260
261
#ifdef HAVE_NEPOMUK
        // Check whether there is at least one role that must be resolved
262
263
264
        // with the help of Nepomuk. If this is the case, a (quite expensive)
        // resolving will be done in KFileItemModelRolesUpdater::rolesData() and
        // the role gets watched for changes.
265
        const KNepomukRolesProvider& rolesProvider = KNepomukRolesProvider::instance();
266
        bool hasNepomukRole = false;
267
268
269
        QSetIterator<QByteArray> it(roles);
        while (it.hasNext()) {
            const QByteArray& role = it.next();
270
            if (rolesProvider.roles().contains(role)) {
271
                hasNepomukRole = true;
272
273
274
                break;
            }
        }
275
276
277
278

        if (hasNepomukRole && !m_nepomukResourceWatcher) {
            Q_ASSERT(m_nepomukUriItems.isEmpty());

279
280
281
            m_nepomukResourceWatcher = new Nepomuk2::ResourceWatcher(this);
            connect(m_nepomukResourceWatcher, SIGNAL(propertyChanged(Nepomuk2::Resource,Nepomuk2::Types::Property,QVariantList,QVariantList)),
                    this, SLOT(applyChangedNepomukRoles(Nepomuk2::Resource)));
282
283
284
285
286
        } else if (!hasNepomukRole && m_nepomukResourceWatcher) {
            delete m_nepomukResourceWatcher;
            m_nepomukResourceWatcher = 0;
            m_nepomukUriItems.clear();
        }
287
288
#endif

289
290
        updateSortProgress();

291
292
293
294
295
        if (m_paused) {
            m_rolesChangedDuringPausing = true;
        } else {
            sortAndResolveAllRoles();
        }
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
    }
}

QSet<QByteArray> KFileItemModelRolesUpdater::roles() const
{
    return m_roles;
}

bool KFileItemModelRolesUpdater::isPaused() const
{
    return m_paused;
}

QStringList KFileItemModelRolesUpdater::enabledPlugins() const
{
    return m_enabledPlugins;
}

void KFileItemModelRolesUpdater::slotItemsInserted(const KItemRangeList& itemRanges)
{
316
    startUpdating(itemRanges);
317
318
319
320
321
}

void KFileItemModelRolesUpdater::slotItemsRemoved(const KItemRangeList& itemRanges)
{
    Q_UNUSED(itemRanges);
322

323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
    const bool allItemsRemoved = (m_model->count() == 0);

    if (!m_watchedDirs.isEmpty()) {
        // Don't let KDirWatch watch for removed items
        if (allItemsRemoved) {
            foreach (const QString& path, m_watchedDirs) {
                m_dirWatcher->removeDir(path);
            }
            m_watchedDirs.clear();
        } else {
            QMutableSetIterator<QString> it(m_watchedDirs);
            while (it.hasNext()) {
                const QString& path = it.next();
                if (m_model->index(KUrl(path)) < 0) {
                    m_dirWatcher->removeDir(path);
                    it.remove();
                }
            }
        }
    }

344
345
346
#ifdef HAVE_NEPOMUK
    if (m_nepomukResourceWatcher) {
        // Don't let the ResourceWatcher watch for removed items
347
        if (allItemsRemoved) {
348
            m_nepomukResourceWatcher->setResources(QList<Nepomuk2::Resource>());
349
            m_nepomukResourceWatcher->stop();
350
351
            m_nepomukUriItems.clear();
        } else {
352
353
354
355
            QList<Nepomuk2::Resource> newResources;
            const QList<Nepomuk2::Resource> oldResources = m_nepomukResourceWatcher->resources();
            foreach (const Nepomuk2::Resource& resource, oldResources) {
                const QUrl uri = resource.uri();
356
357
358
359
360
361
362
363
                const KUrl itemUrl = m_nepomukUriItems.value(uri);
                if (m_model->index(itemUrl) >= 0) {
                    newResources.append(resource);
                } else {
                    m_nepomukUriItems.remove(uri);
                }
            }
            m_nepomukResourceWatcher->setResources(newResources);
364
365
366
367
            if (newResources.isEmpty()) {
                Q_ASSERT(m_nepomukUriItems.isEmpty());
                m_nepomukResourceWatcher->stop();
            }
368
369
370
371
        }
    }
#endif

372
    m_firstVisibleIndex = 0;
373
    m_lastVisibleIndex = -1;
374
375
376
377
    if (!hasPendingRoles()) {
        return;
    }

378
    if (allItemsRemoved) {
379
380
        // Most probably a directory change is done. Clear all pending items
        // and also kill all ongoing preview-jobs.
381
        resetPendingRoles();
382
383
384

        m_changedItems.clear();
        m_changedItemsTimer->stop();
385
386
    } else {
        // Remove all items from m_pendingVisibleItems and m_pendingInvisibleItems
387
388
389
        // that are not part of the model anymore. The items from m_changedItems
        // don't need to be handled here, removed items are just skipped in
        // resolveChangedItems().
390
391
392
393
394
395
396
397
398
399
        for (int i = 0; i <= 1; ++i) {
            QSet<KFileItem>& pendingItems = (i == 0) ? m_pendingVisibleItems : m_pendingInvisibleItems;
            QMutableSetIterator<KFileItem> it(pendingItems);
            while (it.hasNext()) {
                const KFileItem item = it.next();
                if (m_model->index(item) < 0) {
                    pendingItems.remove(item);
                }
            }
        }
400
401
402
403
404
405
406
    }
}

void KFileItemModelRolesUpdater::slotItemsChanged(const KItemRangeList& itemRanges,
                                                  const QSet<QByteArray>& roles)
{
    Q_UNUSED(roles);
407
408
409
410
411
412

    if (m_changedItemsTimer->isActive()) {
        // A call of slotItemsChanged() has been done recently. Postpone the resolving
        // of the roles until the timer has exceeded.
        foreach (const KItemRange& itemRange, itemRanges) {
            int index = itemRange.index;
413
            for (int count = itemRange.count; count > 0; --count) {
414
415
416
417
418
419
420
421
422
                m_changedItems.insert(m_model->fileItem(index));
                ++index;
            }
        }
    } else {
        // No call of slotItemsChanged() has been done recently, resolve the roles now.
        startUpdating(itemRanges);
    }
    m_changedItemsTimer->start();
423
424
}

425
426
427
428
429
430
431
432
void KFileItemModelRolesUpdater::slotSortRoleChanged(const QByteArray& current,
                                                     const QByteArray& previous)
{
    Q_UNUSED(current);
    Q_UNUSED(previous);
    updateSortProgress();
}

433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
void KFileItemModelRolesUpdater::slotGotPreview(const KFileItem& item, const QPixmap& pixmap)
{
    m_pendingVisibleItems.remove(item);
    m_pendingInvisibleItems.remove(item);

    const int index = m_model->index(item);
    if (index < 0) {
        return;
    }

    QPixmap scaledPixmap = pixmap;

    const QString mimeType = item.mimetype();
    const int slashIndex = mimeType.indexOf(QLatin1Char('/'));
    const QString mimeTypeGroup = mimeType.left(slashIndex);
    if (mimeTypeGroup == QLatin1String("image")) {
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
        if (m_enlargeSmallPreviews) {
            KPixmapModifier::applyFrame(scaledPixmap, m_iconSize);
        } else {
            // Assure that small previews don't get enlarged. Instead they
            // should be shown centered within the frame.
            const QSize contentSize = KPixmapModifier::sizeInsideFrame(m_iconSize);
            const bool enlargingRequired = scaledPixmap.width()  < contentSize.width() &&
                                           scaledPixmap.height() < contentSize.height();
            if (enlargingRequired) {
                QSize frameSize = scaledPixmap.size();
                frameSize.scale(m_iconSize, Qt::KeepAspectRatio);

                QPixmap largeFrame(frameSize);
                largeFrame.fill(Qt::transparent);

                KPixmapModifier::applyFrame(largeFrame, frameSize);

                QPainter painter(&largeFrame);
                painter.drawPixmap((largeFrame.width()  - scaledPixmap.width()) / 2,
                                   (largeFrame.height() - scaledPixmap.height()) / 2,
                                   scaledPixmap);
                scaledPixmap = largeFrame;
            } else {
                // The image must be shrinked as it is too large to fit into
                // the available icon size
                KPixmapModifier::applyFrame(scaledPixmap, m_iconSize);
            }
        }
477
478
479
480
481
482
483
484
485
486
487
488
    } else {
        KPixmapModifier::scale(scaledPixmap, m_iconSize);
    }

    QHash<QByteArray, QVariant> data = rolesData(item);
    data.insert("iconPixmap", scaledPixmap);

    disconnect(m_model, SIGNAL(itemsChanged(KItemRangeList,QSet<QByteArray>)),
               this,    SLOT(slotItemsChanged(KItemRangeList,QSet<QByteArray>)));
    m_model->setData(index, data);
    connect(m_model, SIGNAL(itemsChanged(KItemRangeList,QSet<QByteArray>)),
            this,    SLOT(slotItemsChanged(KItemRangeList,QSet<QByteArray>)));
489
490

    applySortProgressToModel();
491
492
493
494
495
496
497
498
499
500
501
}

void KFileItemModelRolesUpdater::slotPreviewFailed(const KFileItem& item)
{
    m_pendingVisibleItems.remove(item);
    m_pendingInvisibleItems.remove(item);

    const bool clearPreviews = m_clearPreviews;
    m_clearPreviews = true;
    applyResolvedRoles(item, ResolveAll);
    m_clearPreviews = clearPreviews;
502
503

    applySortProgressToModel();
504
505
506
507
508
509
510
511
512
513
514
515
516
}

void KFileItemModelRolesUpdater::slotPreviewJobFinished(KJob* job)
{
#ifdef KFILEITEMMODELROLESUPDATER_DEBUG
    kDebug() << "Preview job finished. Pending visible:" << m_pendingVisibleItems.count() << "invisible:" << m_pendingInvisibleItems.count();
#endif

    m_previewJobs.removeOne(job);
    if (!m_previewJobs.isEmpty() || !hasPendingRoles()) {
        return;
    }

517
518
    const KFileItemList visibleItems = sortedItems(m_pendingVisibleItems);
    startPreviewJob(visibleItems + m_pendingInvisibleItems.toList());
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
}

void KFileItemModelRolesUpdater::resolveNextPendingRoles()
{
    if (m_paused) {
        return;
    }

    if (m_previewShown) {
        // The preview has been turned on since the last run. Skip
        // resolving further pending roles as this is done as soon
        // as a preview has been received.
        return;
    }

    int resolvedCount = 0;
    bool changed = false;
    for (int i = 0; i <= 1; ++i) {
        QSet<KFileItem>& pendingItems = (i == 0) ? m_pendingVisibleItems : m_pendingInvisibleItems;
538
539
540
541
        QSet<KFileItem>::iterator it = pendingItems.begin();
        while (it != pendingItems.end() && !changed && resolvedCount < MaxResolveItemsCount) {
            changed = applyResolvedRoles(*it, ResolveAll);
            it = pendingItems.erase(it);
542
543
544
545
546
547
548
549
550
551
            ++resolvedCount;
        }
    }

    if (hasPendingRoles()) {
        QTimer::singleShot(0, this, SLOT(resolveNextPendingRoles()));
    } else {
        m_clearPreviews = false;
    }

552
553
    applySortProgressToModel();

554
555
556
557
558
559
560
561
562
563
#ifdef KFILEITEMMODELROLESUPDATER_DEBUG
    static int callCount = 0;
    ++callCount;
    if (callCount % 100 == 0) {
        kDebug() << "Remaining visible roles to resolve:" << m_pendingVisibleItems.count()
                 << "invisible:" << m_pendingInvisibleItems.count();
    }
#endif
}

564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
void KFileItemModelRolesUpdater::resolveChangedItems()
{
    if (m_changedItems.isEmpty()) {
        return;
    }

    KItemRangeList itemRanges;

    QSetIterator<KFileItem> it(m_changedItems);
    while (it.hasNext()) {
        const KFileItem& item = it.next();
        const int index = m_model->index(item);
        if (index >= 0) {
            itemRanges.append(KItemRange(index, 1));
        }
    }
580
    m_changedItems.clear();
581
582
583
584

    startUpdating(itemRanges);
}

585
void KFileItemModelRolesUpdater::applyChangedNepomukRoles(const Nepomuk2::Resource& resource)
586
587
{
#ifdef HAVE_NEPOMUK
588
    const KUrl itemUrl = m_nepomukUriItems.value(resource.uri());
589
    const KFileItem item = m_model->fileItem(itemUrl);
590
591
592
593
594
595
596

    if (item.isNull()) {
        // itemUrl is not in the model anymore, probably because
        // the corresponding file has been deleted in the meantime.
        return;
    }

597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
    QHash<QByteArray, QVariant> data = rolesData(item);

    const KNepomukRolesProvider& rolesProvider = KNepomukRolesProvider::instance();
    QHashIterator<QByteArray, QVariant> it(rolesProvider.roleValues(resource, m_roles));
    while (it.hasNext()) {
        it.next();
        data.insert(it.key(), it.value());
    }

    disconnect(m_model, SIGNAL(itemsChanged(KItemRangeList,QSet<QByteArray>)),
               this,    SLOT(slotItemsChanged(KItemRangeList,QSet<QByteArray>)));
    const int index = m_model->index(item);
    m_model->setData(index, data);
    connect(m_model, SIGNAL(itemsChanged(KItemRangeList,QSet<QByteArray>)),
            this,    SLOT(slotItemsChanged(KItemRangeList,QSet<QByteArray>)));
#else
613
#ifndef Q_CC_MSVC
614
615
    Q_UNUSED(resource);
#endif
616
#endif
617
618
}

619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
void KFileItemModelRolesUpdater::slotDirWatchDirty(const QString& path)
{
    const bool getSizeRole = m_roles.contains("size");
    const bool getIsExpandableRole = m_roles.contains("isExpandable");

    if (getSizeRole || getIsExpandableRole) {
        const int index = m_model->index(KUrl(path));
        if (index >= 0) {
            QHash<QByteArray, QVariant> data;

            const int count = subItemsCount(path);
            if (getSizeRole) {
                data.insert("size", count);
            }
            if (getIsExpandableRole) {
                data.insert("isExpandable", count > 0);
            }

            m_model->setData(index, data);
        }
    }
}

642
643
644
645
646
647
648
649
650
void KFileItemModelRolesUpdater::startUpdating(const KItemRangeList& itemRanges)
{
    // If no valid index range is given assume that all items are visible.
    // A cleanup will be done later as soon as the index range has been set.
    const bool hasValidIndexRange = (m_lastVisibleIndex >= 0);

    if (hasValidIndexRange) {
        // Move all current pending visible items that are not visible anymore
        // to the pending invisible items.
651
652
653
        QSet<KFileItem>::iterator it = m_pendingVisibleItems.begin();
        while (it != m_pendingVisibleItems.end()) {
            const KFileItem item = *it;
654
655
            const int index = m_model->index(item);
            if (index < m_firstVisibleIndex || index > m_lastVisibleIndex) {
656
                it = m_pendingVisibleItems.erase(it);
657
                m_pendingInvisibleItems.insert(item);
658
659
            } else {
                ++it;
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
            }
        }
    }

    int rangesCount = 0;

    foreach (const KItemRange& range, itemRanges) {
        rangesCount += range.count;

        // Add the inserted items to the pending visible and invisible items
        const int lastIndex = range.index + range.count - 1;
        for (int i = range.index; i <= lastIndex; ++i) {
            const KFileItem item = m_model->fileItem(i);
            if (!hasValidIndexRange || (i >= m_firstVisibleIndex && i <= m_lastVisibleIndex)) {
                m_pendingVisibleItems.insert(item);
            } else {
                m_pendingInvisibleItems.insert(item);
            }
        }
    }

681
    resolvePendingRoles();
682
683
}

684
685
void KFileItemModelRolesUpdater::startPreviewJob(const KFileItemList& items)
{
686
    if (items.isEmpty() || m_paused) {
687
688
689
690
691
692
693
694
695
696
697
        return;
    }

    // PreviewJob internally caches items always with the size of
    // 128 x 128 pixels or 256 x 256 pixels. A (slow) downscaling is done
    // by PreviewJob if a smaller size is requested. For images KFileItemModelRolesUpdater must
    // do a downscaling anyhow because of the frame, so in this case only the provided
    // cache sizes are requested.
    const QSize cacheSize = (m_iconSize.width() > 128) || (m_iconSize.height() > 128)
                            ? QSize(256, 256) : QSize(128, 128);

698
699
700
701
    // KIO::filePreview() will request the MIME-type of all passed items, which (in the
    // worst case) might block the application for several seconds. To prevent such
    // a blocking the MIME-type of the items will determined until the MaxBlockTimeout
    // has been reached and only those items will get passed. As soon as the MIME-type
702
    // has been resolved once KIO::PreviewJob() can already access the resolved
703
704
705
    // MIME-type in a fast way.
    QElapsedTimer timer;
    timer.start();
706

707
    KFileItemList itemSubSet;
708
709
710
    const int count = items.count();
    itemSubSet.reserve(count);
    for (int i = 0; i < count; ++i) {
711
712
        KFileItem item = items.at(i);
        item.determineMimeType();
713
        itemSubSet.append(item);
714
715
716
717
718
719
        if (timer.elapsed() > MaxBlockTimeout) {
#ifdef KFILEITEMMODELROLESUPDATER_DEBUG
            kDebug() << "Maximum time of" << MaxBlockTimeout << "ms exceeded, creating only previews for"
                     << (i + 1) << "items," << (items.count() - (i + 1)) << "will be resolved later";
#endif
            break;
720
721
        }
    }
722
723
    KIO::PreviewJob* job = new KIO::PreviewJob(itemSubSet, cacheSize, &m_enabledPlugins);
    job->setIgnoreMaximumSize(items.first().isLocalFile());
724
725
726
    if (job->ui()) {
        job->ui()->setWindow(qApp->activeWindow());
    }
727

Laurent Montel's avatar
Laurent Montel committed
728
729
    connect(job,  SIGNAL(gotPreview(KFileItem,QPixmap)),
            this, SLOT(slotGotPreview(KFileItem,QPixmap)));
730
731
732
733
734
735
736
737
738
739
740
741
742
743
    connect(job,  SIGNAL(failed(KFileItem)),
            this, SLOT(slotPreviewFailed(KFileItem)));
    connect(job,  SIGNAL(finished(KJob*)),
            this, SLOT(slotPreviewJobFinished(KJob*)));

    m_previewJobs.append(job);
}


bool KFileItemModelRolesUpdater::hasPendingRoles() const
{
    return !m_pendingVisibleItems.isEmpty() || !m_pendingInvisibleItems.isEmpty();
}

744
745
746
747
void KFileItemModelRolesUpdater::resolvePendingRoles()
{
    int resolvedCount = 0;

748
749
750
751
752
753
754
755
756
757
758
    bool hasSlowRoles = m_previewShown;
    if (!hasSlowRoles) {
        QSetIterator<QByteArray> it(m_roles);
        while (it.hasNext()) {
            if (m_resolvableRoles.contains(it.next())) {
                hasSlowRoles = true;
                break;
            }
        }
    }

759
760
761
762
763
764
765
766
767
    const ResolveHint resolveHint = hasSlowRoles ? ResolveFast : ResolveAll;

    // Resolving the MIME type can be expensive. Assure that not more than MaxBlockTimeout ms are
    // spend for resolving them synchronously. Usually this is more than enough to determine
    // all visible items, but there are corner cases where this limit gets easily exceeded.
    QElapsedTimer timer;
    timer.start();

    // Resolve the MIME type of all visible items
768
769
770
    QSet<KFileItem>::iterator visibleIt = m_pendingVisibleItems.begin();
    while (visibleIt != m_pendingVisibleItems.end()) {
        const KFileItem item = *visibleIt;
771
772
        if (!hasSlowRoles) {
            Q_ASSERT(!m_pendingInvisibleItems.contains(item));
773
            // All roles will be resolved by applyResolvedRoles()
774
775
776
            visibleIt = m_pendingVisibleItems.erase(visibleIt);
        } else {
            ++visibleIt;
777
        }
778
        applyResolvedRoles(item, resolveHint);
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
        ++resolvedCount;

        if (timer.elapsed() > MaxBlockTimeout) {
            break;
        }
    }

    // Resolve the MIME type of the invisible items at least until the timeout
    // has been exceeded or the maximum number of items has been reached
    KFileItemList invisibleItems;
    if (m_lastVisibleIndex >= 0) {
        // The visible range is valid, don't care about the order how the MIME
        // type of invisible items get resolved
        invisibleItems = m_pendingInvisibleItems.toList();
    } else {
        // The visible range is temporary invalid (e.g. happens when loading
        // a directory) so take care to sort the currently invisible items where
        // a part will get visible later
        invisibleItems = sortedItems(m_pendingInvisibleItems);
    }

    int index = 0;
    while (resolvedCount < MaxResolveItemsCount && index < invisibleItems.count() && timer.elapsed() <= MaxBlockTimeout) {
        const KFileItem item = invisibleItems.at(index);
        applyResolvedRoles(item, resolveHint);

        if (!hasSlowRoles) {
            // All roles have been resolved already by applyResolvedRoles()
            m_pendingInvisibleItems.remove(item);
        }
        ++index;
        ++resolvedCount;
    }

    if (m_previewShown) {
        KFileItemList items = sortedItems(m_pendingVisibleItems);
        items += invisibleItems;
        startPreviewJob(items);
    } else {
        QTimer::singleShot(0, this, SLOT(resolveNextPendingRoles()));
    }

#ifdef KFILEITEMMODELROLESUPDATER_DEBUG
    if (timer.elapsed() > MaxBlockTimeout) {
        kDebug() << "Maximum time of" << MaxBlockTimeout
                 << "ms exceeded, skipping items... Remaining visible:" << m_pendingVisibleItems.count()
                 << "invisible:" << m_pendingInvisibleItems.count();
    }
    kDebug() << "[TIME] Resolved pending roles:" << timer.elapsed();
#endif
829
830

    applySortProgressToModel();
831
832
}

833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
void KFileItemModelRolesUpdater::resetPendingRoles()
{
    m_pendingVisibleItems.clear();
    m_pendingInvisibleItems.clear();

    foreach (KJob* job, m_previewJobs) {
        job->kill();
    }
    Q_ASSERT(m_previewJobs.isEmpty());
}

void KFileItemModelRolesUpdater::sortAndResolveAllRoles()
{
    if (m_paused) {
        return;
    }

    resetPendingRoles();
    Q_ASSERT(m_pendingVisibleItems.isEmpty());
    Q_ASSERT(m_pendingInvisibleItems.isEmpty());

854
    if (m_model->count() == 0) {
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
        return;
    }

    // Determine all visible items
    Q_ASSERT(m_firstVisibleIndex >= 0);
    for (int i = m_firstVisibleIndex; i <= m_lastVisibleIndex; ++i) {
        const KFileItem item = m_model->fileItem(i);
        if (!item.isNull()) {
            m_pendingVisibleItems.insert(item);
        }
    }

    // Determine all invisible items
    for (int i = 0; i < m_firstVisibleIndex; ++i) {
        const KFileItem item = m_model->fileItem(i);
        if (!item.isNull()) {
            m_pendingInvisibleItems.insert(item);
        }
    }
874
875
    const int count = m_model->count();
    for (int i = m_lastVisibleIndex + 1; i < count; ++i) {
876
877
878
879
880
881
        const KFileItem item = m_model->fileItem(i);
        if (!item.isNull()) {
            m_pendingInvisibleItems.insert(item);
        }
    }

882
    resolvePendingRoles();
883
884
885
886
887
}

void KFileItemModelRolesUpdater::sortAndResolvePendingRoles()
{
    Q_ASSERT(!m_paused);
888
    if (m_model->count() == 0) {
889
890
891
892
893
894
895
896
897
898
        return;
    }

    // If no valid index range is given assume that all items are visible.
    // A cleanup will be done later as soon as the index range has been set.
    const bool hasValidIndexRange = (m_lastVisibleIndex >= 0);

    // Trigger a preview generation of all pending items. Assure that the visible
    // pending items get generated first.

899
900
901
902
903
904
905
906
907
    // Step 1: Check if any items in m_pendingVisibleItems are not visible any more
    //         and move them to m_pendingInvisibleItems.
    QSet<KFileItem>::iterator itVisible = m_pendingVisibleItems.begin();
    while (itVisible != m_pendingVisibleItems.end()) {
        const KFileItem item = *itVisible;
        if (item.isNull()) {
            itVisible = m_pendingVisibleItems.erase(itVisible);
            continue;
        }
908

909
910
911
912
913
914
915
916
917
918
919
920
921
922
        const int index = m_model->index(item);
        if (!hasValidIndexRange || (index >= m_firstVisibleIndex && index <= m_lastVisibleIndex)) {
            ++itVisible;
        } else {
            itVisible = m_pendingVisibleItems.erase(itVisible);
            m_pendingInvisibleItems.insert(item);
        }
    }

    // Step 2: Check if any items in m_pendingInvisibleItems have become visible
    //         and move them to m_pendingVisibleItems.
    QSet<KFileItem>::iterator itInvisible = m_pendingInvisibleItems.begin();
    while (itInvisible != m_pendingInvisibleItems.end()) {
        const KFileItem item = *itInvisible;
923
        if (item.isNull()) {
924
            itInvisible = m_pendingInvisibleItems.erase(itInvisible);
925
926
927
928
929
            continue;
        }

        const int index = m_model->index(item);
        if (!hasValidIndexRange || (index >= m_firstVisibleIndex && index <= m_lastVisibleIndex)) {
930
            itInvisible = m_pendingInvisibleItems.erase(itInvisible);
931
932
            m_pendingVisibleItems.insert(item);
        } else {
933
            ++itInvisible;
934
935
936
        }
    }

937
    resolvePendingRoles();
938
939
}

940
941
void KFileItemModelRolesUpdater::applySortProgressToModel()
{
942
    if (m_sortingProgress < 0) {
943
944
945
946
947
948
949
950
951
952
953
        return;
    }

    // Inform the model about the progress of the resolved items,
    // so that it can give an indication when the sorting has been finished.
    const int resolvedCount = m_model->count()
                              - m_pendingVisibleItems.count()
                              - m_pendingInvisibleItems.count();
    if (resolvedCount > 0) {
        m_model->emitSortProgress(resolvedCount);
        if (resolvedCount == m_model->count()) {
954
            m_sortingProgress = -1;
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
        }
    }
}

void KFileItemModelRolesUpdater::updateSortProgress()
{
    const QByteArray sortRole = m_model->sortRole();

    // Optimization if the sorting is done by type: In case if all MIME-types
    // are known, the types have been resolved already by KFileItemModel and
    // no sort-progress feedback is required.
    const bool showProgress = (sortRole == "type")
                              ? hasUnknownMimeTypes()
                              : m_resolvableRoles.contains(sortRole);

970
    if (m_sortingProgress >= 0) {
971
972
973
        // Mark the current sorting as finished
        m_model->emitSortProgress(m_model->count());
    }
974
    m_sortingProgress = showProgress ? 0 : -1;
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
}

bool KFileItemModelRolesUpdater::hasUnknownMimeTypes() const
{
    const int count = m_model->count();
    for (int i = 0; i < count; ++i) {
        const KFileItem item = m_model->fileItem(i);
        if (!item.isMimeTypeKnown()) {
            return true;
        }
    }

    return false;
}

990
991
bool KFileItemModelRolesUpdater::applyResolvedRoles(const KFileItem& item, ResolveHint hint)
{
992
993
994
995
    if (item.isNull()) {
        return false;
    }

996
997
998
    const bool resolveAll = (hint == ResolveAll);

    bool mimeTypeChanged = false;
999
    if (!item.isMimeTypeKnown() || !item.isFinalIconKnown()) {
1000
        item.determineMimeType();