foldermodel.cpp 65.6 KB
Newer Older
1
/***************************************************************************
2
 *   Copyright (C) 2006 David Faure <faure@kde.org>                        *
3
 *   Copyright (C) 2008 Fredrik Höglund <fredrik@kde.org>                  *
4
 *   Copyright (C) 2008 Rafael Fernández López <ereslibre@kde.org>           *
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
 *   Copyright (C) 2011 Marco Martin <mart@kde.org>                        *
 *   Copyright (C) 2014 by Eike Hein <hein@kde.org>                        *
 *                                                                         *
 *   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 "foldermodel.h"
25
26
#include "itemviewadapter.h"
#include "positioner.h"
Andras Mantia's avatar
Andras Mantia committed
27
#include "screenmapper.h"
28
29
30
31

#include <QApplication>
#include <QClipboard>
#include <QCollator>
32
33
34
#include <QDesktopWidget>
#include <QDrag>
#include <QImage>
35
#include <QItemSelectionModel>
36
#include <QMenu>
37
#include <QMimeData>
38
#include <QMimeDatabase>
39
40
41
42
#include <QPainter>
#include <QPixmap>
#include <QQuickItem>
#include <QQuickWindow>
43
44
#include <QTimer>
#include <QLoggingCategory>
45
46
#include <qplatformdefs.h>

47
#include <KDirWatch>
48
#include <KIO/DropJob>
49
50
#include <KAuthorized>
#include <KConfigGroup>
51
#include <KFileCopyToMenu>
52
53
#include <KFileItemActions>
#include <KFileItemListProperties>
54
#include <KNewFileMenu>
55
#include <KIO/DeleteJob>
56
#include <KIO/EmptyTrashJob>
57
#include <KIO/FileUndoManager>
58
#include <KIO/JobUiDelegate>
59
#include <KIO/Paste>
60
#include <KIO/PasteJob>
61
#include <KIO/RestoreJob>
62
#include <KLocalizedString>
63
#include <KPropertiesDialog>
64
#include <KSharedConfig>
65
#include <KShell>
66

67
68
#include <KCoreDirLister>
#include <KDirLister>
69
70
#include <KDesktopFile>
#include <KDirModel>
71
#include <KIO/CopyJob>
72
#include <KIO/Job>
73
#include <KIO/PreviewJob>
74
#include <KProtocolInfo>
75
76
#include <KRun>

Andras Mantia's avatar
Andras Mantia committed
77
78
79
80
#include <Plasma/Applet>
#include <Plasma/Containment>
#include <Plasma/Corona>

81
82
83
84
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>

85
86
Q_LOGGING_CATEGORY(FOLDERMODEL, "plasma.containments.desktop.folder.foldermodel")

87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
DirLister::DirLister(QObject *parent) : KDirLister(parent)
{
}

DirLister:: ~DirLister()
{
}

void DirLister::handleError(KIO::Job *job)
{
    if (!autoErrorHandlingEnabled()) {
        emit error(job->errorString());
        return;
    }

    KDirLister::handleError(job);
}

FolderModel::FolderModel(QObject *parent) : QSortFilterProxyModel(parent),
106
    m_dirWatch(nullptr),
107
    m_dragInProgress(false),
108
    m_urlChangedWhileDragging(false),
109
    m_dropTargetPositionsCleanup(new QTimer(this)),
110
111
    m_previewGenerator(nullptr),
    m_viewAdapter(nullptr),
112
    m_actionCollection(this),
113
114
    m_newMenu(nullptr),
    m_fileItemActions(nullptr),
115
    m_usedByContainment(false),
116
    m_locked(true),
Eike Hein's avatar
Eike Hein committed
117
    m_sortMode(0),
118
119
120
121
122
    m_sortDesc(false),
    m_sortDirsFirst(true),
    m_parseDesktopFiles(false),
    m_previews(false),
    m_filterMode(NoFilter),
123
    m_filterPatternMatchAll(true),
Aleix Pol Gonzalez's avatar
Aleix Pol Gonzalez committed
124
    m_screenUsed(false),
125
    m_screenMapper(ScreenMapper::instance()),
Aleix Pol Gonzalez's avatar
Aleix Pol Gonzalez committed
126
    m_complete(false)
127
{
128
129
    //needed to pass the job around with qml
    qmlRegisterType<KIO::DropJob>();
130
131
    DirLister *dirLister = new DirLister(this);
    dirLister->setDelayedMimeTypes(true);
132
    dirLister->setAutoErrorHandlingEnabled(false, nullptr);
133
134
    connect(dirLister, &DirLister::error, this, &FolderModel::dirListFailed);
    connect(dirLister, &KCoreDirLister::itemsDeleted, this, &FolderModel::evictFromIsDirCache);
135
136
137

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

138
    void (KCoreDirLister::*myCompletedSignal)() = &KCoreDirLister::completed;
139
140
141
142
143
    QObject::connect(dirLister, myCompletedSignal, this, [this] {
        setStatus(Status::Ready);
        emit listingCompleted();
    });

144
    void (KCoreDirLister::*myCanceledSignal)() = &KCoreDirLister::canceled;
145
146
147
148
    QObject::connect(dirLister, myCanceledSignal, this, [this] {
        setStatus(Status::Canceled);
        emit listingCanceled();
    });
149
150
151

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

154
155
156
157
158
159
160
161
162
    // If we have dropped items queued for moving, go unsorted now.
    connect(this, &QAbstractItemModel::rowsAboutToBeInserted,
            this, [this]() {
            if (!m_dropTargetPositions.isEmpty()) {
                setSortMode(-1);
            }
    });

    // Position dropped items at the desired target position.
163
    connect(this, &QAbstractItemModel::rowsInserted,
164
165
            this, [this](const QModelIndex &parent, int first, int last) {
        for (int i = first; i <= last; ++i) {
166
167
            const auto idx = index(i, 0, parent);
            const auto url = itemForIndex(idx).url();
168
169
170
171
172
173
174
            auto it = m_dropTargetPositions.find(url.fileName());
            if (it != m_dropTargetPositions.end()) {
                const auto pos = it.value();
                m_dropTargetPositions.erase(it);
                emit move(pos.x(), pos.y(), {url});
            }
        }
175
    });
176

177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
    /*
     * 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();
        }
    });

196
    m_selectionModel = new QItemSelectionModel(this, this);
197
198
    connect(m_selectionModel, &QItemSelectionModel::selectionChanged,
            this, &FolderModel::selectionChanged);
199
200
201
202
203
204
205

    setSourceModel(m_dirModel);

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

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

208
209
210
211
212
    createActions();
}

FolderModel::~FolderModel()
{
213
    if (m_usedByContainment) {
Andras Mantia's avatar
Andras Mantia committed
214
215
216
        // disconnect so we don't handle signals from the screen mapper when
        // removeScreen is called
        m_screenMapper->disconnect(this);
217
        m_screenMapper->removeScreen(m_screen, resolvedUrl());
Andras Mantia's avatar
Andras Mantia committed
218
    }
219
220
221
222
}

QHash< int, QByteArray > FolderModel::roleNames() const
{
223
224
225
226
227
228
    return staticRoleNames();
}

QHash< int, QByteArray > FolderModel::staticRoleNames()
{
    QHash<int, QByteArray> roleNames;
229
230
    roleNames[Qt::DisplayRole] = "display";
    roleNames[Qt::DecorationRole] = "decoration";
231
    roleNames[BlankRole] = "blank";
232
    roleNames[OverlaysRole] = "overlays";
233
234
    roleNames[SelectedRole] = "selected";
    roleNames[IsDirRole] = "isDir";
235
    roleNames[IsLinkRole] = "isLink";
236
    roleNames[IsHiddenRole] = "isHidden";
237
    roleNames[UrlRole] = "url";
238
    roleNames[LinkDestinationUrl] = "linkDestinationUrl";
239
240
    roleNames[SizeRole] = "size";
    roleNames[TypeRole] = "type";
241
242
243
244

    return roleNames;
}

245
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
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();
}

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

283
284
QString FolderModel::url() const
{
285
    return m_url;
286
287
}

288
void FolderModel::setUrl(const QString& url)
289
{
290
    const QUrl &resolvedNewUrl = resolve(url);
291

292
    if (url == m_url) {
293
        m_dirModel->dirLister()->updateDirectory(resolvedNewUrl);
294
295
296
        return;
    }

297
    const auto oldUrl = resolvedUrl();
Andras Mantia's avatar
Andras Mantia committed
298

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

    emit urlChanged();
308
    emit resolvedUrlChanged();
309
310
311

    m_errorString.clear();
    emit errorStringChanged();
312
313
314

    if (m_dirWatch) {
        delete m_dirWatch;
315
        m_dirWatch = nullptr;
316
317
    }

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

325
326
327
328
    if (m_dragInProgress) {
        m_urlChangedWhileDragging = true;
    }

329
    emit iconNameChanged();
Andras Mantia's avatar
Andras Mantia committed
330

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

337
338
339
340
341
QUrl FolderModel::resolvedUrl() const
{
    return m_dirModel->dirLister()->url();
}

342
343
344
345
QUrl FolderModel::resolve(const QString& url)
{
    QUrl resolvedUrl;

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

    return resolvedUrl;
}

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

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

    return rootItem.iconName();
}

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

void FolderModel::setStatus(Status status)
{
    if (m_status != status) {
        m_status = status;
        emit statusChanged();
    }
}

379
380
381
382
383
QString FolderModel::errorString() const
{
    return m_errorString;
}

384
385
386
387
388
bool FolderModel::dragging() const
{
    return m_dragInProgress;
}

389
390
391
392
393
394
395
396
397
398
bool FolderModel::usedByContainment() const
{
    return m_usedByContainment;
}

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

399
        QAction *action = m_actionCollection.action(QStringLiteral("refresh"));
400
401
402

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

406
407
408
409
        m_screenMapper->disconnect(this);
        connect(m_screenMapper, &ScreenMapper::screensChanged, this, &FolderModel::invalidateFilterIfComplete);
        connect(m_screenMapper, &ScreenMapper::screenMappingChanged, this, &FolderModel::invalidateFilterIfComplete);

410
411
412
413
        emit usedByContainmentChanged();
    }
}

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

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

        emit lockedChanged();
    }
}

428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
void FolderModel::dirListFailed(const QString& error)
{
    m_errorString = error;
    emit errorStringChanged();
}

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
444
        if (mode == -1 /* Unsorted */) {
445
446
            setDynamicSortFilter(false);
        } else {
447
            invalidateIfComplete();
Eike Hein's avatar
Eike Hein committed
448
449
            sort(m_sortMode, m_sortDesc ? Qt::DescendingOrder : Qt::AscendingOrder);
            setDynamicSortFilter(true);
450
        }
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465

        emit sortModeChanged();
    }
}

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
466
        if (m_sortMode != -1 /* Unsorted */) {
467
            invalidateIfComplete();
Eike Hein's avatar
Eike Hein committed
468
            sort(m_sortMode, m_sortDesc ? Qt::DescendingOrder : Qt::AscendingOrder);
469
        }
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484

        emit sortDescChanged();
    }
}

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
485
        if (m_sortMode != -1 /* Unsorted */) {
486
            invalidateIfComplete();
Eike Hein's avatar
Eike Hein committed
487
            sort(m_sortMode, m_sortDesc ? Qt::DescendingOrder : Qt::AscendingOrder);
488
        }
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521

        emit sortDirsFirstChanged();
    }
}

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

