kfileitemmodel.cpp 91.8 KB
Newer Older
1
2
3
4
5
6
7
/*
 * SPDX-FileCopyrightText: 2011 Peter Penz <peter.penz19@gmail.com>
 * SPDX-FileCopyrightText: 2013 Frank Reininghaus <frank78ac@googlemail.com>
 * SPDX-FileCopyrightText: 2013 Emmanuel Pescosta <emmanuelpescosta099@gmail.com>
 *
 * SPDX-License-Identifier: GPL-2.0-or-later
 */
8
9
10

#include "kfileitemmodel.h"

11
#include "dolphin_generalsettings.h"
12
#include "dolphin_detailsmodesettings.h"
Roman Inflianskas's avatar
Roman Inflianskas committed
13
14
15
#include "dolphindebug.h"
#include "private/kfileitemmodeldirlister.h"
#include "private/kfileitemmodelsortalgorithm.h"
16

Laurent Montel's avatar
Laurent Montel committed
17
#include <KLocalizedString>
18
19
#include <KUrlMimeData>

20
#include <QElapsedTimer>
21
#include <QMimeData>
22
#include <QTimer>
23
#include <QWidget>
24
25
26
#include <QMutex>

Q_GLOBAL_STATIC_WITH_ARGS(QMutex, s_collatorMutex, (QMutex::Recursive))
27

28
// #define KFILEITEMMODEL_DEBUG
29

30
KFileItemModel::KFileItemModel(QObject* parent) :
31
    KItemModelBase("text", parent),
Kevin Funk's avatar
Kevin Funk committed
32
    m_dirLister(nullptr),
33
    m_sortDirsFirst(true),
34
    m_sortRole(NameRole),
35
    m_sortingProgressPercent(-1),
36
    m_roles(),
37
    m_itemData(),
38
    m_items(),
Peter Penz's avatar
Peter Penz committed
39
    m_filter(),
Peter Penz's avatar
Peter Penz committed
40
    m_filteredItems(),
41
    m_requestRole(),
Kevin Funk's avatar
Kevin Funk committed
42
43
    m_maximumUpdateIntervalTimer(nullptr),
    m_resortAllItemsTimer(nullptr),
44
    m_pendingItemsToInsert(),
Peter Penz's avatar
Peter Penz committed
45
    m_groups(),
46
    m_expandedDirs(),
47
    m_urlsToExpand()
48
{
49
50
    m_collator.setNumericMode(true);

51
52
    loadSortingSettings();

53
54
    m_dirLister = new KFileItemModelDirLister(this);
    m_dirLister->setDelayedMimeTypes(true);
55
56
57
58
59

    const QWidget* parentWidget = qobject_cast<QWidget*>(parent);
    if (parentWidget) {
        m_dirLister->setMainWindow(parentWidget->window());
    }
60

61
    connect(m_dirLister, &KFileItemModelDirLister::started, this, &KFileItemModel::directoryLoadingStarted);
62
63
    connect(m_dirLister, QOverload<>::of(&KCoreDirLister::canceled), this, &KFileItemModel::slotCanceled);
    connect(m_dirLister, QOverload<const QUrl&>::of(&KCoreDirLister::completed), this, &KFileItemModel::slotCompleted);
64
65
66
    connect(m_dirLister, &KFileItemModelDirLister::itemsAdded, this, &KFileItemModel::slotItemsAdded);
    connect(m_dirLister, &KFileItemModelDirLister::itemsDeleted, this, &KFileItemModel::slotItemsDeleted);
    connect(m_dirLister, &KFileItemModelDirLister::refreshItems, this, &KFileItemModel::slotRefreshItems);
67
    connect(m_dirLister, QOverload<>::of(&KCoreDirLister::clear), this, &KFileItemModel::slotClear);
68
69
    connect(m_dirLister, &KFileItemModelDirLister::infoMessage, this, &KFileItemModel::infoMessage);
    connect(m_dirLister, &KFileItemModelDirLister::errorMessage, this, &KFileItemModel::errorMessage);
70
    connect(m_dirLister, &KFileItemModelDirLister::percent, this, &KFileItemModel::directoryLoadingProgress);
71
    connect(m_dirLister, QOverload<const QUrl&, const QUrl&>::of(&KCoreDirLister::redirection), this, &KFileItemModel::directoryRedirection);
72
    connect(m_dirLister, &KFileItemModelDirLister::urlIsFileError, this, &KFileItemModel::urlIsFileError);
73

74
    // Apply default roles that should be determined
75
76
77
    resetRoles();
    m_requestRole[NameRole] = true;
    m_requestRole[IsDirRole] = true;
78
    m_requestRole[IsLinkRole] = true;
79
    m_roles.insert("text");
80
    m_roles.insert("isDir");
81
    m_roles.insert("isLink");
82
    m_roles.insert("isHidden");
83
84
85
86
87
88

    // For slow KIO-slaves like used for searching it makes sense to show results periodically even
    // before the completed() or canceled() signal has been emitted.
    m_maximumUpdateIntervalTimer = new QTimer(this);
    m_maximumUpdateIntervalTimer->setInterval(2000);
    m_maximumUpdateIntervalTimer->setSingleShot(true);
89
    connect(m_maximumUpdateIntervalTimer, &QTimer::timeout, this, &KFileItemModel::dispatchPendingItemsToInsert);
90

91
92
93
94
95
    // When changing the value of an item which represents the sort-role a resorting must be
    // triggered. Especially in combination with KFileItemModelRolesUpdater this might be done
    // for a lot of items within a quite small timeslot. To prevent expensive resortings the
    // resorting is postponed until the timer has been exceeded.
    m_resortAllItemsTimer = new QTimer(this);
96
    m_resortAllItemsTimer->setInterval(500);
97
    m_resortAllItemsTimer->setSingleShot(true);
98
    connect(m_resortAllItemsTimer, &QTimer::timeout, this, &KFileItemModel::resortAllItems);
99

100
    connect(GeneralSettings::self(), &GeneralSettings::sortingChoiceChanged, this, &KFileItemModel::slotSortingChoiceChanged);
101
102
103
104
}

