foldermodel.cpp 64.1 KB
Newer Older
1
2
3
4
5
6
7
8
9
/*
    SPDX-FileCopyrightText: 2006 David Faure <faure@kde.org>
    SPDX-FileCopyrightText: 2008 Fredrik Höglund <fredrik@kde.org>
    SPDX-FileCopyrightText: 2008 Rafael Fernández López <ereslibre@kde.org>
    SPDX-FileCopyrightText: 2011 Marco Martin <mart@kde.org>
    SPDX-FileCopyrightText: 2014 Eike Hein <hein@kde.org>

    SPDX-License-Identifier: GPL-2.0-or-later
*/
10
11

#include "foldermodel.h"
12
13
#include "itemviewadapter.h"
#include "positioner.h"
14
#include "removeaction.h"
Andras Mantia's avatar
Andras Mantia committed
15
#include "screenmapper.h"
16
17
18
19

#include <QApplication>
#include <QClipboard>
#include <QCollator>
20
21
22
#include <QDesktopWidget>
#include <QDrag>
#include <QImage>
23
#include <QItemSelectionModel>
Alexander Lohnau's avatar
Alexander Lohnau committed
24
#include <QLoggingCategory>
25
#include <QMenu>
26
#include <QMimeData>
27
#include <QMimeDatabase>
28
29
30
31
#include <QPainter>
#include <QPixmap>
#include <QQuickItem>
#include <QQuickWindow>
32
#include <QScreen>
33
#include <QTimer>
34
35
36
37
#include <qplatformdefs.h>

#include <KAuthorized>
#include <KConfigGroup>
Alexander Lohnau's avatar
Alexander Lohnau committed
38
#include <KDirWatch>
39
#include <KFileCopyToMenu>
40
41
#include <KFileItemActions>
#include <KFileItemListProperties>
42
#include <KIO/DeleteJob>
Alexander Lohnau's avatar
Alexander Lohnau committed
43
#include <KIO/DropJob>
44
#include <KIO/EmptyTrashJob>
45
#include <KIO/FileUndoManager>
46
#include <KIO/JobUiDelegate>
47
#include <KIO/Paste>
48
#include <KIO/PasteJob>
49
#include <KIO/RestoreJob>
50
#include <KLocalizedString>
51
#include <KPropertiesDialog>
52
#include <KSharedConfig>
53
#include <KShell>
54

55
#include <KCoreDirLister>
56
57
#include <KDesktopFile>
#include <KDirModel>
58
#include <KIO/CopyJob>
59
#include <KIO/Job>
60
#include <KIO/PreviewJob>
61
#include <KProtocolInfo>
62
#include <KRun>
63
#include <KStringHandler>
64

Andras Mantia's avatar
Andras Mantia committed
65
66
67
68
#include <Plasma/Applet>
#include <Plasma/Containment>
#include <Plasma/Corona>

69
#include <sys/stat.h>
Alexander Lohnau's avatar
Alexander Lohnau committed
70
#include <sys/types.h>
71
72
#include <unistd.h>

73
74
Q_LOGGING_CATEGORY(FOLDERMODEL, "plasma.containments.desktop.folder.foldermodel")

Alexander Lohnau's avatar
Alexander Lohnau committed
75
76
DirLister::DirLister(QObject *parent)
    : KDirLister(parent)
77
78
79
{
}

Alexander Lohnau's avatar
Alexander Lohnau committed
80
DirLister::~DirLister()
81
82
83
84
85
86
{
}

void DirLister::handleError(KIO::Job *job)
{
    if (!autoErrorHandlingEnabled()) {
Laurent Montel's avatar
Laurent Montel committed
87
        Q_EMIT error(job->errorString());
88
89
90
91
92
93
        return;
    }

    KDirLister::handleError(job);
}