void FolderModel::setParseDesktopFiles(bool enable)
{
    if (m_parseDesktopFiles != enable) {
        m_parseDesktopFiles = enable;
        emit parseDesktopFilesChanged();
    }
}

QObject* FolderModel::viewAdapter() const
{
    return m_viewAdapter;
}

void FolderModel::setViewAdapter(QObject* adapter)
{
    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);
522
            m_previewGenerator->setEnabledPlugins(m_effectivePreviewPlugins);
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
        }

        emit viewAdapterChanged();
    }
}

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);
        }

        emit previewsChanged();
    }
}

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

void FolderModel::setPreviewPlugins(const QStringList& previewPlugins)
{
554
555
556
557
558
559
560
    QStringList effectivePlugins = previewPlugins;
    if (effectivePlugins.isEmpty()) {
        effectivePlugins = KIO::PreviewJob::defaultPlugins();
    }

    if (m_effectivePreviewPlugins != effectivePlugins) {
        m_effectivePreviewPlugins = effectivePlugins;
561
562
563

        if (m_previewGenerator) {
            m_previewGenerator->setPreviewShown(false);
564
            m_previewGenerator->setEnabledPlugins(m_effectivePreviewPlugins);
565
566
            m_previewGenerator->setPreviewShown(true);
        }
567
    }
568

569
570
    if (m_previewPlugins != previewPlugins) {
        m_previewPlugins = previewPlugins;
571
572
573
574
575
576
577
578
579
580
581
582
583
584
        emit previewPluginsChanged();
    }
}

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

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