KFileItemModel::~KFileItemModel()
{
105
    qDeleteAll(m_itemData);
106
    qDeleteAll(m_filteredItems);
107
    qDeleteAll(m_pendingItemsToInsert);
108
109
}

110
void KFileItemModel::loadDirectory(const QUrl &url)
111
112
113
114
{
    m_dirLister->openUrl(url);
}

115
void KFileItemModel::refreshDirectory(const QUrl &url)
116
{
117
    // Refresh all expanded directories first (Bug 295300)
118
    QHashIterator<QUrl, QUrl> expandedDirs(m_expandedDirs);
119
120
121
    while (expandedDirs.hasNext()) {
        expandedDirs.next();
        m_dirLister->openUrl(expandedDirs.value(), KDirLister::Reload);
122
123
    }

124
125
126
    m_dirLister->openUrl(url, KDirLister::Reload);
}

127
QUrl KFileItemModel::directory() const
128
129
130
131
{
    return m_dirLister->url();
}

132
133
134
135
136
void KFileItemModel::cancelDirectoryLoading()
{
    m_dirLister->stop();
}

137
138
int KFileItemModel::count() const
{
139
    return m_itemData.count();
140
141
142
143
144
}

QHash<QByteArray, QVariant> KFileItemModel::data(int index) const
{
    if (index >= 0 && index < count()) {
145
146
147
148
149
150
        ItemData* data = m_itemData.at(index);
        if (data->values.isEmpty()) {
            data->values = retrieveData(data->item, data->parent);
        }

        return data->values;
151
152
153
154
155
156
    }
    return QHash<QByteArray, QVariant>();
}

bool KFileItemModel::setData(int index, const QHash<QByteArray, QVariant>& values)
{
157
158
159
    if (index < 0 || index >= count()) {
        return false;
    }
160

161
    QHash<QByteArray, QVariant> currentValues = data(index);
162
163
164
165
166
167

    // Determine which roles have been changed
    QSet<QByteArray> changedRoles;
    QHashIterator<QByteArray, QVariant> it(values);
    while (it.hasNext()) {
        it.next();
168
        const QByteArray role = sharedValue(it.key());
169
170
        const QVariant value = it.value();

171
172
        if (currentValues[role] != value) {
            currentValues[role] = value;
173
            changedRoles.insert(role);
174
        }
175
176
177
178
179
180
    }

    if (changedRoles.isEmpty()) {
        return false;
    }

181
    m_itemData[index]->values = currentValues;
Peter Penz's avatar
Peter Penz committed
182
    if (changedRoles.contains("text")) {
Lukáš Tinkl's avatar
Lukáš Tinkl committed
183
184
185
        QUrl url = m_itemData[index]->item.url();
        url = url.adjusted(QUrl::RemoveFilename);
        url.setPath(url.path() + currentValues["text"].toString());
Peter Penz's avatar
Peter Penz committed
186
187
188
        m_itemData[index]->item.setUrl(url);
    }

189
    emitItemsChangedAndTriggerResorting(KItemRangeList() << KItemRange(index, 1), changedRoles);
190

191
    return true;
192
193
}

194
void KFileItemModel::setSortDirectoriesFirst(bool dirsFirst)
195
{
196
197
    if (dirsFirst != m_sortDirsFirst) {
        m_sortDirsFirst = dirsFirst;
198
199
200
201
        resortAllItems();
    }
}

202
bool KFileItemModel::sortDirectoriesFirst() const
203
{
204
    return m_sortDirsFirst;
205
206
}

Peter Penz's avatar
Peter Penz committed
207
208
void KFileItemModel::setShowHiddenFiles(bool show)
{
209
210
211
    m_dirLister->setShowingDotFiles(show);
    m_dirLister->emitChanges();
    if (show) {
212
        dispatchPendingItemsToInsert();
Peter Penz's avatar
Peter Penz committed
213
214
215
216
217
    }
}

bool KFileItemModel::showHiddenFiles() const
{
218
    return m_dirLister->showingDotFiles();
Peter Penz's avatar
Peter Penz committed
219
220
}

221
void KFileItemModel::setShowDirectoriesOnly(bool enabled)
222
{
223
    m_dirLister->setDirOnlyMode(enabled);
224
225
}

226
bool KFileItemModel::showDirectoriesOnly() const
227
{
228
    return m_dirLister->dirOnlyMode();
229
230
}