Alexander Lohnau's avatar
Alexander Lohnau committed
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
FolderModel::FolderModel(QObject *parent)
    : QSortFilterProxyModel(parent)
    , m_dirWatch(nullptr)
    , m_dragInProgress(false)
    , m_urlChangedWhileDragging(false)
    , m_dropTargetPositionsCleanup(new QTimer(this))
    , m_previewGenerator(nullptr)
    , m_viewAdapter(nullptr)
    , m_actionCollection(this)
    , m_newMenu(nullptr)
    , m_fileItemActions(nullptr)
    , m_usedByContainment(false)
    , m_locked(true)
    , m_sortMode(0)
    , m_sortDesc(false)
    , m_sortDirsFirst(true)
    , m_parseDesktopFiles(false)
    , m_previews(false)
    , m_filterMode(NoFilter)
    , m_filterPatternMatchAll(true)
    , m_screenUsed(false)
    , m_screenMapper(ScreenMapper::instance())
    , m_complete(false)
{
    // needed to pass the job around with qml
119
    qmlRegisterType<KIO::DropJob>();
120
121
    DirLister *dirLister = new DirLister(this);
    dirLister->setDelayedMimeTypes(true);
122
    dirLister->setAutoErrorHandlingEnabled(false, nullptr);
123
124
    connect(dirLister, &DirLister::error, this, &FolderModel::dirListFailed);
    connect(dirLister, &KCoreDirLister::itemsDeleted, this, &FolderModel::evictFromIsDirCache);
125
126
127

    connect(dirLister, &KCoreDirLister::started, this, std::bind(&FolderModel::setStatus, this, Status::Listing));

128
    void (KCoreDirLister::*myCompletedSignal)() = &KCoreDirLister::completed;
129
130
    QObject::connect(dirLister, myCompletedSignal, this, [this] {
        setStatus(Status::Ready);
Laurent Montel's avatar
Laurent Montel committed
131
        Q_EMIT listingCompleted();
132
133
    });

134
    void (KCoreDirLister::*myCanceledSignal)() = &KCoreDirLister::canceled;
135
136
    QObject::connect(dirLister, myCanceledSignal, this, [this] {
        setStatus(Status::Canceled);
Laurent Montel's avatar
Laurent Montel committed
137
        Q_EMIT listingCanceled();
138
    });
139
140
141

    m_dirModel = new KDirModel(this);
    m_dirModel->setDirLister(dirLister);
Eike Hein's avatar
Eike Hein committed
142
    m_dirModel->setDropsAllowed(KDirModel::DropOnDirectory | KDirModel::DropOnLocalExecutable);
143

144
    // If we have dropped items queued for moving, go unsorted now.
Alexander Lohnau's avatar
Alexander Lohnau committed
145
146
147
148
    connect(this, &QAbstractItemModel::rowsAboutToBeInserted, this, [this]() {
        if (!m_dropTargetPositions.isEmpty()) {
            setSortMode(-1);
        }
149
150
151
    });

    // Position dropped items at the desired target position.
Alexander Lohnau's avatar
Alexander Lohnau committed
152
    connect(this, &QAbstractItemModel::rowsInserted, this, [this](const QModelIndex &parent, int first, int last) {
153
        for (int i = first; i <= last; ++i) {
154
155
            const auto idx = index(i, 0, parent);
            const auto url = itemForIndex(idx).url();
156
157
158
159
            auto it = m_dropTargetPositions.find(url.fileName());
            if (it != m_dropTargetPositions.end()) {
                const auto pos = it.value();
                m_dropTargetPositions.erase(it);
Laurent Montel's avatar
Laurent Montel committed
160
                Q_EMIT move(pos.x(), pos.y(), {url});
161
162
            }
        }
163
    });
164

165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
    /*
     * Dropped files may not actually show up as new files, e.g. when we overwrite
     * an existing file. Or files that fail to be listed by the dirLister, or...
     * To ensure we don't grow the map indefinitely, clean it up periodically.
     * The cleanup timer is (re)started whenever we modify the map. We use a quite
     * high interval of 10s. This should ensure, that we don't accidentally wipe
     * the mapping when we actually still want to use it. Since the time between
     * adding an entry in the map and it showing up in the model should be
     * small, this should rarely, if ever happen.
     */
    m_dropTargetPositionsCleanup->setInterval(10000);
    m_dropTargetPositionsCleanup->setSingleShot(true);
    connect(m_dropTargetPositionsCleanup, &QTimer::timeout, this, [this]() {
        if (!m_dropTargetPositions.isEmpty()) {
            qCDebug(FOLDERMODEL) << "clearing drop target positions after timeout:" << m_dropTargetPositions;
            m_dropTargetPositions.clear();
        }
    });

184
    m_selectionModel = new QItemSelectionModel(this, this);
Alexander Lohnau's avatar
Alexander Lohnau committed
185
    connect(m_selectionModel, &QItemSelectionModel::selectionChanged, this, &FolderModel::selectionChanged);
186
187
188
189
190
191
192

    setSourceModel(m_dirModel);

    setSortLocaleAware(true);
    setFilterCaseSensitivity(Qt::CaseInsensitive);
    setDynamicSortFilter(true);

Eike Hein's avatar
Eike Hein committed
193
    sort(m_sortMode, m_sortDesc ? Qt::DescendingOrder : Qt::AscendingOrder);
194

195
196
197
198
199
    createActions();
}

FolderModel::~FolderModel()
{
200
    if (m_usedByContainment) {
Andras Mantia's avatar
Andras Mantia committed
201
202
203
        // disconnect so we don't handle signals from the screen mapper when
        // removeScreen is called
        m_screenMapper->disconnect(this);
204
        m_screenMapper->removeScreen(m_screen, resolvedUrl());
Andras Mantia's avatar
Andras Mantia committed
205
    }
206
207
}

Alexander Lohnau's avatar
Alexander Lohnau committed
208
QHash<int, QByteArray> FolderModel::roleNames() const
209
{
210
211
212
    return staticRoleNames();
}

Alexander Lohnau's avatar
Alexander Lohnau committed
213
QHash<int, QByteArray> FolderModel::staticRoleNames()
214
215
{
    QHash<int, QByteArray> roleNames;
216
217
    roleNames[Qt::DisplayRole] = "display";
    roleNames[Qt::DecorationRole] = "decoration";
218
    roleNames[BlankRole] = "blank";
219
    roleNames[OverlaysRole] = "overlays";
220
221
    roleNames[SelectedRole] = "selected";
    roleNames[IsDirRole] = "isDir";
222
    roleNames[IsLinkRole] = "isLink";
223
    roleNames[IsHiddenRole] = "isHidden";
224
    roleNames[UrlRole] = "url";
225
    roleNames[LinkDestinationUrl] = "linkDestinationUrl";
226
227
    roleNames[SizeRole] = "size";
    roleNames[TypeRole] = "type";
228
    roleNames[FileNameWrappedRole] = "displayWrapped";
229
230
231
232

    return roleNames;
}

233
QPoint FolderModel::localMenuPosition() const
234
235
236
{
    QScreen *screen = nullptr;
    for (auto *s : qApp->screens()) {
237
        if (s->geometry().contains(m_menuPosition)) {
238
239
240
241
242
            screen = s;
            break;
        }
    }
    if (screen) {
243
        return m_menuPosition - screen->geometry().topLeft();
244
    }
245
    return m_menuPosition;
246
247
}

248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
void FolderModel::classBegin()
{
}

void FolderModel::componentComplete()
{
    m_complete = true;
    invalidate();
}

void FolderModel::invalidateIfComplete()
{
    if (!m_complete) {
        return;
    }

    invalidate();
}

void FolderModel::invalidateFilterIfComplete()
{
    if (!m_complete) {
        return;
    }

    invalidateFilter();
}