585
        invalidateFilterIfComplete();
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602

        emit filterModeChanged();
    }
}

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

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

    m_filterPattern = pattern;
603
    m_filterPatternMatchAll = (pattern == QLatin1String("*"));
604

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

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

616
    invalidateFilterIfComplete();
617

618
619
620
621
622
    emit filterPatternChanged();
}

QStringList FolderModel::filterMimeTypes() const
{
623
    return m_mimeSet.values();
624
625
626
627
}

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

    if (m_mimeSet != set) {

        m_mimeSet = set;

634
        invalidateFilterIfComplete();
635
636
637
638
639

        emit filterMimeTypesChanged();
    }
}

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

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

    m_screen = screen;
648
    if (m_usedByContainment && !m_screenMapper->sharedDesktops()) {
649
        m_screenMapper->addScreen(screen, resolvedUrl());
Andras Mantia's avatar
Andras Mantia committed
650
651
652
653
    }
    emit screenChanged();
}

654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
bool FolderModel::eventFilter(QObject *watched, QEvent *event)
{
    Q_UNUSED(watched)

    // Catching Shift modifier usage on open context menus to swap the
    // Trash/Delete actions.
    if (event->type() == QEvent::KeyPress) {
        QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);

        if (keyEvent->key() == Qt::Key_Shift) {
            m_actionCollection.action(QStringLiteral("trash"))->setVisible(false);
            m_actionCollection.action(QStringLiteral("del"))->setVisible(true);
        }
    } else if (event->type() == QEvent::KeyRelease) {
        QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);

        if (keyEvent->key() == Qt::Key_Shift) {
            m_actionCollection.action(QStringLiteral("trash"))->setVisible(true);
            m_actionCollection.action(QStringLiteral("del"))->setVisible(false);
        }
    }

    return false;
}