231
QMimeData* KFileItemModel::createMimeData(const KItemSet& indexes) const
232
233
234
{
    QMimeData* data = new QMimeData();

235
236
    // The following code has been taken from KDirModel::mimeData()
    // (kdelibs/kio/kio/kdirmodel.cpp)
237
    // SPDX-FileCopyrightText: 2006 David Faure <faure@kde.org>
Lukáš Tinkl's avatar
Lukáš Tinkl committed
238
239
    QList<QUrl> urls;
    QList<QUrl> mostLocalUrls;
Kevin Funk's avatar
Kevin Funk committed
240
    const ItemData* lastAddedItem = nullptr;
241

242
    for (int index : indexes) {
243
244
245
246
        const ItemData* itemData = m_itemData.at(index);
        const ItemData* parent = itemData->parent;

        while (parent && parent != lastAddedItem) {
247
            parent = parent->parent;
248
249
250
251
252
253
254
255
256
        }

        if (parent && parent == lastAddedItem) {
            // A parent of 'itemData' has been added already.
            continue;
        }

        lastAddedItem = itemData;
        const KFileItem& item = itemData->item;
257
        if (!item.isNull()) {
258
            urls << item.url();
259
260

            bool isLocal;
261
            mostLocalUrls << item.mostLocalUrl(&isLocal);
262
263
264
        }
    }

265
    KUrlMimeData::setUrls(urls, mostLocalUrls, data);
266
267
268
    return data;
}

Peter Penz's avatar
Peter Penz committed
269
270
271
int KFileItemModel::indexForKeyboardSearch(const QString& text, int startFromIndex) const
{
    startFromIndex = qMax(0, startFromIndex);
Peter Penz's avatar
Peter Penz committed
272
    for (int i = startFromIndex; i < count(); ++i) {
273
        if (fileItem(i).text().startsWith(text, Qt::CaseInsensitive)) {
Peter Penz's avatar
Peter Penz committed
274
275
276
            return i;
        }
    }
Peter Penz's avatar
Peter Penz committed
277
    for (int i = 0; i < startFromIndex; ++i) {
278
        if (fileItem(i).text().startsWith(text, Qt::CaseInsensitive)) {
Peter Penz's avatar
Peter Penz committed
279
280
281
282
283
284
285
286
287
            return i;
        }
    }
    return -1;
}

bool KFileItemModel::supportsDropping(int index) const
{
    const KFileItem item = fileItem(index);
Peter Penz's avatar
Peter Penz committed
288
    return !item.isNull() && (item.isDir() || item.isDesktopFile());
Peter Penz's avatar
Peter Penz committed
289
290
}

291
292
QString KFileItemModel::roleDescription(const QByteArray& role) const
{
Peter Penz's avatar
Peter Penz committed
293
294
295
296
297
    static QHash<QByteArray, QString> description;
    if (description.isEmpty()) {
        int count = 0;
        const RoleInfoMap* map = rolesInfoMap(count);
        for (int i = 0; i < count; ++i) {
Elvis Angelaccio's avatar
Elvis Angelaccio committed
298
299
300
            if (!map[i].roleTranslation) {
                continue;
            }
301
            description.insert(map[i].role, i18nc(map[i].roleTranslationContext, map[i].roleTranslation));
Peter Penz's avatar
Peter Penz committed
302
        }
303
304
    }

Peter Penz's avatar
Peter Penz committed
305
    return description.value(role);
306
307
}

308
309
QList<QPair<int, QVariant> > KFileItemModel::groups() const
{
310
    if (!m_itemData.isEmpty() && m_groups.isEmpty()) {
Peter Penz's avatar
Peter Penz committed
311
312
313
314
#ifdef KFILEITEMMODEL_DEBUG
        QElapsedTimer timer;
        timer.start();
#endif
Peter Penz's avatar
Peter Penz committed
315
        switch (typeForRole(sortRole())) {
316
317
        case NameRole:        m_groups = nameRoleGroups(); break;
        case SizeRole:        m_groups = sizeRoleGroups(); break;
318
319
320
321
322
        case ModificationTimeRole:
            m_groups = timeRoleGroups([](const ItemData *item) {
                return item->item.time(KFileItem::ModificationTime);
            });
            break;
323
324
325
326
327
        case CreationTimeRole:
            m_groups = timeRoleGroups([](const ItemData *item) {
                return item->item.time(KFileItem::CreationTime);
            });
            break;
328
329
330
331
332
333
334
335
336
337
        case AccessTimeRole:
            m_groups = timeRoleGroups([](const ItemData *item) {
                return item->item.time(KFileItem::AccessTime);
            });
            break;
        case DeletionTimeRole:
            m_groups = timeRoleGroups([](const ItemData *item) {
                return item->values.value("deletiontime").toDateTime();
            });
            break;
338
339
        case PermissionsRole: m_groups = permissionRoleGroups(); break;
        case RatingRole:      m_groups = ratingRoleGroups(); break;
340
        default:              m_groups = genericStringRoleGroups(sortRole()); break;
Peter Penz's avatar
Peter Penz committed
341
        }
342

Peter Penz's avatar
Peter Penz committed
343
#ifdef KFILEITEMMODEL_DEBUG
344
        qCDebug(DolphinDebug) << "[TIME] Calculating groups for" << count() << "items:" << timer.elapsed();
Peter Penz's avatar
Peter Penz committed
345
346
347
348
#endif
    }

    return m_groups;
349
350
}

351
352
353
KFileItem KFileItemModel::fileItem(int index) const
{
    if (index >= 0 && index < count()) {
354
        return m_itemData.at(index)->item;
355
356
357
358
359
    }

    return KFileItem();
}