276
277
void FolderModel::newFileMenuItemCreated(const QUrl &url)
{
278
    if (m_usedByContainment && !m_screenMapper->sharedDesktops()) {
279
        m_screenMapper->addMapping(url, m_screen, ScreenMapper::DelayedSignal);
280
        m_dropTargetPositions.insert(url.fileName(), localMenuPosition());
281
282
        m_menuPosition = {};
        m_dropTargetPositionsCleanup->start();
283
284
285
    }
}

286
287
QString FolderModel::url() const
{
288
    return m_url;
289
290
}

Alexander Lohnau's avatar
Alexander Lohnau committed
291
void FolderModel::setUrl(const QString &url)
292
{
293
    const QUrl &resolvedNewUrl = resolve(url);
294

295
    if (url == m_url) {
296
        m_dirModel->dirLister()->updateDirectory(resolvedNewUrl);
297
298
299
        return;
    }

300
    const auto oldUrl = resolvedUrl();
Andras Mantia's avatar
Andras Mantia committed
301

302
    beginResetModel();
303
    m_url = url;
304
    m_isDirCache.clear();
305
    m_dirModel->dirLister()->openUrl(resolvedNewUrl);
306
    clearDragImages();
307
    m_dragIndexes.clear();
308
309
    endResetModel();

Laurent Montel's avatar
Laurent Montel committed
310
311
    Q_EMIT urlChanged();
    Q_EMIT resolvedUrlChanged();
312
313

    m_errorString.clear();
Laurent Montel's avatar
Laurent Montel committed
314
    Q_EMIT errorStringChanged();
315
316
317

    if (m_dirWatch) {
        delete m_dirWatch;
318
        m_dirWatch = nullptr;
319
320
    }

321
    if (resolvedNewUrl.isValid()) {
322
323
324
        m_dirWatch = new KDirWatch(this);
        connect(m_dirWatch, &KDirWatch::created, this, &FolderModel::iconNameChanged);
        connect(m_dirWatch, &KDirWatch::dirty, this, &FolderModel::iconNameChanged);
325
        m_dirWatch->addFile(resolvedNewUrl.toLocalFile() + QLatin1String("/.directory"));
326
327
    }

328
329
330
331
    if (m_dragInProgress) {
        m_urlChangedWhileDragging = true;
    }

Laurent Montel's avatar
Laurent Montel committed
332
    Q_EMIT iconNameChanged();
Andras Mantia's avatar
Andras Mantia committed
333

334
    if (m_usedByContainment && !m_screenMapper->sharedDesktops()) {
Andras Mantia's avatar
Andras Mantia committed
335
        m_screenMapper->removeScreen(m_screen, oldUrl);
336
        m_screenMapper->addScreen(m_screen, resolvedUrl());
Andras Mantia's avatar
Andras Mantia committed
337
    }
338
339
}

340
341
342
343
344
QUrl FolderModel::resolvedUrl() const
{
    return m_dirModel->dirLister()->url();
}

Alexander Lohnau's avatar
Alexander Lohnau committed
345
QUrl FolderModel::resolve(const QString &url)
346
347
348
{
    QUrl resolvedUrl;

349
    if (url.startsWith(QLatin1Char('~'))) {
350
351
352
353
354
355
356
357
        resolvedUrl = QUrl::fromLocalFile(KShell::tildeExpand(url));
    } else {
        resolvedUrl = QUrl::fromUserInput(url);
    }

    return resolvedUrl;
}

358
359
360
361
362
363
364
365
366
367
368
QString FolderModel::iconName() const
{
    const KFileItem rootItem(m_dirModel->dirLister()->url());

    if (!rootItem.isFinalIconKnown()) {
        rootItem.determineMimeType();
    }

    return rootItem.iconName();
}

369
370
371
372
373
374
375
376
377
FolderModel::Status FolderModel::status() const
{
    return m_status;
}

void FolderModel::setStatus(Status status)
{
    if (m_status != status) {
        m_status = status;
Laurent Montel's avatar
Laurent Montel committed
378
        Q_EMIT statusChanged();
379
380
381
    }
}

382
383
384
385
386
QString FolderModel::errorString() const
{
    return m_errorString;
}

387
388
389
390
391
bool FolderModel::dragging() const
{
    return m_dragInProgress;
}

392
393
394
395
396
397
398
399
400
401
bool FolderModel::usedByContainment() const
{
    return m_usedByContainment;
}

void FolderModel::setUsedByContainment(bool used)
{
    if (m_usedByContainment != used) {
        m_usedByContainment = used;

402
        QAction *action = m_actionCollection.action(QStringLiteral("refresh"));
403
404
405

        if (action) {
            action->setText(m_usedByContainment ? i18n("&Refresh Desktop") : i18n("&Refresh View"));
406
            action->setIcon(m_usedByContainment ? QIcon::fromTheme(QStringLiteral("user-desktop")) : QIcon::fromTheme(QStringLiteral("view-refresh")));
407
408
        }

409
410
411
412
        m_screenMapper->disconnect(this);
        connect(m_screenMapper, &ScreenMapper::screensChanged, this, &FolderModel::invalidateFilterIfComplete);
        connect(m_screenMapper, &ScreenMapper::screenMappingChanged, this, &FolderModel::invalidateFilterIfComplete);

Laurent Montel's avatar
Laurent Montel committed
413
        Q_EMIT usedByContainmentChanged();
414
415
416
    }
}

417
418
419
420
421
422
423
424
425
426
bool FolderModel::locked() const
{
    return m_locked;
}

void FolderModel::setLocked(bool locked)
{
    if (m_locked != locked) {
        m_locked = locked;

Laurent Montel's avatar
Laurent Montel committed
427
        Q_EMIT lockedChanged();
428
429
430
    }
}