679
680
681
682
683
KFileItem FolderModel::rootItem() const
{
    return m_dirModel->dirLister()->rootItem();
}

684
685
686
687
688
689
690
691
692
void FolderModel::up()
{
    const QUrl &up = KIO::upUrl(resolvedUrl());

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

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

699
700
701
702
703
704
705
    const QModelIndex idx = index(row, 0);
    bool isDir = data(idx, IsDirRole).toBool();

    if (isDir) {
        const KFileItem  item = itemForIndex(idx);
        if (m_parseDesktopFiles && item.isDesktopFile()) {
            const KDesktopFile file(item.targetUrl().path());
Laurent Montel's avatar
Laurent Montel committed
706
            if (file.hasLinkType()) {
707
708
                setUrl(file.readUrl());
            }
709
710
        } else {
            setUrl(item.targetUrl().toString());
711
        }
712
713
    }
}
714

715
716
717
718
719
720
721
void FolderModel::run(int row)
{
    if (row < 0) {
        return;
    }

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

723
724
725
726
    QUrl url(item.targetUrl());

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

730
    KRun *run = new KRun(url, nullptr);
731
732
733
734
735
    // 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")
                                   || item.url().adjusted(QUrl::RemoveFilename).path() != QLatin1String("/"));
736
737
}