360
KFileItem KFileItemModel::fileItem(const QUrl &url) const
361
{
362
363
364
    const int indexForUrl = index(url);
    if (indexForUrl >= 0) {
        return m_itemData.at(indexForUrl)->item;
365
366
367
368
    }
    return KFileItem();
}

369
370
int KFileItemModel::index(const KFileItem& item) const
{
371
    return index(item.url());
372
373
}

374
int KFileItemModel::index(const QUrl& url) const
375
{
Lukáš Tinkl's avatar
Lukáš Tinkl committed
376
    const QUrl urlToFind = url.adjusted(QUrl::StripTrailingSlash);
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393

    const int itemCount = m_itemData.count();
    int itemsInHash = m_items.count();

    int index = m_items.value(urlToFind, -1);
    while (index < 0 && itemsInHash < itemCount) {
        // Not all URLs are stored yet in m_items. We grow m_items until either
        // urlToFind is found, or all URLs have been stored in m_items.
        // Note that we do not add the URLs to m_items one by one, but in
        // larger blocks. After each block, we check if urlToFind is in
        // m_items. We could in principle compare urlToFind with each URL while
        // we are going through m_itemData, but comparing two QUrls will,
        // unlike calling qHash for the URLs, trigger a parsing of the URLs
        // which costs both CPU cycles and memory.
        const int blockSize = 1000;
        const int currentBlockEnd = qMin(itemsInHash + blockSize, itemCount);
        for (int i = itemsInHash; i < currentBlockEnd; ++i) {
Lukáš Tinkl's avatar
Lukáš Tinkl committed
394
            const QUrl nextUrl = m_itemData.at(i)->item.url();
395
396
397
398
399
400
401
            m_items.insert(nextUrl, i);
        }

        itemsInHash = currentBlockEnd;
        index = m_items.value(urlToFind, -1);
    }

402
403
404
405
406
407
408
409
410
411
412
    if (index < 0) {
        // The item could not be found, even though all items from m_itemData
        // should be in m_items now. We print some diagnostic information which
        // might help to find the cause of the problem, but only once. This
        // prevents that obtaining and printing the debugging information
        // wastes CPU cycles and floods the shell or .xsession-errors.
        static bool printDebugInfo = true;

        if (m_items.count() != m_itemData.count() && printDebugInfo) {
            printDebugInfo = false;

413
414
415
            qCWarning(DolphinDebug) << "The model is in an inconsistent state.";
            qCWarning(DolphinDebug) << "m_items.count()    ==" << m_items.count();
            qCWarning(DolphinDebug) << "m_itemData.count() ==" << m_itemData.count();
416
417

            // Check if there are multiple items with the same URL.
Lukáš Tinkl's avatar
Lukáš Tinkl committed
418
            QMultiHash<QUrl, int> indexesForUrl;
419
420
421
422
            for (int i = 0; i < m_itemData.count(); ++i) {
                indexesForUrl.insert(m_itemData.at(i)->item.url(), i);
            }

Lukáš Tinkl's avatar
Lukáš Tinkl committed
423
            foreach (const QUrl& url, indexesForUrl.uniqueKeys()) {
424
                if (indexesForUrl.count(url) > 1) {
425
                    qCWarning(DolphinDebug) << "Multiple items found with the URL" << url;
426
427
428
429
430

                    auto it = indexesForUrl.find(url);
                    while (it != indexesForUrl.end() && it.key() == url) {
                        const ItemData* data = m_itemData.at(it.value());
                        qCWarning(DolphinDebug) << "index" << it.value() << ":" << data->item;
431
                        if (data->parent) {
432
                            qCWarning(DolphinDebug) << "parent" << data->parent->item;
433
                        }
434
                        ++it;
435
436
437
438
439
                    }
                }
            }
        }
    }
440
441

    return index;
442
443
}

444
KFileItem KFileItemModel::rootItem() const
445
{
446
    return m_dirLister->rootItem();
447
448
}

449
450
451
452
453
454
455
void KFileItemModel::clear()
{
    slotClear();
}

void KFileItemModel::setRoles(const QSet<QByteArray>& roles)
{
456
457
458
    if (m_roles == roles) {
        return;
    }
459
460

    const QSet<QByteArray> changedRoles = (roles - m_roles) + (m_roles - roles);
461
462
    m_roles = roles;

463
    if (count() > 0) {
464
465
        const bool supportedExpanding = m_requestRole[ExpandedParentsCountRole];
        const bool willSupportExpanding = roles.contains("expandedParentsCount");
466
467
468
469
470
471
472
        if (supportedExpanding && !willSupportExpanding) {
            // No expanding is supported anymore. Take care to delete all items that have an expansion level
            // that is not 0 (and hence are part of an expanded item).
            removeExpandedItems();
        }
    }

Peter Penz's avatar
Peter Penz committed
473
    m_groups.clear();
474
    resetRoles();
475

476
477
478
    QSetIterator<QByteArray> it(roles);
    while (it.hasNext()) {
        const QByteArray& role = it.next();
Peter Penz's avatar
Peter Penz committed
479
        m_requestRole[typeForRole(role)] = true;
480
481
482
483
484
485
    }

    if (count() > 0) {
        // Update m_data with the changed requested roles
        const int maxIndex = count() - 1;
        for (int i = 0; i <= maxIndex; ++i) {
486
            m_itemData[i]->values = retrieveData(m_itemData.at(i)->item, m_itemData.at(i)->parent);
487
488
        }

489
        emit itemsChanged(KItemRangeList() << KItemRange(0, count()), changedRoles);
490
    }
491
492
493
494
495
496
497
498
499

    // Clear the 'values' of all filtered items. They will be re-populated with the
    // correct roles the next time 'values' will be accessed via data(int).
    QHash<KFileItem, ItemData*>::iterator filteredIt = m_filteredItems.begin();
    const QHash<KFileItem, ItemData*>::iterator filteredEnd = m_filteredItems.end();
    while (filteredIt != filteredEnd) {
        (*filteredIt)->values.clear();
        ++filteredIt;
    }
500
501
502
503
}