Alexander Lohnau's avatar
Alexander Lohnau committed
431
void FolderModel::dirListFailed(const QString &error)
432
433
{
    m_errorString = error;
Laurent Montel's avatar
Laurent Montel committed
434
    Q_EMIT errorStringChanged();
435
436
437
438
439
440
441
442
443
444
445
446
}

int FolderModel::sortMode() const
{
    return m_sortMode;
}

void FolderModel::setSortMode(int mode)
{
    if (m_sortMode != mode) {
        m_sortMode = mode;

Eike Hein's avatar
Eike Hein committed
447
        if (mode == -1 /* Unsorted */) {
448
449
            setDynamicSortFilter(false);
        } else {
450
            invalidateIfComplete();
Eike Hein's avatar
Eike Hein committed
451
452
            sort(m_sortMode, m_sortDesc ? Qt::DescendingOrder : Qt::AscendingOrder);
            setDynamicSortFilter(true);
453
        }
454

Laurent Montel's avatar
Laurent Montel committed
455
        Q_EMIT sortModeChanged();
456
457
458
459
460
461
462
463
464
465
466
467
468
    }
}

bool FolderModel::sortDesc() const
{
    return m_sortDesc;
}

void FolderModel::setSortDesc(bool desc)
{
    if (m_sortDesc != desc) {
        m_sortDesc = desc;

Eike Hein's avatar
Eike Hein committed
469
        if (m_sortMode != -1 /* Unsorted */) {
470
            invalidateIfComplete();
Eike Hein's avatar
Eike Hein committed
471
            sort(m_sortMode, m_sortDesc ? Qt::DescendingOrder : Qt::AscendingOrder);
472
        }
473

Laurent Montel's avatar
Laurent Montel committed
474
        Q_EMIT sortDescChanged();
475
476
477
478
479
480
481
482
483
484
485
486
487
    }
}

bool FolderModel::sortDirsFirst() const
{
    return m_sortDirsFirst;
}

void FolderModel::setSortDirsFirst(bool enable)
{
    if (m_sortDirsFirst != enable) {
        m_sortDirsFirst = enable;

Eike Hein's avatar
Eike Hein committed
488
        if (m_sortMode != -1 /* Unsorted */) {
489
            invalidateIfComplete();
Eike Hein's avatar
Eike Hein committed
490
            sort(m_sortMode, m_sortDesc ? Qt::DescendingOrder : Qt::AscendingOrder);
491
        }
492

Laurent Montel's avatar
Laurent Montel committed
493
        Q_EMIT sortDirsFirstChanged();
494
495
496
497
498
499
500
501
502
503
504
505
    }
}

bool FolderModel::parseDesktopFiles() const
{
    return m_parseDesktopFiles;
}

void FolderModel::setParseDesktopFiles(bool enable)
{
    if (m_parseDesktopFiles != enable) {
        m_parseDesktopFiles = enable;
Laurent Montel's avatar
Laurent Montel committed
506
        Q_EMIT parseDesktopFilesChanged();
507
508
509
    }
}

Alexander Lohnau's avatar
Alexander Lohnau committed
510
QObject *FolderModel::viewAdapter() const
511
512
513
514
{
    return m_viewAdapter;
}

Alexander Lohnau's avatar
Alexander Lohnau committed
515
void FolderModel::setViewAdapter(QObject *adapter)
516
517
518
519
520
521
522
523
524
{
    if (m_viewAdapter != adapter) {
        KAbstractViewAdapter *abstractViewAdapter = dynamic_cast<KAbstractViewAdapter *>(adapter);

        m_viewAdapter = abstractViewAdapter;

        if (m_viewAdapter && !m_previewGenerator) {
            m_previewGenerator = new KFilePreviewGenerator(abstractViewAdapter, this);
            m_previewGenerator->setPreviewShown(m_previews);
525
            m_previewGenerator->setEnabledPlugins(m_effectivePreviewPlugins);
526
527
        }

Laurent Montel's avatar
Laurent Montel committed
528
        Q_EMIT viewAdapterChanged();
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
    }
}

bool FolderModel::previews() const
{
    return m_previews;
}

void FolderModel::setPreviews(bool previews)
{
    if (m_previews != previews) {
        m_previews = previews;

        if (m_previewGenerator) {
            m_previewGenerator->setPreviewShown(m_previews);
        }

Laurent Montel's avatar
Laurent Montel committed
546
        Q_EMIT previewsChanged();
547
548
549
550
551
552
553
554
    }
}

QStringList FolderModel::previewPlugins() const
{
    return m_previewPlugins;
}

Alexander Lohnau's avatar
Alexander Lohnau committed
555
void FolderModel::setPreviewPlugins(const QStringList &previewPlugins)
556
{
557
558
559
560
561
562
563
    QStringList effectivePlugins = previewPlugins;
    if (effectivePlugins.isEmpty()) {
        effectivePlugins = KIO::PreviewJob::defaultPlugins();
    }

    if (m_effectivePreviewPlugins != effectivePlugins) {
        m_effectivePreviewPlugins = effectivePlugins;
564
565
566

        if (m_previewGenerator) {
            m_previewGenerator->setPreviewShown(false);
567
            m_previewGenerator->setEnabledPlugins(m_effectivePreviewPlugins);
568
569
            m_previewGenerator->setPreviewShown(true);
        }
570
    }
571

572
573
    if (m_previewPlugins != previewPlugins) {
        m_previewPlugins = previewPlugins;
Laurent Montel's avatar
Laurent Montel committed
574
        Q_EMIT previewPluginsChanged();
575
576
577
578
579
580
581
582
583
584
585
586
587
    }
}

int FolderModel::filterMode() const
{
    return m_filterMode;
}