738
739
740
741
742
743
void FolderModel::runSelected()
{
    if (!m_selectionModel->hasSelection()) {
        return;
    }

744
745
746
747
748
749
750
    if (m_selectionModel->selectedIndexes().count() == 1) {
        run(m_selectionModel->selectedIndexes().constFirst().row());
        return;
    }

    KFileItemActions fileItemActions(this);
    KFileItemList items;
751

752
    foreach (const QModelIndex &index, m_selectionModel->selectedIndexes()) {
753
        // Skip over directories.
754
755
        if (!index.data(IsDirRole).toBool()) {
            items << itemForIndex(index);
756
        }
757
    }
758
759

    fileItemActions.runPreferredApplications(items, QString());
760
761
}

762
763
void FolderModel::rename(int row, const QString& name)
{
764
765
766
767
    if (row < 0) {
        return;
    }

768
769
770
771
    QModelIndex idx = index(row, 0);
    m_dirModel->setData(mapToSource(idx), name, Qt::EditRole);
}

772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
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
799
bool FolderModel::hasSelection() const
800
801
802
803
{
    return m_selectionModel->hasSelection();
}

804
805
bool FolderModel::isSelected(int row)
{
806
807
808
809
    if (row < 0) {
        return false;
    }

810
811
812
    return m_selectionModel->isSelected(index(row, 0));
}

813
814
void FolderModel::setSelected(int row)
{
815
816
817
818
    if (row < 0) {
        return;
    }

819
820
821
    m_selectionModel->select(index(row, 0), QItemSelectionModel::Select);
}

822
823
824
825
826
827
828
829
830
void FolderModel::toggleSelected(int row)
{
    if (row < 0) {
        return;
    }

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

831
void FolderModel::setRangeSelected(int anchor, int to)
832
{
833
    if (anchor < 0 || to < 0) {
834
835
836
        return;
    }

837
    QItemSelection selection(index(anchor, 0), index(to, 0));
838
839
840
    m_selectionModel->select(selection, QItemSelectionModel::ClearAndSelect);
}

841
void FolderModel::updateSelection(const QVariantList &rows, bool toggle)
842
{
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
    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);
    }
865
866
867
868
}

void FolderModel::clearSelection()
{
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
    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;
    }

890
    delete m_dragImages.take(row);
891

892
    DragImage *dragImage = new DragImage();
893
894
895
896
897
898
899
900
901
902
    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()
{
903
904
    qDeleteAll(m_dragImages);
    m_dragImages.clear();
905
906
907
908
909
910
911
912
913
914
}

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
915
916
    DragImage *image = m_dragImages.value(row);
    if (!image) {
917
        return QPoint(0, 0);
918
919
    }

Laurent Montel's avatar
Laurent Montel committed
920
    return image->cursorOffset;
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
}

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
953
954
            image->cursorOffset.setX(pos.x() - (x - offset.x()));
            image->cursorOffset.setY(pos.y() - (y - offset.y()));
955
956
957
958
959
960
961
962
963
964
965
966
967

            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)
968
{
Eike Hein's avatar
Eike Hein committed
969
970
971
972
973
    if (m_dragInProgress) {
        return;
    }

    m_dragInProgress = true;
974
975
    emit draggingChanged();
    m_urlChangedWhileDragging = false;
Eike Hein's avatar
Eike Hein committed
976

977
978
979
980
981
982
983
984
    // 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).
    QMetaObject::invokeMethod(this, "dragSelectedInternal", Qt::QueuedConnection,
        Q_ARG(int, x),
        Q_ARG(int, y));
}