QSet<QByteArray> KFileItemModel::roles() const
{
504
    return m_roles;
505
506
507
508
}

bool KFileItemModel::setExpanded(int index, bool expanded)
{
Peter Penz's avatar
Peter Penz committed
509
    if (!isExpandable(index) || isExpanded(index) == expanded) {
510
511
512
513
        return false;
    }

    QHash<QByteArray, QVariant> values;
514
    values.insert(sharedValue("isExpanded"), expanded);
515
516
517
518
    if (!setData(index, values)) {
        return false;
    }

519
    const KFileItem item = m_itemData.at(index)->item;
Lukáš Tinkl's avatar
Lukáš Tinkl committed
520
521
    const QUrl url = item.url();
    const QUrl targetUrl = item.targetUrl();
522
    if (expanded) {
523
        m_expandedDirs.insert(targetUrl, url);
524
        m_dirLister->openUrl(url, KDirLister::Keep);
525

Lukáš Tinkl's avatar
Lukáš Tinkl committed
526
527
528
        const QVariantList previouslyExpandedChildren = m_itemData.at(index)->values.value("previouslyExpandedChildren").value<QVariantList>();
        foreach (const QVariant& var, previouslyExpandedChildren) {
            m_urlsToExpand.insert(var.toUrl());
529
        }
530
    } else {
531
532
533
534
535
536
537
538
539
540
541
542
        // Note that there might be (indirect) children of the folder which is to be collapsed in
        // m_pendingItemsToInsert. To prevent that they will be inserted into the model later,
        // possibly without a parent, which might result in a crash, we insert all pending items
        // right now. All new items which would be without a parent will then be removed.
        dispatchPendingItemsToInsert();

        // Check if the index of the collapsed folder has changed. If that is the case, then items
        // were inserted before the collapsed folder, and its index needs to be updated.
        if (m_itemData.at(index)->item != item) {
            index = this->index(item);
        }

543
        m_expandedDirs.remove(targetUrl);
544
        m_dirLister->stop(url);
545

546
547
548
549
        const int parentLevel = expandedParentsCount(index);
        const int itemCount = m_itemData.count();
        const int firstChildIndex = index + 1;

Lukáš Tinkl's avatar
Lukáš Tinkl committed
550
        QVariantList expandedChildren;
551

552
553
        int childIndex = firstChildIndex;
        while (childIndex < itemCount && expandedParentsCount(childIndex) > parentLevel) {
554
555
            ItemData* itemData = m_itemData.at(childIndex);
            if (itemData->values.value("isExpanded").toBool()) {
Lukáš Tinkl's avatar
Lukáš Tinkl committed
556
557
                const QUrl targetUrl = itemData->item.targetUrl();
                const QUrl url = itemData->item.url();
558
                m_expandedDirs.remove(targetUrl);
559
                m_dirLister->stop(url);     // TODO: try to unit-test this, see https://bugs.kde.org/show_bug.cgi?id=332102#c11
560
561
                expandedChildren.append(targetUrl);
            }
562
563
564
            ++childIndex;
        }
        const int childrenCount = childIndex - firstChildIndex;
565

566
567
        removeFilteredChildren(KItemRangeList() << KItemRange(index, 1 + childrenCount));
        removeItems(KItemRangeList() << KItemRange(firstChildIndex, childrenCount), DeleteItemData);
568
569

        m_itemData.at(index)->values.insert("previouslyExpandedChildren", expandedChildren);
570
571
    }

572
    return true;
573
574
575
576
577
}

bool KFileItemModel::isExpanded(int index) const
{
    if (index >= 0 && index < count()) {
578
        return m_itemData.at(index)->values.value("isExpanded").toBool();
579
580
581
582
583
584
585
    }
    return false;
}

bool KFileItemModel::isExpandable(int index) const
{
    if (index >= 0 && index < count()) {
586
587
588
        // Call data (instead of accessing m_itemData directly)
        // to ensure that the value is initialized.
        return data(index).value("isExpandable").toBool();
589
590
591
592
    }
    return false;
}

593
594
595
int KFileItemModel::expandedParentsCount(int index) const
{
    if (index >= 0 && index < count()) {
596
        return expandedParentsCount(m_itemData.at(index));
597
598
599
600
    }
    return 0;
}

601
QSet<QUrl> KFileItemModel::expandedDirectories() const
602
{
603
604
605
606
607
608
    QSet<QUrl> result;
    const auto dirs = m_expandedDirs;
    for (const auto &dir : dirs) {
        result.insert(dir);
    }
    return result;
609
610
}

611
void KFileItemModel::restoreExpandedDirectories(const QSet<QUrl> &urls)
612
613
614
615
{
    m_urlsToExpand = urls;
}