void FolderModel::setFilterMode(int filterMode)
{
    if (m_filterMode != (FilterMode)filterMode) {
        m_filterMode = (FilterMode)filterMode;

588
        invalidateFilterIfComplete();
589

Laurent Montel's avatar
Laurent Montel committed
590
        Q_EMIT filterModeChanged();
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
    }
}

QString FolderModel::filterPattern() const
{
    return m_filterPattern;
}

void FolderModel::setFilterPattern(const QString &pattern)
{
    if (m_filterPattern == pattern) {
        return;
    }

    m_filterPattern = pattern;
606
    m_filterPatternMatchAll = (pattern == QLatin1String("*"));
607

608
    const QStringList patterns = pattern.split(QLatin1Char(' '));
609
    m_regExps.clear();
Laurent Montel's avatar
Laurent Montel committed
610
    m_regExps.reserve(patterns.count());
611
612
613
614
615
616
617
618

    foreach (const QString &pattern, patterns) {
        QRegExp rx(pattern);
        rx.setPatternSyntax(QRegExp::Wildcard);
        rx.setCaseSensitivity(Qt::CaseInsensitive);
        m_regExps.append(rx);
    }

619
    invalidateFilterIfComplete();
620

Laurent Montel's avatar
Laurent Montel committed
621
    Q_EMIT filterPatternChanged();
622
623
624
625
}

QStringList FolderModel::filterMimeTypes() const
{
626
    return m_mimeSet.values();
627
628
629
630
}

void FolderModel::setFilterMimeTypes(const QStringList &mimeList)
{
Aleix Pol Gonzalez's avatar
Aleix Pol Gonzalez committed
631
    const QSet<QString> set(mimeList.constBegin(), mimeList.constEnd());
632
633
634
635

    if (m_mimeSet != set) {
        m_mimeSet = set;

636
        invalidateFilterIfComplete();
637

Laurent Montel's avatar
Laurent Montel committed
638
        Q_EMIT filterMimeTypesChanged();
639
640
641
    }
}

Andras Mantia's avatar
Andras Mantia committed
642
643
void FolderModel::setScreen(int screen)
{
644
645
646
    m_screenUsed = (screen != -1);

    if (!m_screenUsed || m_screen == screen)
Andras Mantia's avatar
Andras Mantia committed
647
648
649
        return;

    m_screen = screen;
650
    if (m_usedByContainment && !m_screenMapper->sharedDesktops()) {
651
        m_screenMapper->addScreen(screen, resolvedUrl());
Andras Mantia's avatar
Andras Mantia committed
652
    }
Laurent Montel's avatar
Laurent Montel committed
653
    Q_EMIT screenChanged();
Andras Mantia's avatar
Andras Mantia committed
654
655
}

656
657
658
659
660
KFileItem FolderModel::rootItem() const
{
    return m_dirModel->dirLister()->rootItem();
}

661
662
663
664
665
666
667
668
669
void FolderModel::up()
{
    const QUrl &up = KIO::upUrl(resolvedUrl());

    if (up.isValid()) {
        setUrl(up.toString());
    }
}

670
void FolderModel::cd(int row)
671
{
672
673
674
675
    if (row < 0) {
        return;
    }

676
677
678
679
    const QModelIndex idx = index(row, 0);
    bool isDir = data(idx, IsDirRole).toBool();

    if (isDir) {
Alexander Lohnau's avatar
Alexander Lohnau committed
680
        const KFileItem item = itemForIndex(idx);
681
682
        if (m_parseDesktopFiles && item.isDesktopFile()) {
            const KDesktopFile file(item.targetUrl().path());
Laurent Montel's avatar
Laurent Montel committed
683
            if (file.hasLinkType()) {
684
685
                setUrl(file.readUrl());
            }
686
687
        } else {
            setUrl(item.targetUrl().toString());
688
        }
689
690
    }
}
691

692
693
694
695
696
697
698
void FolderModel::run(int row)
{
    if (row < 0) {
        return;
    }

    KFileItem item = itemForIndex(index(row, 0));
699

700
701
702
703
    QUrl url(item.targetUrl());

    // FIXME TODO: This can go once we depend on a KIO w/ fe1f50caaf2.
    if (url.scheme().isEmpty()) {
704
        url.setScheme(QStringLiteral("file"));
705
    }
706

707
    KRun *run = new KRun(url, nullptr);
708
709
710
711
    // On desktop:/ we want to be able to run .desktop files right away,
    // otherwise ask for security reasons. We also don't use the targetUrl()
    // from above since we don't want the resolved /home/foo/Desktop URL.
    run->setShowScriptExecutionPrompt(item.url().scheme() != QLatin1String("desktop")
Alexander Lohnau's avatar
Alexander Lohnau committed
712
                                      || item.url().adjusted(QUrl::RemoveFilename).path() != QLatin1String("/"));
713
714
}

715
716
717
718
719
720
void FolderModel::runSelected()
{
    if (!m_selectionModel->hasSelection()) {
        return;
    }

721
722
723
724
725
726
727
    if (m_selectionModel->selectedIndexes().count() == 1) {
        run(m_selectionModel->selectedIndexes().constFirst().row());
        return;
    }

    KFileItemActions fileItemActions(this);
    KFileItemList items;
728

729
    foreach (const QModelIndex &index, m_selectionModel->selectedIndexes()) {
730
        // Skip over directories.
731
732
        if (!index.data(IsDirRole).toBool()) {
            items << itemForIndex(index);
733
        }
734
    }
735
736

    fileItemActions.runPreferredApplications(items, QString());
737
738
}

Alexander Lohnau's avatar
Alexander Lohnau committed
739
void FolderModel::rename(int row, const QString &name)
740
{
741
742
743
744
    if (row < 0) {
        return;
    }

745
746
747
748
    QModelIndex idx = index(row, 0);
    m_dirModel->setData(mapToSource(idx), name, Qt::EditRole);
}