Bhushan Shah's avatar
Bhushan Shah committed
985
void FolderModel::dragSelectedInternal(int x, int y)
986
987
{
    if (!m_viewAdapter || !m_selectionModel->hasSelection()) {
Eike Hein's avatar
Eike Hein committed
988
        m_dragInProgress = false;
989
        emit draggingChanged();
990
991
992
993
994
995
996
997
998
999
1000
1001
        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
1002
    std::sort(m_dragIndexes.begin(), m_dragIndexes.end());
1003
1004
1005
1006
1007

    // TODO: Optimize to emit contiguous groups.
    emit dataChanged(m_dragIndexes.first(), m_dragIndexes.last(), QVector<int>() << BlankRole);

    QModelIndexList sourceDragIndexes;
Laurent Montel's avatar
Laurent Montel committed
1008
    sourceDragIndexes.reserve(m_dragIndexes.count());
1009
1010
1011
1012
1013
1014
    foreach (const QModelIndex &index, m_dragIndexes) {
        sourceDragIndexes.append(mapToSource(index));
    }

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

1015
1016
1017
1018
1019
1020
    // 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
    // unnecessarily emit dataChanged() for (possibly invalid) indices
    // after it ends.
    const QUrl currentUrl(m_dirModel->dirLister()->url());

1021
1022
    item->grabMouse();
    drag->exec(supportedDragActions());
1023

1024
1025
    item->ungrabMouse();

1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
    m_dragInProgress = false;
    emit draggingChanged();
    m_urlChangedWhileDragging = false;

    if (m_dirModel->dirLister()->url() == currentUrl) {
        const QModelIndex first(m_dragIndexes.first());
        const QModelIndex last(m_dragIndexes.last());
        m_dragIndexes.clear();
        // TODO: Optimize to emit contiguous groups.
        emit dataChanged(first, last, QVector<int>() << BlankRole);
    }
1037
1038
}

1039
1040
1041
static bool isDropBetweenSharedViews(const QList<QUrl> &urls, const QUrl &folderUrl)
{
    for (const auto &url : urls) {
1042
1043
        if (folderUrl.adjusted(QUrl::StripTrailingSlash)
               != url.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash)) {
1044
1045
1046
1047
1048
1049
            return false;
        }
    }
    return true;
}

1050
void FolderModel::drop(QQuickItem *target, QObject* dropEvent, int row, bool showMenuManually)
1051
{
1052
    QMimeData *mimeData = qobject_cast<QMimeData *>(dropEvent->property("mimeData").value<QObject *>());
1053
1054
1055
1056
1057

    if (!mimeData) {
        return;
    }

Eike Hein's avatar
Eike Hein committed
1058
    QModelIndex idx;
1059
1060
1061
    KFileItem item;

    if (row > -1 && row < rowCount()) {
Eike Hein's avatar
Eike Hein committed
1062
1063
         idx = index(row, 0);
         item = itemForIndex(idx);
1064
1065
    }

1066
1067
    QUrl dropTargetUrl;

Kai Uwe Broulik's avatar
Kai Uwe Broulik committed
1068
1069
    // So we get to run mostLocalUrl() over the current URL.
    if (item.isNull()) {
1070
        item = rootItem();
Kai Uwe Broulik's avatar
Kai Uwe Broulik committed
1071
1072
    }

1073
1074
1075
1076
1077
    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
1078
        if (file.hasLinkType()) {
1079
1080
1081
1082
1083
1084
1085
1086
            dropTargetUrl = QUrl(file.readUrl());
        } else {
            dropTargetUrl = item.mostLocalUrl();
        }
    } else {
        dropTargetUrl = item.mostLocalUrl();
    }

1087
    auto dropTargetFolderUrl = dropTargetUrl;