616
void KFileItemModel::expandParentDirectories(const QUrl &url)
617
{
618

619
620
    // Assure that each sub-path of the URL that should be
    // expanded is added to m_urlsToExpand. KDirLister
621
622
    // does not care whether the parent-URL has already been
    // expanded.
Lukáš Tinkl's avatar
Lukáš Tinkl committed
623
    QUrl urlToExpand = m_dirLister->url();
624
    const int pos = urlToExpand.path().length();
625
626
627
628

    // first subdir can be empty, if m_dirLister->url().path() does not end with '/'
    // this happens if baseUrl is not root but a home directory, see FoldersPanel,
    // so using QString::SkipEmptyParts
629
    const QStringList subDirs = url.path().mid(pos).split(QDir::separator(), Qt::SkipEmptyParts);
630
    for (int i = 0; i < subDirs.count() - 1; ++i) {
631
632
633
634
635
        QString path = urlToExpand.path();
        if (!path.endsWith(QLatin1Char('/'))) {
            path.append(QLatin1Char('/'));
        }
        urlToExpand.setPath(path + subDirs.at(i));
636
        m_urlsToExpand.insert(urlToExpand);
637
638
639
640
641
    }

    // KDirLister::open() must called at least once to trigger an initial
    // loading. The pending URLs that must be restored are handled
    // in slotCompleted().
642
    QSetIterator<QUrl> it2(m_urlsToExpand);
643
644
645
646
647
648
649
    while (it2.hasNext()) {
        const int idx = index(it2.next());
        if (idx >= 0 && !isExpanded(idx)) {
            setExpanded(idx, true);
            break;
        }
    }
650
651
}

Peter Penz's avatar
Peter Penz committed
652
653
void KFileItemModel::setNameFilter(const QString& nameFilter)
{
Peter Penz's avatar
Peter Penz committed
654
    if (m_filter.pattern() != nameFilter) {
Peter Penz's avatar
Peter Penz committed
655
        dispatchPendingItemsToInsert();
Peter Penz's avatar
Peter Penz committed
656
        m_filter.setPattern(nameFilter);
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
        applyFilters();
    }
}

QString KFileItemModel::nameFilter() const
{
    return m_filter.pattern();
}

void KFileItemModel::setMimeTypeFilters(const QStringList& filters)
{
    if (m_filter.mimeTypes() != filters) {
        dispatchPendingItemsToInsert();
        m_filter.setMimeTypes(filters);
        applyFilters();
    }
}

QStringList KFileItemModel::mimeTypeFilters() const
{
    return m_filter.mimeTypes();
}
Peter Penz's avatar
Peter Penz committed
679
680


681
682
683
684
void KFileItemModel::applyFilters()
{
    // Check which shown items from m_itemData must get
    // hidden and hence moved to m_filteredItems.
685
686
687
688
689
    QVector<int> newFilteredIndexes;

    const int itemCount = m_itemData.count();
    for (int index = 0; index < itemCount; ++index) {
        ItemData* itemData = m_itemData.at(index);
690
691
692
693

        // Only filter non-expanded items as child items may never
        // exist without a parent item
        if (!itemData->values.value("isExpanded").toBool()) {
694
695
            const KFileItem item = itemData->item;
            if (!m_filter.matches(item)) {
696
                newFilteredIndexes.append(index);
697
                m_filteredItems.insert(item, itemData);
Peter Penz's avatar
Peter Penz committed
698
699
            }
        }
700
    }
Peter Penz's avatar
Peter Penz committed
701

702
703
    const KItemRangeList removedRanges = KItemRangeList::fromSortedContainer(newFilteredIndexes);
    removeItems(removedRanges, KeepItemData);
Peter Penz's avatar
Peter Penz committed
704

705
706
    // Check which hidden items from m_filteredItems should
    // get visible again and hence removed from m_filteredItems.
707
    QList<ItemData*> newVisibleItems;
Peter Penz's avatar
Peter Penz committed
708

709
710
711
712
713
714
715
    QHash<KFileItem, ItemData*>::iterator it = m_filteredItems.begin();
    while (it != m_filteredItems.end()) {
        if (m_filter.matches(it.key())) {
            newVisibleItems.append(it.value());
            it = m_filteredItems.erase(it);
        } else {
            ++it;
Peter Penz's avatar
Peter Penz committed
716
717
718
        }
    }

719
    insertItems(newVisibleItems);
Peter Penz's avatar
Peter Penz committed
720
721
}

722
void KFileItemModel::removeFilteredChildren(const KItemRangeList& itemRanges)
723
{
724
725
726
    if (m_filteredItems.isEmpty() || !m_requestRole[ExpandedParentsCountRole]) {
        // There are either no filtered items, or it is not possible to expand
        // folders -> there cannot be any filtered children.
727
728
729
        return;
    }

730
731
732
733
734
735
    QSet<ItemData*> parents;
    foreach (const KItemRange& range, itemRanges) {
        for (int index = range.index; index < range.index + range.count; ++index) {
            parents.insert(m_itemData.at(index));
        }
    }
736
737
738

    QHash<KFileItem, ItemData*>::iterator it = m_filteredItems.begin();
    while (it != m_filteredItems.end()) {
739
        if (parents.contains(it.value()->parent)) {
740
741
742
743
744
745
746
747
            delete it.value();
            it = m_filteredItems.erase(it);
        } else {
            ++it;
        }
    }
}