749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
int FolderModel::fileExtensionBoundary(int row)
{
    const QModelIndex idx = index(row, 0);
    const QString &name = data(idx, Qt::DisplayRole).toString();

    int boundary = name.length();

    if (data(idx, IsDirRole).toBool()) {
        return boundary;
    }

    QMimeDatabase db;
    const QString &ext = db.suffixForFileName(name);

    if (ext.isEmpty()) {
        boundary = name.lastIndexOf(QLatin1Char('.'));

        if (boundary < 1) {
            boundary = name.length();
        }
    } else {
        boundary -= ext.length() + 1;
    }

    return boundary;
}

Laurent Montel's avatar
Laurent Montel committed
776
bool FolderModel::hasSelection() const
777
778
779
780
{
    return m_selectionModel->hasSelection();
}

781
782
bool FolderModel::isSelected(int row)
{
783
784
785
786
    if (row < 0) {
        return false;
    }

787
788
789
    return m_selectionModel->isSelected(index(row, 0));
}

790
791
void FolderModel::setSelected(int row)
{
792
793
794
795
    if (row < 0) {
        return;
    }

796
797
798
    m_selectionModel->select(index(row, 0), QItemSelectionModel::Select);
}

799
800
801
802
803
804
805
806
807
void FolderModel::toggleSelected(int row)
{
    if (row < 0) {
        return;
    }

    m_selectionModel->select(index(row, 0), QItemSelectionModel::Toggle);
}

808
void FolderModel::setRangeSelected(int anchor, int to)
809
{
810
    if (anchor < 0 || to < 0) {
811
812
813
        return;
    }

814
    QItemSelection selection(index(anchor, 0), index(to, 0));
815
816
817
    m_selectionModel->select(selection, QItemSelectionModel::ClearAndSelect);
}

818
void FolderModel::updateSelection(const QVariantList &rows, bool toggle)
819
{
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
    QItemSelection newSelection;

    int iRow = -1;

    foreach (const QVariant &row, rows) {
        iRow = row.toInt();

        if (iRow < 0) {
            return;
        }

        const QModelIndex &idx = index(iRow, 0);
        newSelection.select(idx, idx);
    }

    if (toggle) {
        QItemSelection pinnedSelection = m_pinnedSelection;
        pinnedSelection.merge(newSelection, QItemSelectionModel::Toggle);
        m_selectionModel->select(pinnedSelection, QItemSelectionModel::ClearAndSelect);
    } else {
        m_selectionModel->select(newSelection, QItemSelectionModel::ClearAndSelect);
    }
842
843
844
845
}

void FolderModel::clearSelection()
{
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
    if (m_selectionModel->hasSelection()) {
        m_selectionModel->clear();
    }
}

void FolderModel::pinSelection()
{
    m_pinnedSelection = m_selectionModel->selection();
}

void FolderModel::unpinSelection()
{
    m_pinnedSelection = QItemSelection();
}

void FolderModel::addItemDragImage(int row, int x, int y, int width, int height, const QVariant &image)
{
    if (row < 0) {
        return;
    }

867
    delete m_dragImages.take(row);
868

869
    DragImage *dragImage = new DragImage();
870
871
872
873
874
875
876
877
878
879
    dragImage->row = row;
    dragImage->rect = QRect(x, y, width, height);
    dragImage->image = image.value<QImage>();
    dragImage->blank = false;

    m_dragImages.insert(row, dragImage);
}

void FolderModel::clearDragImages()
{
880
881
    qDeleteAll(m_dragImages);
    m_dragImages.clear();
882
883
884
885
886
887
888
889
890
891
}

void FolderModel::setDragHotSpotScrollOffset(int x, int y)
{
    m_dragHotSpotScrollOffset.setX(x);
    m_dragHotSpotScrollOffset.setY(y);
}

QPoint FolderModel::dragCursorOffset(int row)
{
Laurent Montel's avatar
Laurent Montel committed
892
893
    DragImage *image = m_dragImages.value(row);
    if (!image) {
894
        return QPoint(0, 0);
895
896
    }

Laurent Montel's avatar
Laurent Montel committed
897
    return image->cursorOffset;
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
}

void FolderModel::addDragImage(QDrag *drag, int x, int y)
{
    if (!drag || m_dragImages.isEmpty()) {
        return;
    }

    QRegion region;

    foreach (DragImage *image, m_dragImages) {
        image->blank = isBlank(image->row);
        image->rect.translate(-m_dragHotSpotScrollOffset.x(), -m_dragHotSpotScrollOffset.y());
        if (!image->blank && !image->image.isNull()) {
            region = region.united(image->rect);
        }
    }

    QRect rect = region.boundingRect();
    QPoint offset = rect.topLeft();
    rect.translate(-offset.x(), -offset.y());

    QImage dragImage(rect.size(), QImage::Format_RGBA8888);
    dragImage.fill(Qt::transparent);

    QPainter painter(&dragImage);

    QPoint pos;

    foreach (DragImage *image, m_dragImages) {
        if (!image->blank && !image->image.isNull()) {
            pos = image->rect.translated(-offset.x(), -offset.y()).topLeft();
Eike Hein's avatar
Eike Hein committed
930
931
            image->cursorOffset.setX(pos.x() - (x - offset.x()));
            image->cursorOffset.setY(pos.y() - (y - offset.y()));
932
933
934
935
936
937
938
939
940
941
942
943
944

            painter.drawImage(pos, image->image);
        }

        // FIXME HACK: Operate on copy.
        image->rect.translate(m_dragHotSpotScrollOffset.x(), m_dragHotSpotScrollOffset.y());
    }

    drag->setPixmap(QPixmap::fromImage(dragImage));
    drag->setHotSpot(QPoint(x - offset.x(), y - offset.y()));
}