1088
    if (dropTargetFolderUrl.fileName() == QLatin1Char('.')) {
1089
1090
1091
1092
1093
1094
1095
1096
1097
        // 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
     */
1098
    auto mappableUrl = [this, dropTargetFolderUrl](const QUrl &url) -> QUrl {
1099
        if (dropTargetFolderUrl != m_dirModel->dirLister()->url()) {
1100
            QString mappedUrl = url.toString();
1101
1102
1103
1104
1105
            const auto local = dropTargetFolderUrl.toString();
            const auto internal = m_dirModel->dirLister()->url().toString();
            if (mappedUrl.startsWith(local)) {
                mappedUrl.replace(0, local.size(), internal);
            }
1106
            return ScreenMapper::stringToUrl(mappedUrl);
1107
        }
1108
        return url;
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
    };

    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));
        }
        emit move(x, y, mimeData->urls());

        return;
    }

1132
    if (mimeData->hasFormat(QStringLiteral("application/x-kde-ark-dndextract-service")) &&
1133
1134
1135
        mimeData->hasFormat(QStringLiteral("application/x-kde-ark-dndextract-path"))) {
        const QString remoteDBusClient = mimeData->data(QStringLiteral("application/x-kde-ark-dndextract-service"));
        const QString remoteDBusPath = mimeData->data(QStringLiteral("application/x-kde-ark-dndextract-path"));
1136
1137
1138

        QDBusMessage message =
            QDBusMessage::createMethodCall(remoteDBusClient, remoteDBusPath,
1139
1140
                                            QStringLiteral("org.kde.ark.DndExtract"),
                                            QStringLiteral("extractSelectedFilesTo"));
1141
        message.setArguments({dropTargetUrl.toDisplayString(QUrl::PreferLocalFile)});
1142

1143
        QDBusConnection::sessionBus().call(message, QDBus::NoBlock);
1144
1145
1146
1147

        return;
    }

Eike Hein's avatar
Eike Hein committed
1148
    if (idx.isValid() && !(flags(idx) & Qt::ItemIsDropEnabled)) {
1149
1150
1151
        return;
    }

1152
1153
1154
1155
1156
    // 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);
    }
1157

1158
    if (m_usedByContainment && !m_screenMapper->sharedDesktops()) {
1159
1160
        if (isDropBetweenSharedViews(mimeData->urls(), dropTargetFolderUrl)) {
            setSortMode(-1);
1161
1162
1163
1164
            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));
1165
1166
1167
1168
1169
1170
            }
            m_dropTargetPositionsCleanup->start();
            return;
        }
    }

1171
1172
1173
1174
1175
    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());

1176
1177
    auto pos = target->mapToScene(dropPos).toPoint();
    pos = target->window()->mapToGlobal(pos);
1178
1179
1180
    QDropEvent ev(pos, possibleActions, mimeData, buttons, modifiers);
    ev.setDropAction(proposedAction);

1181
1182
    KIO::DropJobFlag flag = showMenuManually? KIO::ShowMenuManually : KIO::DropJobDefaultFlags;
    KIO::DropJob *dropJob = KIO::drop(&ev, dropTargetUrl, flag);
1183
    dropJob->uiDelegate()->setAutoErrorHandlingEnabled(true);
1184

1185
1186
1187
1188
1189
1190
1191
    // The QMimeData we extract from the DropArea's drop event is deleted as soon as this method
    // ends but we need to keep a copy for when popupMenuAboutToShow fires.
    QMimeData *mimeCopy = new QMimeData();
    for (const QString &format : mimeData->formats()) {
        mimeCopy->setData(format, mimeData->data(format));
    }

1192
    connect(dropJob, &KIO::DropJob::popupMenuAboutToShow, this, [this, mimeCopy, x, y, dropJob](const KFileItemListProperties &) {
1193
1194
        emit popupMenuAboutToShow(dropJob, mimeCopy, x, y);
        mimeCopy->deleteLater();
1195
    });
1196
1197
1198
1199
1200
1201
1202
1203

    /*
     * Position files that come from a drag'n'drop event at the drop event
     * target position. To do so, we first listen to copy job to figure out
     * the target URL. Then we store the position of this drop event in the
     * hash and eventually trigger a move request when we get notified about
     * the new file event from the source model.
     */