Peter Penz's avatar
Peter Penz committed
748
749
750
751
752
753
754
755
756
757
QList<KFileItemModel::RoleInfo> KFileItemModel::rolesInformation()
{
    static QList<RoleInfo> rolesInfo;
    if (rolesInfo.isEmpty()) {
        int count = 0;
        const RoleInfoMap* map = rolesInfoMap(count);
        for (int i = 0; i < count; ++i) {
            if (map[i].roleType != NoRole) {
                RoleInfo info;
                info.role = map[i].role;
758
                info.translation = i18nc(map[i].roleTranslationContext, map[i].roleTranslation);
759
760
761
762
763
764
765
766
                if (map[i].groupTranslation) {
                    info.group = i18nc(map[i].groupTranslationContext, map[i].groupTranslation);
                } else {
                    // For top level roles, groupTranslation is 0. We must make sure that
                    // info.group is an empty string then because the code that generates
                    // menus tries to put the actions into sub menus otherwise.
                    info.group = QString();
                }
Vishesh Handa's avatar
Vishesh Handa committed
767
                info.requiresBaloo = map[i].requiresBaloo;
768
                info.requiresIndexer = map[i].requiresIndexer;
Peter Penz's avatar
Peter Penz committed
769
770
771
772
773
774
775
776
                rolesInfo.append(info);
            }
        }
    }

    return rolesInfo;
}

777
void KFileItemModel::onGroupedSortingChanged(bool current)
778
{
779
    Q_UNUSED(current)
Peter Penz's avatar
Peter Penz committed
780
    m_groups.clear();
781
782
}

783
void KFileItemModel::onSortRoleChanged(const QByteArray& current, const QByteArray& previous, bool resortItems)
784
{
785
    Q_UNUSED(previous)
Peter Penz's avatar
Peter Penz committed
786
    m_sortRole = typeForRole(current);
787
788

    if (!m_requestRole[m_sortRole]) {
789
790
791
        QSet<QByteArray> newRoles = m_roles;
        newRoles << current;
        setRoles(newRoles);
792
793
    }

794
795
796
    if (resortItems) {
        resortAllItems();
    }
797
}
798

799
800
void KFileItemModel::onSortOrderChanged(Qt::SortOrder current, Qt::SortOrder previous)
{
801
802
    Q_UNUSED(current)
    Q_UNUSED(previous)
803
    resortAllItems();
804
805
}

806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
void KFileItemModel::loadSortingSettings()
{
    using Choice = GeneralSettings::EnumSortingChoice;
    switch (GeneralSettings::sortingChoice()) {
    case Choice::NaturalSorting:
        m_naturalSorting = true;
        m_collator.setCaseSensitivity(Qt::CaseInsensitive);
        break;
    case Choice::CaseSensitiveSorting:
        m_naturalSorting = false;
        m_collator.setCaseSensitivity(Qt::CaseSensitive);
        break;
    case Choice::CaseInsensitiveSorting:
        m_naturalSorting = false;
        m_collator.setCaseSensitivity(Qt::CaseInsensitive);
        break;
    default:
        Q_UNREACHABLE();
    }
Jaime Torres Amate's avatar
Jaime Torres Amate committed
825
826
827
    // Workaround for bug https://bugreports.qt.io/browse/QTBUG-69361
    // Force the clean state of QCollator in single thread to avoid thread safety problems in sort
    m_collator.compare(QString(), QString());
828
829
}

830
831
832
void KFileItemModel::resortAllItems()
{
    m_resortAllItemsTimer->stop();
833

834
835
836
837
838
839
840
841
    const int itemCount = count();
    if (itemCount <= 0) {
        return;
    }

#ifdef KFILEITEMMODEL_DEBUG
    QElapsedTimer timer;
    timer.start();
842
843
    qCDebug(DolphinDebug) << "===========================================================";
    qCDebug(DolphinDebug) << "Resorting" << itemCount << "items";
844
845
846
847
848
#endif

    // Remember the order of the current URLs so
    // that it can be determined which indexes have
    // been moved because of the resorting.
Lukáš Tinkl's avatar
Lukáš Tinkl committed
849
    QList<QUrl> oldUrls;
850
851
852
853
    oldUrls.reserve(itemCount);
    foreach (const ItemData* itemData, m_itemData) {
        oldUrls.append(itemData->item.url());
    }
854

855
    m_items.clear();
856
    m_items.reserve(itemCount);
857

858
    // Resort the items
859
    sort(m_itemData.begin(), m_itemData.end());
860
861
862
    for (int i = 0; i < itemCount; ++i) {
        m_items.insert(m_itemData.at(i)->item.url(), i);
    }
863

864
865
866
867
868
    // Determine the first index that has been moved.
    int firstMovedIndex = 0;
    while (firstMovedIndex < itemCount
           && firstMovedIndex == m_items.value(oldUrls.at(firstMovedIndex))) {
        ++firstMovedIndex;
869
    }
870

871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
    const bool itemsHaveMoved = firstMovedIndex < itemCount;
    if (itemsHaveMoved) {
        m_groups.clear();

        int lastMovedIndex = itemCount - 1;
        while (lastMovedIndex > firstMovedIndex
               && lastMovedIndex == m_items.value(oldUrls.at(lastMovedIndex))) {
            --lastMovedIndex;
        }

        Q_ASSERT(firstMovedIndex <= lastMovedIndex);

        // Create a list movedToIndexes, which has the property that
        // movedToIndexes[i] is the new index of the item with the old index
        // firstMovedIndex + i.
        const int movedItemsCount = lastMovedIndex - firstMovedIndex + 1;
        QList<int> movedToIndexes;
        movedToIndexes.reserve(movedItemsCount);
        for (int i = firstMovedIndex; i <= lastMovedIndex; ++i) {
            const int newIndex = m_items.value(oldUrls.at(i));
            movedToIndexes.append(newIndex);
        }

        emit itemsMoved(KItemRange(firstMovedIndex, movedItemsCount), movedToIndexes);
    } else if (groupedSorting()) {
        // The groups might have changed even if the order of the items has not.
        const QList<QPair<int, QVariant> > oldGroups = m_groups;
        m_groups.clear();
        if (groups() != oldGroups) {
            emit groupsChanged();
        }
    }
903

904
#ifdef KFILEITEMMODEL_DEBUG
905
    qCDebug(DolphinDebug) << "[TIME] Resorting of" << itemCount << "items:" << timer.elapsed();
906
#endif
907
908
909
910
}