void FolderModel::dragSelected(int x, int y)
945
{
Eike Hein's avatar
Eike Hein committed
946
947
948
949
950
    if (m_dragInProgress) {
        return;
    }

    m_dragInProgress = true;
Laurent Montel's avatar
Laurent Montel committed
951
    Q_EMIT draggingChanged();
952
    m_urlChangedWhileDragging = false;
Eike Hein's avatar
Eike Hein committed
953

954
955
956
    // Avoid starting a drag synchronously in a mouse handler or interferes with
    // child event filtering in parent items (and thus e.g. press-and-hold hand-
    // ling in a containment).
Alexander Lohnau's avatar
Alexander Lohnau committed
957
    QMetaObject::invokeMethod(this, "dragSelectedInternal", Qt::QueuedConnection, Q_ARG(int, x), Q_ARG(int, y));
958
959
}

Bhushan Shah's avatar
Bhushan Shah committed
960
void FolderModel::dragSelectedInternal(int x, int y)
961
962
{
    if (!m_viewAdapter || !m_selectionModel->hasSelection()) {
Eike Hein's avatar
Eike Hein committed
963
        m_dragInProgress = false;
Laurent Montel's avatar
Laurent Montel committed
964
        Q_EMIT draggingChanged();
965
966
967
968
969
970
971
972
973
974
975
976
        return;
    }

    ItemViewAdapter *adapter = qobject_cast<ItemViewAdapter *>(m_viewAdapter);
    QQuickItem *item = qobject_cast<QQuickItem *>(adapter->adapterView());

    QDrag *drag = new QDrag(item);

    addDragImage(drag, x, y);

    m_dragIndexes = m_selectionModel->selectedIndexes();

Laurent Montel's avatar
Laurent Montel committed
977
    std::sort(m_dragIndexes.begin(), m_dragIndexes.end());
978

Laurent Montel's avatar
Laurent Montel committed
979
980
    // TODO: Optimize to Q_EMIT contiguous groups.
    Q_EMIT dataChanged(m_dragIndexes.first(), m_dragIndexes.last(), QVector<int>() << BlankRole);
981
982

    QModelIndexList sourceDragIndexes;
Laurent Montel's avatar
Laurent Montel committed
983
    sourceDragIndexes.reserve(m_dragIndexes.count());
984
985
986
987
988
989
    foreach (const QModelIndex &index, m_dragIndexes) {
        sourceDragIndexes.append(mapToSource(index));
    }

    drag->setMimeData(m_dirModel->mimeData(sourceDragIndexes));

990
991
    // Due to spring-loading (aka auto-expand), the URL might change
    // while the drag is in-flight - in that case we don't want to
Laurent Montel's avatar
Laurent Montel committed
992
    // unnecessarily Q_EMIT dataChanged() for (possibly invalid) indices
993
994
995
    // after it ends.
    const QUrl currentUrl(m_dirModel->dirLister()->url());

996
997
    item->grabMouse();
    drag->exec(supportedDragActions());
998

999
1000
    item->ungrabMouse();

1001
    m_dragInProgress = false;
Laurent Montel's avatar
Laurent Montel committed
1002
    Q_EMIT draggingChanged();
1003
1004
1005
1006
1007
1008
    m_urlChangedWhileDragging = false;

    if (m_dirModel->dirLister()->url() == currentUrl) {
        const QModelIndex first(m_dragIndexes.first());
        const QModelIndex last(m_dragIndexes.last());
        m_dragIndexes.clear();
Laurent Montel's avatar
Laurent Montel committed
1009
1010
        // TODO: Optimize to Q_EMIT contiguous groups.
        Q_EMIT dataChanged(first, last, QVector<int>() << BlankRole);
1011
    }
1012
1013
}

1014
1015
1016
static bool isDropBetweenSharedViews(const QList<QUrl> &urls, const QUrl &folderUrl)
{
    for (const auto &url : urls) {
Alexander Lohnau's avatar
Alexander Lohnau committed
1017
        if (folderUrl.adjusted(QUrl::StripTrailingSlash) != url.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash)) {
1018
1019
1020
1021
1022
1023
            return false;
        }
    }
    return true;
}

Ahmad Samir's avatar
Ahmad Samir committed
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
static const char *s_ark_dndextract_service = "application/x-kde-ark-dndextract-service";
static const char *s_ark_dndextract_path = "application/x-kde-ark-dndextract-path";

static QString arkDbusServiceName(const QMimeData *mimeData)
{
    return QString::fromUtf8(mimeData->data(QString::fromLatin1(s_ark_dndextract_service)));
}

static QString arkDbusPath(const QMimeData *mimeData)
{
    return QString::fromUtf8(mimeData->data(QString::fromLatin1(s_ark_dndextract_path)));
}

static bool isMimeDataArkDnd(const QMimeData *mimeData)
{
    return mimeData->hasFormat(QString::fromLatin1(s_ark_dndextract_service)) //
        && mimeData->hasFormat(QString::fromLatin1(s_ark_dndextract_path));
}