1204
1205
    connect(dropJob, &KIO::DropJob::copyJobStarted, this, [this, dropPos, dropTargetUrl](KIO::CopyJob* copyJob) {
        auto map = [this, dropPos, dropTargetUrl](const QUrl &targetUrl) {
1206
1207
            m_dropTargetPositions.insert(targetUrl.fileName(), dropPos);
            m_dropTargetPositionsCleanup->start();
1208

1209
            if (m_usedByContainment && !m_screenMapper->sharedDesktops()) {
1210
1211
                // assign a screen for the item before the copy is actually done, so
                // filterAcceptsRow doesn't assign the default screen to it
1212
                QUrl url = resolvedUrl();
1213
1214
                // if the folderview's folder is a standard path, just use the targetUrl for mapping
                if (targetUrl.toString().startsWith(url.toString())) {
1215
                    m_screenMapper->addMapping(targetUrl, m_screen, ScreenMapper::DelayedSignal);
1216
1217
1218
1219
1220
1221
1222
                } else if (targetUrl.toString().startsWith(dropTargetUrl.toString())) {
                    // if the folderview's folder is a special path, like desktop:// , we need to convert
                    // the targetUrl file:// path to a desktop:/ path for mapping
                    auto destPath = dropTargetUrl.path();
                    auto filePath = targetUrl.path();
                    if (filePath.startsWith(destPath)) {
                        url.setPath(filePath.remove(0, destPath.length()));
1223
                        m_screenMapper->addMapping(url, m_screen, ScreenMapper::DelayedSignal);
1224
1225
1226
                    }
                }
            }
1227
1228
1229
        };
        // remember drop target position for target URL and forget about the source URL
        connect(copyJob, &KIO::CopyJob::copyingDone,
David Edmundson's avatar
David Edmundson committed
1230
                this, [ map](KIO::Job *, const QUrl &, const QUrl &targetUrl, const QDateTime &, bool, bool) {
1231
1232
1233
            map(targetUrl);
        });
        connect(copyJob, &KIO::CopyJob::copyingLinkDone,
David Edmundson's avatar
David Edmundson committed
1234
                this, [ map](KIO::Job *, const QUrl &, const QString &, const QUrl &targetUrl) {
1235
1236
1237
            map(targetUrl);
        });
    });
1238
1239
}

1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
void FolderModel::dropCwd(QObject* dropEvent)
{
    QMimeData *mimeData = qobject_cast<QMimeData *>(dropEvent->property("mimeData").value<QObject *>());

    if (!mimeData) {
        return;
    }

    if (mimeData->hasFormat(QStringLiteral("application/x-kde-ark-dndextract-service")) &&
        mimeData->hasFormat(QStringLiteral("application/x-kde-ark-dndextract-path"))) {
        const QString remoteDBusClient = mimeData->data(QStringLiteral("application/x-kde-ark-dndextract-service"));
        const QString remoteDBusPath = mimeData->data(QStringLiteral("application/x-kde-ark-dndextract-path"));

        QDBusMessage message =
            QDBusMessage::createMethodCall(remoteDBusClient, remoteDBusPath,
                                            QStringLiteral("org.kde.ark.DndExtract"),
                                            QStringLiteral("extractSelectedFilesTo"));
        message.setArguments(QVariantList() << m_dirModel->dirLister()->url().adjusted(QUrl::PreferLocalFile).toString());

1259
        QDBusConnection::sessionBus().call(message, QDBus::NoBlock);
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
    } else {
        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());

        QDropEvent ev(QPoint(), possibleActions, mimeData, buttons, modifiers);
        ev.setDropAction(proposedAction);

        KIO::DropJob *dropJob = KIO::drop(&ev, m_dirModel->dirLister()->url().adjusted(QUrl::PreferLocalFile));
1270
        dropJob->uiDelegate()->setAutoErrorHandlingEnabled(true);
1271
1272
1273
    }
}

1274
void FolderModel::selectionChanged(const QItemSelection &selected, const QItemSelection &deselected)