void KFileItemModel::slotCompleted()
{
911
    m_maximumUpdateIntervalTimer->stop();
912
    dispatchPendingItemsToInsert();
913

914
    if (!m_urlsToExpand.isEmpty()) {
915
916
917
918
        // Try to find a URL that can be expanded.
        // Note that the parent folder must be expanded before any of its subfolders become visible.
        // Therefore, some URLs in m_restoredExpandedUrls might not be visible yet
        // -> we expand the first visible URL we find in m_restoredExpandedUrls.
Lukáš Tinkl's avatar
Lukáš Tinkl committed
919
        foreach (const QUrl& url, m_urlsToExpand) {
920
921
            const int indexForUrl = index(url);
            if (indexForUrl >= 0) {
922
                m_urlsToExpand.remove(url);
923
                if (setExpanded(indexForUrl, true)) {
924
925
926
927
                    // The dir lister has been triggered. This slot will be called
                    // again after the directory has been expanded.
                    return;
                }
928
929
930
931
932
            }
        }

        // None of the URLs in m_restoredExpandedUrls could be found in the model. This can happen
        // if these URLs have been deleted in the meantime.
933
        m_urlsToExpand.clear();
934
935
    }

936
    emit directoryLoadingCompleted();
937
938
939
940
941
}

void KFileItemModel::slotCanceled()
{
    m_maximumUpdateIntervalTimer->stop();
942
    dispatchPendingItemsToInsert();
943
944

    emit directoryLoadingCanceled();
945
946
}

947
void KFileItemModel::slotItemsAdded(const QUrl &directoryUrl, const KFileItemList& items)
948
{
949
950
    Q_ASSERT(!items.isEmpty());

Lukáš Tinkl's avatar
Lukáš Tinkl committed
951
    QUrl parentUrl;
952
953
954
    if (m_expandedDirs.contains(directoryUrl)) {
        parentUrl = m_expandedDirs.value(directoryUrl);
    } else {
Lukáš Tinkl's avatar
Lukáš Tinkl committed
955
        parentUrl = directoryUrl.adjusted(QUrl::StripTrailingSlash);
956
    }
957

958
    if (m_requestRole[ExpandedParentsCountRole]) {
959
960
961
962
963
        // If the expanding of items is enabled, the call
        // dirLister->openUrl(url, KDirLister::Keep) in KFileItemModel::setExpanded()
        // might result in emitting the same items twice due to the Keep-parameter.
        // This case happens if an item gets expanded, collapsed and expanded again
        // before the items could be loaded for the first expansion.
Lukáš Tinkl's avatar
Lukáš Tinkl committed
964
        if (index(items.first().url()) >= 0) {
965
966
967
968
            // The items are already part of the model.
            return;
        }

969
970
971
972
973
974
        if (directoryUrl != directory()) {
            // To be able to compare whether the new items may be inserted as children
            // of a parent item the pending items must be added to the model first.
            dispatchPendingItemsToInsert();
        }

975
976
977
        // KDirLister keeps the children of items that got expanded once even if
        // they got collapsed again with KFileItemModel::setExpanded(false). So it must be
        // checked whether the parent for new items is still expanded.
978
        const int parentIndex = index(parentUrl);
979
980
981
        if (parentIndex >= 0 && !m_itemData[parentIndex]->values.value("isExpanded").toBool()) {
            // The parent is not expanded.
            return;
982
983
984
        }
    }

985
986
    QList<ItemData*> itemDataList = createItemDataList(parentUrl, items);

987
    if (!m_filter.hasSetFilters()) {
988
        m_pendingItemsToInsert.append(itemDataList);
Peter Penz's avatar
Peter Penz committed
989
    } else {
990
        // The name or type filter is active. Hide filtered items
Peter Penz's avatar
Peter Penz committed
991
992
        // before inserting them into the model and remember
        // the filtered items in m_filteredItems.
993
994
995
        foreach (ItemData* itemData, itemDataList) {
            if (m_filter.matches(itemData->item)) {
                m_pendingItemsToInsert.append(itemData);
Peter Penz's avatar
Peter Penz committed
996
            } else {
997
                m_filteredItems.insert(itemData->item, itemData);
Peter Penz's avatar
Peter Penz committed
998
999
1000
            }
        }
    }