Alexander Lohnau's avatar
Alexander Lohnau committed
1043
void FolderModel::drop(QQuickItem *target, QObject *dropEvent, int row, bool showMenuManually)
1044
{
1045
    QMimeData *mimeData = qobject_cast<QMimeData *>(dropEvent->property("mimeData").value<QObject *>());
1046
1047
1048
1049
1050

    if (!mimeData) {
        return;
    }

Eike Hein's avatar
Eike Hein committed
1051
    QModelIndex idx;
1052
1053
1054
    KFileItem item;

    if (row > -1 && row < rowCount()) {
Alexander Lohnau's avatar
Alexander Lohnau committed
1055
1056
        idx = index(row, 0);
        item = itemForIndex(idx);
1057
1058
    }

1059
1060
    QUrl dropTargetUrl;

Kai Uwe Broulik's avatar
Kai Uwe Broulik committed
1061
1062
    // So we get to run mostLocalUrl() over the current URL.
    if (item.isNull()) {
1063
        item = rootItem();
Kai Uwe Broulik's avatar
Kai Uwe Broulik committed
1064
1065
    }

1066
1067
1068
1069
1070
    if (item.isNull()) {
        dropTargetUrl = m_dirModel->dirLister()->url();
    } else if (m_parseDesktopFiles && item.isDesktopFile()) {
        const KDesktopFile file(item.targetUrl().path());

Laurent Montel's avatar
Laurent Montel committed
1071
        if (file.hasLinkType()) {
1072
1073
1074
1075
1076
1077
1078
1079
            dropTargetUrl = QUrl(file.readUrl());
        } else {
            dropTargetUrl = item.mostLocalUrl();
        }
    } else {
        dropTargetUrl = item.mostLocalUrl();
    }

1080
    auto dropTargetFolderUrl = dropTargetUrl;
1081
    if (dropTargetFolderUrl.fileName() == QLatin1Char('.')) {
1082
1083
1084
1085
1086
1087
1088
1089
1090
        // the target URL for desktop:/ is e.g. 'file://home/user/Desktop/.'
        dropTargetFolderUrl = dropTargetFolderUrl.adjusted(QUrl::RemoveFilename);
    }

    // use dropTargetUrl to resolve desktop:/ to the actual file location which is also used by the mime data
    /* QMimeData operates on local URLs, but the dir lister and thus screen mapper and positioner may
     * use a fancy scheme like desktop:/ instead. Ensure we always use the latter to properly map URLs,
     * i.e. go from file:///home/user/Desktop/file to desktop:/file
     */
1091
    auto mappableUrl = [this, dropTargetFolderUrl](const QUrl &url) -> QUrl {
1092
        if (dropTargetFolderUrl != m_dirModel->dirLister()->url()) {
1093
            QString mappedUrl = url.toString();
1094
1095
1096
1097
1098
            const auto local = dropTargetFolderUrl.toString();
            const auto internal = m_dirModel->dirLister()->url().toString();
            if (mappedUrl.startsWith(local)) {
                mappedUrl.replace(0, local.size(), internal);
            }
1099
            return ScreenMapper::stringToUrl(mappedUrl);
1100
        }
1101
        return url;
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
    };

    const int x = dropEvent->property("x").toInt();
    const int y = dropEvent->property("y").toInt();
    const QPoint dropPos = {x, y};

    if (m_dragInProgress && row == -1 && !m_urlChangedWhileDragging) {
        if (m_locked || mimeData->urls().isEmpty()) {
            return;
        }

        setSortMode(-1);

        for (const auto &url : mimeData->urls()) {
            m_dropTargetPositions.insert(url.fileName(), dropPos);
            m_screenMapper->addMapping(mappableUrl(url), m_screen, ScreenMapper::DelayedSignal);
            m_screenMapper->removeItemFromDisabledScreen(mappableUrl(url));
        }
Laurent Montel's avatar
Laurent Montel committed
1120
        Q_EMIT move(x, y, mimeData->urls());
1121
1122
1123
1124

        return;
    }

Ahmad Samir's avatar
Ahmad Samir committed
1125
1126
1127
    if (isMimeDataArkDnd(mimeData)) {
        QDBusMessage message = QDBusMessage::createMethodCall(arkDbusServiceName(mimeData),
                                                              arkDbusPath(mimeData),
Alexander Lohnau's avatar
Alexander Lohnau committed
1128
1129
                                                              QStringLiteral("org.kde.ark.DndExtract"),
                                                              QStringLiteral("extractSelectedFilesTo"));
1130
        message.setArguments({dropTargetUrl.toDisplayString(QUrl::PreferLocalFile)});
1131

1132
        QDBusConnection::sessionBus().call(message, QDBus::NoBlock);
1133
1134
1135
1136

        return;
    }

Eike Hein's avatar
Eike Hein committed
1137
    if (idx.isValid() && !(flags(idx) & Qt::ItemIsDropEnabled)) {
1138
1139
1140
        return;
    }

1141
1142
1143
1144
1145
    // Catch drops from a Task Manager and convert to usable URL.
    if (!mimeData->hasUrls() && mimeData->hasFormat(QStringLiteral("text/x-orgkdeplasmataskmanager_taskurl"))) {
        QList<QUrl> urls = {QUrl(QString::fromUtf8(mimeData->data(QStringLiteral("text/x-orgkdeplasmataskmanager_taskurl"))))};
        mimeData->setUrls(urls);
    }
1146

1147
    if (m_usedByContainment && !m_screenMapper->sharedDesktops()) {
1148
1149
        if (isDropBetweenSharedViews(mimeData->urls(), dropTargetFolderUrl)) {
            setSortMode(-1);
1150
1151
            const QList<QUrl> urls = mimeData->urls();
            for (const auto &url : urls) {
1152
1153
1154
                m_dropTargetPositions.insert(url.fileName(), dropPos);
                m_screenMapper->addMapping(mappableUrl(url), m_screen, ScreenMapper::DelayedSignal);
                m_screenMapper->removeItemFromDisabledScreen(mappableUrl(url));
1155
1156
1157
1158
1159
1160
            }
            m_dropTargetPositionsCleanup->start();
            return;
        }
    }

1161
1162
1163
1164
1165
    Qt::DropAction proposedAction((Qt::DropAction)dropEvent->property("proposedAction").toInt());
    Qt::DropActions possibleActions(dropEvent->property("possibleActions").toInt());
    Qt::MouseButtons buttons(dropEvent->property("buttons").toInt());
    Qt::KeyboardModifiers modifiers(dropEvent->property("modifiers").toInt());