1 /*
2     SPDX-FileCopyrightText: 2006 David Faure <faure@kde.org>
3     SPDX-FileCopyrightText: 2008 Fredrik Höglund <fredrik@kde.org>
4     SPDX-FileCopyrightText: 2008 Rafael Fernández López <ereslibre@kde.org>
5     SPDX-FileCopyrightText: 2011 Marco Martin <mart@kde.org>
6     SPDX-FileCopyrightText: 2014 Eike Hein <hein@kde.org>
7 
8     SPDX-License-Identifier: GPL-2.0-or-later
9 */
10 
11 #include "foldermodel.h"
12 #include "itemviewadapter.h"
13 #include "positioner.h"
14 #include "removeaction.h"
15 #include "screenmapper.h"
16 
17 #include <QApplication>
18 #include <QClipboard>
19 #include <QCollator>
20 #include <QDesktopWidget>
21 #include <QDrag>
22 #include <QImage>
23 #include <QItemSelectionModel>
24 #include <QLoggingCategory>
25 #include <QMenu>
26 #include <QMimeData>
27 #include <QMimeDatabase>
28 #include <QPainter>
29 #include <QPixmap>
30 #include <QQuickItem>
31 #include <QQuickWindow>
32 #include <QScreen>
33 #include <QTimer>
34 #include <qplatformdefs.h>
35 
36 #include <KAuthorized>
37 #include <KConfigGroup>
38 #include <KDirWatch>
39 #include <KFileCopyToMenu>
40 #include <KFileItemActions>
41 #include <KFileItemListProperties>
42 #include <KIO/DeleteJob>
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 #include <KDesktopFile>
57 #include <KDirModel>
58 #include <KIO/CopyJob>
59 #include <KIO/Job>
60 #include <KIO/PreviewJob>
61 #include <KProtocolInfo>
62 #include <KRun>
63 #include <KStringHandler>
64 
65 #include <Plasma/Applet>
66 #include <Plasma/Containment>
67 #include <Plasma/Corona>
68 
69 #include <sys/stat.h>
70 #include <sys/types.h>
71 #include <unistd.h>
72 
73 Q_LOGGING_CATEGORY(FOLDERMODEL, "plasma.containments.desktop.folder.foldermodel")
74 
DirLister(QObject * parent)75 DirLister::DirLister(QObject *parent)
76     : KDirLister(parent)
77 {
78 }
79 
~DirLister()80 DirLister::~DirLister()
81 {
82 }
83 
handleError(KIO::Job * job)84 void DirLister::handleError(KIO::Job *job)
85 {
86     if (!autoErrorHandlingEnabled()) {
87         Q_EMIT error(job->errorString());
88         return;
89     }
90 
91     KDirLister::handleError(job);
92 }
93 
FolderModel(QObject * parent)94 FolderModel::FolderModel(QObject *parent)
95     : QSortFilterProxyModel(parent)
96     , m_dirWatch(nullptr)
97     , m_dragInProgress(false)
98     , m_urlChangedWhileDragging(false)
99     , m_dropTargetPositionsCleanup(new QTimer(this))
100     , m_previewGenerator(nullptr)
101     , m_viewAdapter(nullptr)
102     , m_actionCollection(this)
103     , m_newMenu(nullptr)
104     , m_fileItemActions(nullptr)
105     , m_usedByContainment(false)
106     , m_locked(true)
107     , m_sortMode(0)
108     , m_sortDesc(false)
109     , m_sortDirsFirst(true)
110     , m_parseDesktopFiles(false)
111     , m_previews(false)
112     , m_filterMode(NoFilter)
113     , m_filterPatternMatchAll(true)
114     , m_screenUsed(false)
115     , m_screenMapper(ScreenMapper::instance())
116     , m_complete(false)
117 {
118     // needed to pass the job around with qml
119     qmlRegisterType<KIO::DropJob>();
120     DirLister *dirLister = new DirLister(this);
121     dirLister->setDelayedMimeTypes(true);
122     dirLister->setAutoErrorHandlingEnabled(false, nullptr);
123     connect(dirLister, &DirLister::error, this, &FolderModel::dirListFailed);
124     connect(dirLister, &KCoreDirLister::itemsDeleted, this, &FolderModel::evictFromIsDirCache);
125 
126     connect(dirLister, &KCoreDirLister::started, this, std::bind(&FolderModel::setStatus, this, Status::Listing));
127 
128     void (KCoreDirLister::*myCompletedSignal)() = &KCoreDirLister::completed;
129     QObject::connect(dirLister, myCompletedSignal, this, [this] {
130         setStatus(Status::Ready);
131         Q_EMIT listingCompleted();
132     });
133 
134     void (KCoreDirLister::*myCanceledSignal)() = &KCoreDirLister::canceled;
135     QObject::connect(dirLister, myCanceledSignal, this, [this] {
136         setStatus(Status::Canceled);
137         Q_EMIT listingCanceled();
138     });
139 
140     m_dirModel = new KDirModel(this);
141     m_dirModel->setDirLister(dirLister);
142     m_dirModel->setDropsAllowed(KDirModel::DropOnDirectory | KDirModel::DropOnLocalExecutable);
143 
144     // If we have dropped items queued for moving, go unsorted now.
145     connect(this, &QAbstractItemModel::rowsAboutToBeInserted, this, [this]() {
146         if (!m_dropTargetPositions.isEmpty()) {
147             setSortMode(-1);
148         }
149     });
150 
151     // Position dropped items at the desired target position.
152     connect(this, &QAbstractItemModel::rowsInserted, this, [this](const QModelIndex &parent, int first, int last) {
153         for (int i = first; i <= last; ++i) {
154             const auto idx = index(i, 0, parent);
155             const auto url = itemForIndex(idx).url();
156             auto it = m_dropTargetPositions.find(url.fileName());
157             if (it != m_dropTargetPositions.end()) {
158                 const auto pos = it.value();
159                 m_dropTargetPositions.erase(it);
160                 Q_EMIT move(pos.x(), pos.y(), {url});
161             }
162         }
163     });
164 
165     /*
166      * Dropped files may not actually show up as new files, e.g. when we overwrite
167      * an existing file. Or files that fail to be listed by the dirLister, or...
168      * To ensure we don't grow the map indefinitely, clean it up periodically.
169      * The cleanup timer is (re)started whenever we modify the map. We use a quite
170      * high interval of 10s. This should ensure, that we don't accidentally wipe
171      * the mapping when we actually still want to use it. Since the time between
172      * adding an entry in the map and it showing up in the model should be
173      * small, this should rarely, if ever happen.
174      */
175     m_dropTargetPositionsCleanup->setInterval(10000);
176     m_dropTargetPositionsCleanup->setSingleShot(true);
177     connect(m_dropTargetPositionsCleanup, &QTimer::timeout, this, [this]() {
178         if (!m_dropTargetPositions.isEmpty()) {
179             qCDebug(FOLDERMODEL) << "clearing drop target positions after timeout:" << m_dropTargetPositions;
180             m_dropTargetPositions.clear();
181         }
182     });
183 
184     m_selectionModel = new QItemSelectionModel(this, this);
185     connect(m_selectionModel, &QItemSelectionModel::selectionChanged, this, &FolderModel::selectionChanged);
186 
187     setSourceModel(m_dirModel);
188 
189     setSortLocaleAware(true);
190     setFilterCaseSensitivity(Qt::CaseInsensitive);
191     setDynamicSortFilter(true);
192 
193     sort(m_sortMode, m_sortDesc ? Qt::DescendingOrder : Qt::AscendingOrder);
194 
195     createActions();
196 }
197 
~FolderModel()198 FolderModel::~FolderModel()
199 {
200     if (m_usedByContainment) {
201         // disconnect so we don't handle signals from the screen mapper when
202         // removeScreen is called
203         m_screenMapper->disconnect(this);
204         m_screenMapper->removeScreen(m_screen, resolvedUrl());
205     }
206 }
207 
roleNames() const208 QHash<int, QByteArray> FolderModel::roleNames() const
209 {
210     return staticRoleNames();
211 }
212 
staticRoleNames()213 QHash<int, QByteArray> FolderModel::staticRoleNames()
214 {
215     QHash<int, QByteArray> roleNames;
216     roleNames[Qt::DisplayRole] = "display";
217     roleNames[Qt::DecorationRole] = "decoration";
218     roleNames[BlankRole] = "blank";
219     roleNames[SelectedRole] = "selected";
220     roleNames[IsDirRole] = "isDir";
221     roleNames[IsLinkRole] = "isLink";
222     roleNames[IsHiddenRole] = "isHidden";
223     roleNames[UrlRole] = "url";
224     roleNames[LinkDestinationUrl] = "linkDestinationUrl";
225     roleNames[SizeRole] = "size";
226     roleNames[TypeRole] = "type";
227     roleNames[FileNameWrappedRole] = "displayWrapped";
228 
229     return roleNames;
230 }
231 
localMenuPosition() const232 QPoint FolderModel::localMenuPosition() const
233 {
234     QScreen *screen = nullptr;
235     for (auto *s : qApp->screens()) {
236         if (s->geometry().contains(m_menuPosition)) {
237             screen = s;
238             break;
239         }
240     }
241     if (screen) {
242         return m_menuPosition - screen->geometry().topLeft();
243     }
244     return m_menuPosition;
245 }
246 
classBegin()247 void FolderModel::classBegin()
248 {
249 }
250 
componentComplete()251 void FolderModel::componentComplete()
252 {
253     m_complete = true;
254     invalidate();
255 }
256 
invalidateIfComplete()257 void FolderModel::invalidateIfComplete()
258 {
259     if (!m_complete) {
260         return;
261     }
262 
263     invalidate();
264 }
265 
invalidateFilterIfComplete()266 void FolderModel::invalidateFilterIfComplete()
267 {
268     if (!m_complete) {
269         return;
270     }
271 
272     invalidateFilter();
273 }
274 
newFileMenuItemCreated(const QUrl & url)275 void FolderModel::newFileMenuItemCreated(const QUrl &url)
276 {
277     if (m_usedByContainment && !m_screenMapper->sharedDesktops()) {
278         m_screenMapper->addMapping(url, m_screen, ScreenMapper::DelayedSignal);
279         m_dropTargetPositions.insert(url.fileName(), localMenuPosition());
280         m_menuPosition = {};
281         m_dropTargetPositionsCleanup->start();
282     }
283 }
284 
url() const285 QString FolderModel::url() const
286 {
287     return m_url;
288 }
289 
setUrl(const QString & url)290 void FolderModel::setUrl(const QString &url)
291 {
292     const QUrl &resolvedNewUrl = resolve(url);
293 
294     if (url == m_url) {
295         m_dirModel->dirLister()->updateDirectory(resolvedNewUrl);
296         return;
297     }
298 
299     const auto oldUrl = resolvedUrl();
300 
301     beginResetModel();
302     m_url = url;
303     m_isDirCache.clear();
304     m_dirModel->dirLister()->openUrl(resolvedNewUrl);
305     clearDragImages();
306     m_dragIndexes.clear();
307     endResetModel();
308 
309     Q_EMIT urlChanged();
310     Q_EMIT resolvedUrlChanged();
311 
312     m_errorString.clear();
313     Q_EMIT errorStringChanged();
314 
315     if (m_dirWatch) {
316         delete m_dirWatch;
317         m_dirWatch = nullptr;
318     }
319 
320     if (resolvedNewUrl.isValid()) {
321         m_dirWatch = new KDirWatch(this);
322         connect(m_dirWatch, &KDirWatch::created, this, &FolderModel::iconNameChanged);
323         connect(m_dirWatch, &KDirWatch::dirty, this, &FolderModel::iconNameChanged);
324         m_dirWatch->addFile(resolvedNewUrl.toLocalFile() + QLatin1String("/.directory"));
325     }
326 
327     if (m_dragInProgress) {
328         m_urlChangedWhileDragging = true;
329     }
330 
331     Q_EMIT iconNameChanged();
332 
333     if (m_usedByContainment && !m_screenMapper->sharedDesktops()) {
334         m_screenMapper->removeScreen(m_screen, oldUrl);
335         m_screenMapper->addScreen(m_screen, resolvedUrl());
336     }
337 }
338 
resolvedUrl() const339 QUrl FolderModel::resolvedUrl() const
340 {
341     return m_dirModel->dirLister()->url();
342 }
343 
resolve(const QString & url)344 QUrl FolderModel::resolve(const QString &url)
345 {
346     QUrl resolvedUrl;
347 
348     if (url.startsWith(QLatin1Char('~'))) {
349         resolvedUrl = QUrl::fromLocalFile(KShell::tildeExpand(url));
350     } else {
351         resolvedUrl = QUrl::fromUserInput(url);
352     }
353 
354     return resolvedUrl;
355 }
356 
iconName() const357 QString FolderModel::iconName() const
358 {
359     const KFileItem rootItem(m_dirModel->dirLister()->url());
360 
361     if (!rootItem.isFinalIconKnown()) {
362         rootItem.determineMimeType();
363     }
364 
365     return rootItem.iconName();
366 }
367 
status() const368 FolderModel::Status FolderModel::status() const
369 {
370     return m_status;
371 }
372 
setStatus(Status status)373 void FolderModel::setStatus(Status status)
374 {
375     if (m_status != status) {
376         m_status = status;
377         Q_EMIT statusChanged();
378     }
379 }
380 
errorString() const381 QString FolderModel::errorString() const
382 {
383     return m_errorString;
384 }
385 
dragging() const386 bool FolderModel::dragging() const
387 {
388     return m_dragInProgress;
389 }
390 
usedByContainment() const391 bool FolderModel::usedByContainment() const
392 {
393     return m_usedByContainment;
394 }
395 
setUsedByContainment(bool used)396 void FolderModel::setUsedByContainment(bool used)
397 {
398     if (m_usedByContainment != used) {
399         m_usedByContainment = used;
400 
401         QAction *action = m_actionCollection.action(QStringLiteral("refresh"));
402 
403         if (action) {
404             action->setText(m_usedByContainment ? i18n("&Refresh Desktop") : i18n("&Refresh View"));
405             action->setIcon(m_usedByContainment ? QIcon::fromTheme(QStringLiteral("user-desktop")) : QIcon::fromTheme(QStringLiteral("view-refresh")));
406         }
407 
408         m_screenMapper->disconnect(this);
409         connect(m_screenMapper, &ScreenMapper::screensChanged, this, &FolderModel::invalidateFilterIfComplete);
410         connect(m_screenMapper, &ScreenMapper::screenMappingChanged, this, &FolderModel::invalidateFilterIfComplete);
411 
412         Q_EMIT usedByContainmentChanged();
413     }
414 }
415 
locked() const416 bool FolderModel::locked() const
417 {
418     return m_locked;
419 }
420 
setLocked(bool locked)421 void FolderModel::setLocked(bool locked)
422 {
423     if (m_locked != locked) {
424         m_locked = locked;
425 
426         Q_EMIT lockedChanged();
427     }
428 }
429 
dirListFailed(const QString & error)430 void FolderModel::dirListFailed(const QString &error)
431 {
432     m_errorString = error;
433     Q_EMIT errorStringChanged();
434 }
435 
sortMode() const436 int FolderModel::sortMode() const
437 {
438     return m_sortMode;
439 }
440 
setSortMode(int mode)441 void FolderModel::setSortMode(int mode)
442 {
443     if (m_sortMode != mode) {
444         m_sortMode = mode;
445 
446         if (mode == -1 /* Unsorted */) {
447             setDynamicSortFilter(false);
448         } else {
449             invalidateIfComplete();
450             sort(m_sortMode, m_sortDesc ? Qt::DescendingOrder : Qt::AscendingOrder);
451             setDynamicSortFilter(true);
452         }
453 
454         Q_EMIT sortModeChanged();
455     }
456 }
457 
sortDesc() const458 bool FolderModel::sortDesc() const
459 {
460     return m_sortDesc;
461 }
462 
setSortDesc(bool desc)463 void FolderModel::setSortDesc(bool desc)
464 {
465     if (m_sortDesc != desc) {
466         m_sortDesc = desc;
467 
468         if (m_sortMode != -1 /* Unsorted */) {
469             invalidateIfComplete();
470             sort(m_sortMode, m_sortDesc ? Qt::DescendingOrder : Qt::AscendingOrder);
471         }
472 
473         Q_EMIT sortDescChanged();
474     }
475 }
476 
sortDirsFirst() const477 bool FolderModel::sortDirsFirst() const
478 {
479     return m_sortDirsFirst;
480 }
481 
setSortDirsFirst(bool enable)482 void FolderModel::setSortDirsFirst(bool enable)
483 {
484     if (m_sortDirsFirst != enable) {
485         m_sortDirsFirst = enable;
486 
487         if (m_sortMode != -1 /* Unsorted */) {
488             invalidateIfComplete();
489             sort(m_sortMode, m_sortDesc ? Qt::DescendingOrder : Qt::AscendingOrder);
490         }
491 
492         Q_EMIT sortDirsFirstChanged();
493     }
494 }
495 
parseDesktopFiles() const496 bool FolderModel::parseDesktopFiles() const
497 {
498     return m_parseDesktopFiles;
499 }
500 
setParseDesktopFiles(bool enable)501 void FolderModel::setParseDesktopFiles(bool enable)
502 {
503     if (m_parseDesktopFiles != enable) {
504         m_parseDesktopFiles = enable;
505         Q_EMIT parseDesktopFilesChanged();
506     }
507 }
508 
viewAdapter() const509 QObject *FolderModel::viewAdapter() const
510 {
511     return m_viewAdapter;
512 }
513 
setViewAdapter(QObject * adapter)514 void FolderModel::setViewAdapter(QObject *adapter)
515 {
516     if (m_viewAdapter != adapter) {
517         KAbstractViewAdapter *abstractViewAdapter = dynamic_cast<KAbstractViewAdapter *>(adapter);
518 
519         m_viewAdapter = abstractViewAdapter;
520 
521         if (m_viewAdapter && !m_previewGenerator) {
522             m_previewGenerator = new KFilePreviewGenerator(abstractViewAdapter, this);
523             m_previewGenerator->setPreviewShown(m_previews);
524             m_previewGenerator->setEnabledPlugins(m_effectivePreviewPlugins);
525         }
526 
527         Q_EMIT viewAdapterChanged();
528     }
529 }
530 
previews() const531 bool FolderModel::previews() const
532 {
533     return m_previews;
534 }
535 
setPreviews(bool previews)536 void FolderModel::setPreviews(bool previews)
537 {
538     if (m_previews != previews) {
539         m_previews = previews;
540 
541         if (m_previewGenerator) {
542             m_previewGenerator->setPreviewShown(m_previews);
543         }
544 
545         Q_EMIT previewsChanged();
546     }
547 }
548 
previewPlugins() const549 QStringList FolderModel::previewPlugins() const
550 {
551     return m_previewPlugins;
552 }
553 
setPreviewPlugins(const QStringList & previewPlugins)554 void FolderModel::setPreviewPlugins(const QStringList &previewPlugins)
555 {
556     QStringList effectivePlugins = previewPlugins;
557     if (effectivePlugins.isEmpty()) {
558         effectivePlugins = KIO::PreviewJob::defaultPlugins();
559     }
560 
561     if (m_effectivePreviewPlugins != effectivePlugins) {
562         m_effectivePreviewPlugins = effectivePlugins;
563 
564         if (m_previewGenerator) {
565             m_previewGenerator->setPreviewShown(false);
566             m_previewGenerator->setEnabledPlugins(m_effectivePreviewPlugins);
567             m_previewGenerator->setPreviewShown(true);
568         }
569     }
570 
571     if (m_previewPlugins != previewPlugins) {
572         m_previewPlugins = previewPlugins;
573         Q_EMIT previewPluginsChanged();
574     }
575 }
576 
filterMode() const577 int FolderModel::filterMode() const
578 {
579     return m_filterMode;
580 }
581 
setFilterMode(int filterMode)582 void FolderModel::setFilterMode(int filterMode)
583 {
584     if (m_filterMode != (FilterMode)filterMode) {
585         m_filterMode = (FilterMode)filterMode;
586 
587         invalidateFilterIfComplete();
588 
589         Q_EMIT filterModeChanged();
590     }
591 }
592 
filterPattern() const593 QString FolderModel::filterPattern() const
594 {
595     return m_filterPattern;
596 }
597 
setFilterPattern(const QString & pattern)598 void FolderModel::setFilterPattern(const QString &pattern)
599 {
600     if (m_filterPattern == pattern) {
601         return;
602     }
603 
604     m_filterPattern = pattern;
605     m_filterPatternMatchAll = (pattern == QLatin1String("*"));
606 
607     const QStringList patterns = pattern.split(QLatin1Char(' '));
608     m_regExps.clear();
609     m_regExps.reserve(patterns.count());
610 
611     foreach (const QString &pattern, patterns) {
612         QRegExp rx(pattern);
613         rx.setPatternSyntax(QRegExp::Wildcard);
614         rx.setCaseSensitivity(Qt::CaseInsensitive);
615         m_regExps.append(rx);
616     }
617 
618     invalidateFilterIfComplete();
619 
620     Q_EMIT filterPatternChanged();
621 }
622 
filterMimeTypes() const623 QStringList FolderModel::filterMimeTypes() const
624 {
625     return m_mimeSet.values();
626 }
627 
setFilterMimeTypes(const QStringList & mimeList)628 void FolderModel::setFilterMimeTypes(const QStringList &mimeList)
629 {
630     const QSet<QString> set(mimeList.constBegin(), mimeList.constEnd());
631 
632     if (m_mimeSet != set) {
633         m_mimeSet = set;
634 
635         invalidateFilterIfComplete();
636 
637         Q_EMIT filterMimeTypesChanged();
638     }
639 }
640 
setScreen(int screen)641 void FolderModel::setScreen(int screen)
642 {
643     m_screenUsed = (screen != -1);
644 
645     if (!m_screenUsed || m_screen == screen)
646         return;
647 
648     m_screen = screen;
649     if (m_usedByContainment && !m_screenMapper->sharedDesktops()) {
650         m_screenMapper->addScreen(screen, resolvedUrl());
651     }
652     Q_EMIT screenChanged();
653 }
654 
rootItem() const655 KFileItem FolderModel::rootItem() const
656 {
657     return m_dirModel->dirLister()->rootItem();
658 }
659 
up()660 void FolderModel::up()
661 {
662     const QUrl &up = KIO::upUrl(resolvedUrl());
663 
664     if (up.isValid()) {
665         setUrl(up.toString());
666     }
667 }
668 
cd(int row)669 void FolderModel::cd(int row)
670 {
671     if (row < 0) {
672         return;
673     }
674 
675     const QModelIndex idx = index(row, 0);
676     bool isDir = data(idx, IsDirRole).toBool();
677 
678     if (isDir) {
679         const KFileItem item = itemForIndex(idx);
680         if (m_parseDesktopFiles && item.isDesktopFile()) {
681             const KDesktopFile file(item.targetUrl().path());
682             if (file.hasLinkType()) {
683                 setUrl(file.readUrl());
684             }
685         } else {
686             setUrl(item.targetUrl().toString());
687         }
688     }
689 }
690 
run(int row)691 void FolderModel::run(int row)
692 {
693     if (row < 0) {
694         return;
695     }
696 
697     KFileItem item = itemForIndex(index(row, 0));
698 
699     QUrl url(item.targetUrl());
700 
701     // FIXME TODO: This can go once we depend on a KIO w/ fe1f50caaf2.
702     if (url.scheme().isEmpty()) {
703         url.setScheme(QStringLiteral("file"));
704     }
705 
706     KRun *run = new KRun(url, nullptr);
707     // On desktop:/ we want to be able to run .desktop files right away,
708     // otherwise ask for security reasons. We also don't use the targetUrl()
709     // from above since we don't want the resolved /home/foo/Desktop URL.
710     run->setShowScriptExecutionPrompt(item.url().scheme() != QLatin1String("desktop")
711                                       || item.url().adjusted(QUrl::RemoveFilename).path() != QLatin1String("/")
712                                       || !item.isDesktopFile());
713 }
714 
runSelected()715 void FolderModel::runSelected()
716 {
717     if (!m_selectionModel->hasSelection()) {
718         return;
719     }
720 
721     if (m_selectionModel->selectedIndexes().count() == 1) {
722         run(m_selectionModel->selectedIndexes().constFirst().row());
723         return;
724     }
725 
726     KFileItemActions fileItemActions(this);
727     KFileItemList items;
728 
729     foreach (const QModelIndex &index, m_selectionModel->selectedIndexes()) {
730         // Skip over directories.
731         if (!index.data(IsDirRole).toBool()) {
732             items << itemForIndex(index);
733         }
734     }
735 
736     fileItemActions.runPreferredApplications(items, QString());
737 }
738 
rename(int row,const QString & name)739 void FolderModel::rename(int row, const QString &name)
740 {
741     if (row < 0) {
742         return;
743     }
744 
745     QModelIndex idx = index(row, 0);
746     m_dirModel->setData(mapToSource(idx), name, Qt::EditRole);
747 }
748 
fileExtensionBoundary(int row)749 int FolderModel::fileExtensionBoundary(int row)
750 {
751     const QModelIndex idx = index(row, 0);
752     const QString &name = data(idx, Qt::DisplayRole).toString();
753 
754     int boundary = name.length();
755 
756     if (data(idx, IsDirRole).toBool()) {
757         return boundary;
758     }
759 
760     QMimeDatabase db;
761     const QString &ext = db.suffixForFileName(name);
762 
763     if (ext.isEmpty()) {
764         boundary = name.lastIndexOf(QLatin1Char('.'));
765 
766         if (boundary < 1) {
767             boundary = name.length();
768         }
769     } else {
770         boundary -= ext.length() + 1;
771     }
772 
773     return boundary;
774 }
775 
hasSelection() const776 bool FolderModel::hasSelection() const
777 {
778     return m_selectionModel->hasSelection();
779 }
780 
isSelected(int row)781 bool FolderModel::isSelected(int row)
782 {
783     if (row < 0) {
784         return false;
785     }
786 
787     return m_selectionModel->isSelected(index(row, 0));
788 }
789 
setSelected(int row)790 void FolderModel::setSelected(int row)
791 {
792     if (row < 0) {
793         return;
794     }
795 
796     m_selectionModel->select(index(row, 0), QItemSelectionModel::Select);
797 }
798 
toggleSelected(int row)799 void FolderModel::toggleSelected(int row)
800 {
801     if (row < 0) {
802         return;
803     }
804 
805     m_selectionModel->select(index(row, 0), QItemSelectionModel::Toggle);
806 }
807 
setRangeSelected(int anchor,int to)808 void FolderModel::setRangeSelected(int anchor, int to)
809 {
810     if (anchor < 0 || to < 0) {
811         return;
812     }
813 
814     QItemSelection selection(index(anchor, 0), index(to, 0));
815     m_selectionModel->select(selection, QItemSelectionModel::ClearAndSelect);
816 }
817 
updateSelection(const QVariantList & rows,bool toggle)818 void FolderModel::updateSelection(const QVariantList &rows, bool toggle)
819 {
820     QItemSelection newSelection;
821 
822     int iRow = -1;
823 
824     foreach (const QVariant &row, rows) {
825         iRow = row.toInt();
826 
827         if (iRow < 0) {
828             return;
829         }
830 
831         const QModelIndex &idx = index(iRow, 0);
832         newSelection.select(idx, idx);
833     }
834 
835     if (toggle) {
836         QItemSelection pinnedSelection = m_pinnedSelection;
837         pinnedSelection.merge(newSelection, QItemSelectionModel::Toggle);
838         m_selectionModel->select(pinnedSelection, QItemSelectionModel::ClearAndSelect);
839     } else {
840         m_selectionModel->select(newSelection, QItemSelectionModel::ClearAndSelect);
841     }
842 }
843 
clearSelection()844 void FolderModel::clearSelection()
845 {
846     if (m_selectionModel->hasSelection()) {
847         m_selectionModel->clear();
848     }
849 }
850 
pinSelection()851 void FolderModel::pinSelection()
852 {
853     m_pinnedSelection = m_selectionModel->selection();
854 }
855 
unpinSelection()856 void FolderModel::unpinSelection()
857 {
858     m_pinnedSelection = QItemSelection();
859 }
860 
addItemDragImage(int row,int x,int y,int width,int height,const QVariant & image)861 void FolderModel::addItemDragImage(int row, int x, int y, int width, int height, const QVariant &image)
862 {
863     if (row < 0) {
864         return;
865     }
866 
867     delete m_dragImages.take(row);
868 
869     DragImage *dragImage = new DragImage();
870     dragImage->row = row;
871     dragImage->rect = QRect(x, y, width, height);
872     dragImage->image = image.value<QImage>();
873     dragImage->blank = false;
874 
875     m_dragImages.insert(row, dragImage);
876 }
877 
clearDragImages()878 void FolderModel::clearDragImages()
879 {
880     qDeleteAll(m_dragImages);
881     m_dragImages.clear();
882 }
883 
setDragHotSpotScrollOffset(int x,int y)884 void FolderModel::setDragHotSpotScrollOffset(int x, int y)
885 {
886     m_dragHotSpotScrollOffset.setX(x);
887     m_dragHotSpotScrollOffset.setY(y);
888 }
889 
dragCursorOffset(int row)890 QPoint FolderModel::dragCursorOffset(int row)
891 {
892     DragImage *image = m_dragImages.value(row);
893     if (!image) {
894         return QPoint(0, 0);
895     }
896 
897     return image->cursorOffset;
898 }
899 
addDragImage(QDrag * drag,int x,int y)900 void FolderModel::addDragImage(QDrag *drag, int x, int y)
901 {
902     if (!drag || m_dragImages.isEmpty()) {
903         return;
904     }
905 
906     QRegion region;
907 
908     foreach (DragImage *image, m_dragImages) {
909         image->blank = isBlank(image->row);
910         image->rect.translate(-m_dragHotSpotScrollOffset.x(), -m_dragHotSpotScrollOffset.y());
911         if (!image->blank && !image->image.isNull()) {
912             region = region.united(image->rect);
913         }
914     }
915 
916     QRect rect = region.boundingRect();
917     QPoint offset = rect.topLeft();
918     rect.translate(-offset.x(), -offset.y());
919 
920     QImage dragImage(rect.size(), QImage::Format_RGBA8888);
921     dragImage.fill(Qt::transparent);
922 
923     QPainter painter(&dragImage);
924 
925     QPoint pos;
926 
927     foreach (DragImage *image, m_dragImages) {
928         if (!image->blank && !image->image.isNull()) {
929             pos = image->rect.translated(-offset.x(), -offset.y()).topLeft();
930             image->cursorOffset.setX(pos.x() - (x - offset.x()));
931             image->cursorOffset.setY(pos.y() - (y - offset.y()));
932 
933             painter.drawImage(pos, image->image);
934         }
935 
936         // FIXME HACK: Operate on copy.
937         image->rect.translate(m_dragHotSpotScrollOffset.x(), m_dragHotSpotScrollOffset.y());
938     }
939 
940     drag->setPixmap(QPixmap::fromImage(dragImage));
941     drag->setHotSpot(QPoint(x - offset.x(), y - offset.y()));
942 }
943 
dragSelected(int x,int y)944 void FolderModel::dragSelected(int x, int y)
945 {
946     if (m_dragInProgress) {
947         return;
948     }
949 
950     m_dragInProgress = true;
951     Q_EMIT draggingChanged();
952     m_urlChangedWhileDragging = false;
953 
954     // Avoid starting a drag synchronously in a mouse handler or interferes with
955     // child event filtering in parent items (and thus e.g. press-and-hold hand-
956     // ling in a containment).
957     QMetaObject::invokeMethod(this, "dragSelectedInternal", Qt::QueuedConnection, Q_ARG(int, x), Q_ARG(int, y));
958 }
959 
dragSelectedInternal(int x,int y)960 void FolderModel::dragSelectedInternal(int x, int y)
961 {
962     if (!m_viewAdapter || !m_selectionModel->hasSelection()) {
963         m_dragInProgress = false;
964         Q_EMIT draggingChanged();
965         return;
966     }
967 
968     ItemViewAdapter *adapter = qobject_cast<ItemViewAdapter *>(m_viewAdapter);
969     QQuickItem *item = qobject_cast<QQuickItem *>(adapter->adapterView());
970 
971     QDrag *drag = new QDrag(item);
972 
973     addDragImage(drag, x, y);
974 
975     m_dragIndexes = m_selectionModel->selectedIndexes();
976 
977     std::sort(m_dragIndexes.begin(), m_dragIndexes.end());
978 
979     // TODO: Optimize to Q_EMIT contiguous groups.
980     Q_EMIT dataChanged(m_dragIndexes.first(), m_dragIndexes.last(), QVector<int>() << BlankRole);
981 
982     QModelIndexList sourceDragIndexes;
983     sourceDragIndexes.reserve(m_dragIndexes.count());
984     foreach (const QModelIndex &index, m_dragIndexes) {
985         sourceDragIndexes.append(mapToSource(index));
986     }
987 
988     drag->setMimeData(m_dirModel->mimeData(sourceDragIndexes));
989 
990     // Due to spring-loading (aka auto-expand), the URL might change
991     // while the drag is in-flight - in that case we don't want to
992     // unnecessarily Q_EMIT dataChanged() for (possibly invalid) indices
993     // after it ends.
994     const QUrl currentUrl(m_dirModel->dirLister()->url());
995 
996     item->grabMouse();
997     drag->exec(supportedDragActions());
998 
999     item->ungrabMouse();
1000 
1001     m_dragInProgress = false;
1002     Q_EMIT draggingChanged();
1003     m_urlChangedWhileDragging = false;
1004 
1005     if (m_dirModel->dirLister()->url() == currentUrl) {
1006         const QModelIndex first(m_dragIndexes.first());
1007         const QModelIndex last(m_dragIndexes.last());
1008         m_dragIndexes.clear();
1009         // TODO: Optimize to Q_EMIT contiguous groups.
1010         Q_EMIT dataChanged(first, last, QVector<int>() << BlankRole);
1011     }
1012 }
1013 
isDropBetweenSharedViews(const QList<QUrl> & urls,const QUrl & folderUrl)1014 static bool isDropBetweenSharedViews(const QList<QUrl> &urls, const QUrl &folderUrl)
1015 {
1016     for (const auto &url : urls) {
1017         if (folderUrl.adjusted(QUrl::StripTrailingSlash) != url.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash)) {
1018             return false;
1019         }
1020     }
1021     return true;
1022 }
1023 
1024 static const char *s_ark_dndextract_service = "application/x-kde-ark-dndextract-service";
1025 static const char *s_ark_dndextract_path = "application/x-kde-ark-dndextract-path";
1026 
arkDbusServiceName(const QMimeData * mimeData)1027 static QString arkDbusServiceName(const QMimeData *mimeData)
1028 {
1029     return QString::fromUtf8(mimeData->data(QString::fromLatin1(s_ark_dndextract_service)));
1030 }
1031 
arkDbusPath(const QMimeData * mimeData)1032 static QString arkDbusPath(const QMimeData *mimeData)
1033 {
1034     return QString::fromUtf8(mimeData->data(QString::fromLatin1(s_ark_dndextract_path)));
1035 }
1036 
isMimeDataArkDnd(const QMimeData * mimeData)1037 static bool isMimeDataArkDnd(const QMimeData *mimeData)
1038 {
1039     return mimeData->hasFormat(QString::fromLatin1(s_ark_dndextract_service)) //
1040         && mimeData->hasFormat(QString::fromLatin1(s_ark_dndextract_path));
1041 }
1042 
drop(QQuickItem * target,QObject * dropEvent,int row,bool showMenuManually)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     if (!mimeData) {
1048         return;
1049     }
1050 
1051     QModelIndex idx;
1052     KFileItem item;
1053 
1054     if (row > -1 && row < rowCount()) {
1055         idx = index(row, 0);
1056         item = itemForIndex(idx);
1057     }
1058 
1059     QUrl dropTargetUrl;
1060 
1061     // So we get to run mostLocalUrl() over the current URL.
1062     if (item.isNull()) {
1063         item = rootItem();
1064     }
1065 
1066     if (item.isNull()) {
1067         dropTargetUrl = m_dirModel->dirLister()->url();
1068     } else if (m_parseDesktopFiles && item.isDesktopFile()) {
1069         const KDesktopFile file(item.targetUrl().path());
1070 
1071         if (file.hasLinkType()) {
1072             dropTargetUrl = QUrl(file.readUrl());
1073         } else {
1074             dropTargetUrl = item.mostLocalUrl();
1075         }
1076     } else {
1077         dropTargetUrl = item.mostLocalUrl();
1078     }
1079 
1080     auto dropTargetFolderUrl = dropTargetUrl;
1081     if (dropTargetFolderUrl.fileName() == QLatin1Char('.')) {
1082         // the target URL for desktop:/ is e.g. 'file://home/user/Desktop/.'
1083         dropTargetFolderUrl = dropTargetFolderUrl.adjusted(QUrl::RemoveFilename);
1084     }
1085 
1086     // use dropTargetUrl to resolve desktop:/ to the actual file location which is also used by the mime data
1087     /* QMimeData operates on local URLs, but the dir lister and thus screen mapper and positioner may
1088      * use a fancy scheme like desktop:/ instead. Ensure we always use the latter to properly map URLs,
1089      * i.e. go from file:///home/user/Desktop/file to desktop:/file
1090      */
1091     auto mappableUrl = [this, dropTargetFolderUrl](const QUrl &url) -> QUrl {
1092         if (dropTargetFolderUrl != m_dirModel->dirLister()->url()) {
1093             QString mappedUrl = url.toString();
1094             const auto local = dropTargetFolderUrl.toString();
1095             const auto internal = m_dirModel->dirLister()->url().toString();
1096             if (mappedUrl.startsWith(local)) {
1097                 mappedUrl.replace(0, local.size(), internal);
1098             }
1099             return ScreenMapper::stringToUrl(mappedUrl);
1100         }
1101         return url;
1102     };
1103 
1104     const int x = dropEvent->property("x").toInt();
1105     const int y = dropEvent->property("y").toInt();
1106     const QPoint dropPos = {x, y};
1107 
1108     if (m_dragInProgress && row == -1 && !m_urlChangedWhileDragging) {
1109         if (m_locked || mimeData->urls().isEmpty()) {
1110             return;
1111         }
1112 
1113         setSortMode(-1);
1114 
1115         for (const auto &url : mimeData->urls()) {
1116             m_dropTargetPositions.insert(url.fileName(), dropPos);
1117             m_screenMapper->addMapping(mappableUrl(url), m_screen, ScreenMapper::DelayedSignal);
1118             m_screenMapper->removeItemFromDisabledScreen(mappableUrl(url));
1119         }
1120         Q_EMIT move(x, y, mimeData->urls());
1121 
1122         return;
1123     }
1124 
1125     if (isMimeDataArkDnd(mimeData)) {
1126         QDBusMessage message = QDBusMessage::createMethodCall(arkDbusServiceName(mimeData),
1127                                                               arkDbusPath(mimeData),
1128                                                               QStringLiteral("org.kde.ark.DndExtract"),
1129                                                               QStringLiteral("extractSelectedFilesTo"));
1130         message.setArguments({dropTargetUrl.toDisplayString(QUrl::PreferLocalFile)});
1131 
1132         QDBusConnection::sessionBus().call(message, QDBus::NoBlock);
1133 
1134         return;
1135     }
1136 
1137     if (idx.isValid() && !(flags(idx) & Qt::ItemIsDropEnabled)) {
1138         return;
1139     }
1140 
1141     // Catch drops from a Task Manager and convert to usable URL.
1142     if (!mimeData->hasUrls() && mimeData->hasFormat(QStringLiteral("text/x-orgkdeplasmataskmanager_taskurl"))) {
1143         QList<QUrl> urls = {QUrl(QString::fromUtf8(mimeData->data(QStringLiteral("text/x-orgkdeplasmataskmanager_taskurl"))))};
1144         mimeData->setUrls(urls);
1145     }
1146 
1147     if (m_usedByContainment && !m_screenMapper->sharedDesktops()) {
1148         if (isDropBetweenSharedViews(mimeData->urls(), dropTargetFolderUrl)) {
1149             setSortMode(-1);
1150             const QList<QUrl> urls = mimeData->urls();
1151             for (const auto &url : urls) {
1152                 m_dropTargetPositions.insert(url.fileName(), dropPos);
1153                 m_screenMapper->addMapping(mappableUrl(url), m_screen, ScreenMapper::DelayedSignal);
1154                 m_screenMapper->removeItemFromDisabledScreen(mappableUrl(url));
1155             }
1156             m_dropTargetPositionsCleanup->start();
1157             return;
1158         }
1159     }
1160 
1161     Qt::DropAction proposedAction((Qt::DropAction)dropEvent->property("proposedAction").toInt());
1162     Qt::DropActions possibleActions(dropEvent->property("possibleActions").toInt());
1163     Qt::MouseButtons buttons(dropEvent->property("buttons").toInt());
1164     Qt::KeyboardModifiers modifiers(dropEvent->property("modifiers").toInt());
1165 
1166     auto pos = target->mapToScene(dropPos).toPoint();
1167     pos = target->window()->mapToGlobal(pos);
1168     QDropEvent ev(pos, possibleActions, mimeData, buttons, modifiers);
1169     ev.setDropAction(proposedAction);
1170 
1171     KIO::DropJobFlag flag = showMenuManually ? KIO::ShowMenuManually : KIO::DropJobDefaultFlags;
1172     KIO::DropJob *dropJob = KIO::drop(&ev, dropTargetUrl, flag);
1173     dropJob->uiDelegate()->setAutoErrorHandlingEnabled(true);
1174 
1175     // The QMimeData we extract from the DropArea's drop event is deleted as soon as this method
1176     // ends but we need to keep a copy for when popupMenuAboutToShow fires.
1177     QMimeData *mimeCopy = new QMimeData();
1178     const QStringList formats = mimeData->formats();
1179     for (const QString &format : formats) {
1180         mimeCopy->setData(format, mimeData->data(format));
1181     }
1182 
1183     connect(dropJob, &KIO::DropJob::popupMenuAboutToShow, this, [this, mimeCopy, x, y, dropJob](const KFileItemListProperties &) {
1184         Q_EMIT popupMenuAboutToShow(dropJob, mimeCopy, x, y);
1185         mimeCopy->deleteLater();
1186     });
1187 
1188     /*
1189      * Position files that come from a drag'n'drop event at the drop event
1190      * target position. To do so, we first listen to copy job to figure out
1191      * the target URL. Then we store the position of this drop event in the
1192      * hash and eventually trigger a move request when we get notified about
1193      * the new file event from the source model.
1194      */
1195     connect(dropJob, &KIO::DropJob::copyJobStarted, this, [this, dropPos, dropTargetUrl](KIO::CopyJob *copyJob) {
1196         auto map = [this, dropPos, dropTargetUrl](const QUrl &targetUrl) {
1197             m_dropTargetPositions.insert(targetUrl.fileName(), dropPos);
1198             m_dropTargetPositionsCleanup->start();
1199 
1200             if (m_usedByContainment && !m_screenMapper->sharedDesktops()) {
1201                 // assign a screen for the item before the copy is actually done, so
1202                 // filterAcceptsRow doesn't assign the default screen to it
1203                 QUrl url = resolvedUrl();
1204                 // if the folderview's folder is a standard path, just use the targetUrl for mapping
1205                 if (targetUrl.toString().startsWith(url.toString())) {
1206                     m_screenMapper->addMapping(targetUrl, m_screen, ScreenMapper::DelayedSignal);
1207                 } else if (targetUrl.toString().startsWith(dropTargetUrl.toString())) {
1208                     // if the folderview's folder is a special path, like desktop:// , we need to convert
1209                     // the targetUrl file:// path to a desktop:/ path for mapping
1210                     auto destPath = dropTargetUrl.path();
1211                     auto filePath = targetUrl.path();
1212                     if (filePath.startsWith(destPath)) {
1213                         url.setPath(filePath.remove(0, destPath.length()));
1214                         m_screenMapper->addMapping(url, m_screen, ScreenMapper::DelayedSignal);
1215                     }
1216                 }
1217             }
1218         };
1219         // remember drop target position for target URL and forget about the source URL
1220         connect(copyJob, &KIO::CopyJob::copyingDone, this, [map](KIO::Job *, const QUrl &, const QUrl &targetUrl, const QDateTime &, bool, bool) {
1221             map(targetUrl);
1222         });
1223         connect(copyJob, &KIO::CopyJob::copyingLinkDone, this, [map](KIO::Job *, const QUrl &, const QString &, const QUrl &targetUrl) {
1224             map(targetUrl);
1225         });
1226     });
1227 }
1228 
dropCwd(QObject * dropEvent)1229 void FolderModel::dropCwd(QObject *dropEvent)
1230 {
1231     QMimeData *mimeData = qobject_cast<QMimeData *>(dropEvent->property("mimeData").value<QObject *>());
1232 
1233     if (!mimeData) {
1234         return;
1235     }
1236 
1237     if (isMimeDataArkDnd(mimeData)) {
1238         QDBusMessage message = QDBusMessage::createMethodCall(arkDbusServiceName(mimeData),
1239                                                               arkDbusPath(mimeData),
1240                                                               QStringLiteral("org.kde.ark.DndExtract"),
1241                                                               QStringLiteral("extractSelectedFilesTo"));
1242         message.setArguments(QVariantList() << m_dirModel->dirLister()->url().adjusted(QUrl::PreferLocalFile).toString());
1243 
1244         QDBusConnection::sessionBus().call(message, QDBus::NoBlock);
1245     } else {
1246         Qt::DropAction proposedAction((Qt::DropAction)dropEvent->property("proposedAction").toInt());
1247         Qt::DropActions possibleActions(dropEvent->property("possibleActions").toInt());
1248         Qt::MouseButtons buttons(dropEvent->property("buttons").toInt());
1249         Qt::KeyboardModifiers modifiers(dropEvent->property("modifiers").toInt());
1250 
1251         QDropEvent ev(QPoint(), possibleActions, mimeData, buttons, modifiers);
1252         ev.setDropAction(proposedAction);
1253 
1254         KIO::DropJob *dropJob = KIO::drop(&ev, m_dirModel->dirLister()->url().adjusted(QUrl::PreferLocalFile));
1255         dropJob->uiDelegate()->setAutoErrorHandlingEnabled(true);
1256     }
1257 }
1258 
selectionChanged(const QItemSelection & selected,const QItemSelection & deselected)1259 void FolderModel::selectionChanged(const QItemSelection &selected, const QItemSelection &deselected)
1260 {
1261     QModelIndexList indices = selected.indexes();
1262     indices.append(deselected.indexes());
1263 
1264     QVector<int> roles;
1265     roles.append(SelectedRole);
1266 
1267     foreach (const QModelIndex &index, indices) {
1268         Q_EMIT dataChanged(index, index, roles);
1269     }
1270 
1271     if (!m_selectionModel->hasSelection()) {
1272         clearDragImages();
1273     } else {
1274         foreach (const QModelIndex &idx, deselected.indexes()) {
1275             delete m_dragImages.take(idx.row());
1276         }
1277     }
1278 
1279     updateActions();
1280 }
1281 
isBlank(int row) const1282 bool FolderModel::isBlank(int row) const
1283 {
1284     if (row < 0) {
1285         return true;
1286     }
1287 
1288     return data(index(row, 0), BlankRole).toBool();
1289 }
1290 
data(const QModelIndex & index,int role) const1291 QVariant FolderModel::data(const QModelIndex &index, int role) const
1292 {
1293     if (!index.isValid()) {
1294         return QVariant();
1295     }
1296 
1297     if (role == BlankRole) {
1298         return m_dragIndexes.contains(index);
1299     } else if (role == SelectedRole) {
1300         return m_selectionModel->isSelected(index);
1301     } else if (role == IsDirRole) {
1302         return isDir(mapToSource(index), m_dirModel);
1303     } else if (role == IsLinkRole) {
1304         const KFileItem item = itemForIndex(index);
1305         return item.isLink();
1306     } else if (role == IsHiddenRole) {
1307         const KFileItem item = itemForIndex(index);
1308         return item.isHidden();
1309     } else if (role == UrlRole) {
1310         return itemForIndex(index).url();
1311     } else if (role == LinkDestinationUrl) {
1312         const KFileItem item = itemForIndex(index);
1313 
1314         if (m_parseDesktopFiles && item.isDesktopFile()) {
1315             const KDesktopFile file(item.targetUrl().path());
1316 
1317             if (file.hasLinkType()) {
1318                 return file.readUrl();
1319             }
1320         }
1321 
1322         return item.targetUrl();
1323     } else if (role == SizeRole) {
1324         bool isDir = data(index, IsDirRole).toBool();
1325 
1326         if (!isDir) {
1327             return m_dirModel->data(mapToSource(QSortFilterProxyModel::index(index.row(), 1)), Qt::DisplayRole);
1328         }
1329     } else if (role == TypeRole) {
1330         return m_dirModel->data(mapToSource(QSortFilterProxyModel::index(index.row(), 6)), Qt::DisplayRole);
1331     } else if (role == FileNameRole) {
1332         return itemForIndex(index).url().fileName();
1333     } else if (role == FileNameWrappedRole) {
1334         return KStringHandler::preProcessWrap(itemForIndex(index).text());
1335     }
1336 
1337     return QSortFilterProxyModel::data(index, role);
1338 }
1339 
indexForUrl(const QUrl & url) const1340 int FolderModel::indexForUrl(const QUrl &url) const
1341 {
1342     return mapFromSource(m_dirModel->indexForUrl(url)).row();
1343 }
1344 
itemForIndex(const QModelIndex & index) const1345 KFileItem FolderModel::itemForIndex(const QModelIndex &index) const
1346 {
1347     return m_dirModel->itemForIndex(mapToSource(index));
1348 }
1349 
isDir(const QModelIndex & index,const KDirModel * dirModel) const1350 bool FolderModel::isDir(const QModelIndex &index, const KDirModel *dirModel) const
1351 {
1352     KFileItem item = dirModel->itemForIndex(index);
1353     if (item.isDir()) {
1354         return true;
1355     }
1356 
1357     auto it = m_isDirCache.constFind(item.url());
1358     if (it != m_isDirCache.constEnd()) {
1359         return *it;
1360     }
1361 
1362     if (m_parseDesktopFiles && item.isDesktopFile()) {
1363         // Check if the desktop file is a link to a directory
1364         KDesktopFile file(item.targetUrl().path());
1365 
1366         if (!file.hasLinkType()) {
1367             return false;
1368         }
1369 
1370         const QUrl url(file.readUrl());
1371 
1372         // Check if we already have a running StatJob for this URL.
1373         if (m_isDirJobs.contains(item.url())) {
1374             return false;
1375         }
1376 
1377         // Assume the root folder of a protocol is always a folder.
1378         // This avoids spinning up e.g. trash KIO slave just to check whether trash:/ is a folder.
1379         if (url.path() == QLatin1String("/")) {
1380             m_isDirCache.insert(item.url(), true);
1381             return true;
1382         }
1383 
1384         if (KProtocolInfo::protocolClass(url.scheme()) != QLatin1String(":local")) {
1385             return false;
1386         }
1387 
1388         KIO::StatJob *job = KIO::stat(url, KIO::HideProgressInfo);
1389         job->setProperty("org.kde.plasma.folder_url", item.url());
1390         job->setSide(KIO::StatJob::SourceSide);
1391         job->setDetails(0);
1392         connect(job, &KJob::result, this, &FolderModel::statResult);
1393         m_isDirJobs.insert(item.url(), job);
1394     }
1395 
1396     return false;
1397 }
1398 
statResult(KJob * job)1399 void FolderModel::statResult(KJob *job)
1400 {
1401     KIO::StatJob *statJob = static_cast<KIO::StatJob *>(job);
1402 
1403     const QUrl &url = statJob->property("org.kde.plasma.folder_url").toUrl();
1404     const QModelIndex &idx = index(indexForUrl(url), 0);
1405 
1406     if (idx.isValid() && statJob->error() == KJob::NoError) {
1407         m_isDirCache[url] = statJob->statResult().isDir();
1408 
1409         Q_EMIT dataChanged(idx, idx, QVector<int>() << IsDirRole);
1410     }
1411 
1412     m_isDirJobs.remove(url);
1413 }
1414 
evictFromIsDirCache(const KFileItemList & items)1415 void FolderModel::evictFromIsDirCache(const KFileItemList &items)
1416 {
1417     foreach (const KFileItem &item, items) {
1418         m_screenMapper->removeFromMap(item.url());
1419         m_isDirCache.remove(item.url());
1420     }
1421 }
1422 
lessThan(const QModelIndex & left,const QModelIndex & right) const1423 bool FolderModel::lessThan(const QModelIndex &left, const QModelIndex &right) const
1424 {
1425     const KDirModel *dirModel = static_cast<KDirModel *>(sourceModel());
1426 
1427     if (m_sortDirsFirst || left.column() == KDirModel::Size) {
1428         bool leftIsDir = isDir(left, dirModel);
1429         bool rightIsDir = isDir(right, dirModel);
1430 
1431         if (leftIsDir && !rightIsDir) {
1432             return (sortOrder() == Qt::AscendingOrder);
1433         }
1434 
1435         if (!leftIsDir && rightIsDir) {
1436             return (sortOrder() == Qt::DescendingOrder);
1437         }
1438     }
1439 
1440     const KFileItem leftItem = dirModel->data(left, KDirModel::FileItemRole).value<KFileItem>();
1441     const KFileItem rightItem = dirModel->data(right, KDirModel::FileItemRole).value<KFileItem>();
1442     const int column = left.column();
1443     int result = 0;
1444 
1445     switch (column) {
1446     case KDirModel::Size: {
1447         if (isDir(left, dirModel) && isDir(right, dirModel)) {
1448             const int leftChildCount = dirModel->data(left, KDirModel::ChildCountRole).toInt();
1449             const int rightChildCount = dirModel->data(right, KDirModel::ChildCountRole).toInt();
1450             if (leftChildCount < rightChildCount)
1451                 result = -1;
1452             else if (leftChildCount > rightChildCount)
1453                 result = +1;
1454         } else {
1455             const KIO::filesize_t leftSize = leftItem.size();
1456             const KIO::filesize_t rightSize = rightItem.size();
1457             if (leftSize < rightSize)
1458                 result = -1;
1459             else if (leftSize > rightSize)
1460                 result = +1;
1461         }
1462 
1463         break;
1464     }
1465     case KDirModel::ModifiedTime: {
1466         const long long leftTime = leftItem.entry().numberValue(KIO::UDSEntry::UDS_MODIFICATION_TIME, -1);
1467         const long long rightTime = rightItem.entry().numberValue(KIO::UDSEntry::UDS_MODIFICATION_TIME, -1);
1468         if (leftTime < rightTime)
1469             result = -1;
1470         else if (leftTime > rightTime)
1471             result = +1;
1472 
1473         break;
1474     }
1475     case KDirModel::Type:
1476         result = QString::compare(dirModel->data(left, Qt::DisplayRole).toString(), dirModel->data(right, Qt::DisplayRole).toString());
1477         break;
1478 
1479     default:
1480         break;
1481     }
1482 
1483     if (result != 0)
1484         return result < 0;
1485 
1486     QCollator collator;
1487 
1488     result = collator.compare(leftItem.text(), rightItem.text());
1489 
1490     if (result != 0)
1491         return result < 0;
1492 
1493     result = collator.compare(leftItem.name(), rightItem.name());
1494 
1495     if (result != 0)
1496         return result < 0;
1497 
1498     return QString::compare(leftItem.url().url(), rightItem.url().url(), Qt::CaseSensitive);
1499 }
1500 
supportedDragActions() const1501 Qt::DropActions FolderModel::supportedDragActions() const
1502 {
1503     return Qt::CopyAction | Qt::MoveAction | Qt::LinkAction;
1504 }
1505 
supportedDropActions() const1506 Qt::DropActions FolderModel::supportedDropActions() const
1507 {
1508     return Qt::CopyAction | Qt::MoveAction | Qt::LinkAction;
1509 }
1510 
matchMimeType(const KFileItem & item) const1511 inline bool FolderModel::matchMimeType(const KFileItem &item) const
1512 {
1513     if (m_mimeSet.isEmpty()) {
1514         return false;
1515     }
1516 
1517     if (m_mimeSet.contains(QLatin1String("all/all")) || m_mimeSet.contains(QLatin1String("all/allfiles"))) {
1518         return true;
1519     }
1520 
1521     const QString mimeType = item.determineMimeType().name();
1522     return m_mimeSet.contains(mimeType);
1523 }
1524 
matchPattern(const KFileItem & item) const1525 inline bool FolderModel::matchPattern(const KFileItem &item) const
1526 {
1527     if (m_filterPatternMatchAll) {
1528         return true;
1529     }
1530 
1531     const QString name = item.name();
1532     QListIterator<QRegExp> i(m_regExps);
1533     while (i.hasNext()) {
1534         if (i.next().exactMatch(name)) {
1535             return true;
1536         }
1537     }
1538 
1539     return false;
1540 }
1541 
filterAcceptsRow(int sourceRow,const QModelIndex & sourceParent) const1542 bool FolderModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const
1543 {
1544     const KDirModel *dirModel = static_cast<KDirModel *>(sourceModel());
1545     const KFileItem item = dirModel->itemForIndex(dirModel->index(sourceRow, KDirModel::Name, sourceParent));
1546 
1547     if (m_usedByContainment && !m_screenMapper->sharedDesktops()) {
1548         const QUrl url = item.url();
1549         const int screen = m_screenMapper->screenForItem(url);
1550         // don't do anything if the folderview is not associated with a screen
1551         if (m_screenUsed && screen == -1) {
1552             // The item is not associated with a screen, probably because this is the first
1553             // time we see it or the folderview was previously used as a regular applet.
1554             // Associated with this folderview if the view is on the first available screen
1555             if (m_screen == m_screenMapper->firstAvailableScreen(resolvedUrl())) {
1556                 m_screenMapper->addMapping(url, m_screen, ScreenMapper::DelayedSignal);
1557             } else {
1558                 return false;
1559             }
1560         } else if (m_screen != screen) {
1561             // the item belongs to a different screen, filter it out
1562             return false;
1563         }
1564     }
1565 
1566     if (m_filterMode == NoFilter) {
1567         return true;
1568     }
1569 
1570     if (m_filterMode == FilterShowMatches) {
1571         return (matchPattern(item) && matchMimeType(item));
1572     } else {
1573         return !(matchPattern(item) && matchMimeType(item));
1574     }
1575 }
1576 
createActions()1577 void FolderModel::createActions()
1578 {
1579     KIO::FileUndoManager *manager = KIO::FileUndoManager::self();
1580 
1581     QAction *cut = KStandardAction::cut(this, &FolderModel::cut, this);
1582     QAction *copy = KStandardAction::copy(this, &FolderModel::copy, this);
1583 
1584     QAction *undo = KStandardAction::undo(manager, &KIO::FileUndoManager::undo, this);
1585     undo->setEnabled(manager->isUndoAvailable());
1586     undo->setShortcutContext(Qt::WidgetShortcut);
1587     connect(manager, SIGNAL(undoAvailable(bool)), undo, SLOT(setEnabled(bool)));
1588     connect(manager, &KIO::FileUndoManager::undoTextChanged, this, &FolderModel::undoTextChanged);
1589 
1590     QAction *paste = KStandardAction::paste(this, &FolderModel::paste, this);
1591     QAction *pasteTo = KStandardAction::paste(this, &FolderModel::pasteTo, this);
1592 
1593     QAction *refresh = new QAction(QIcon::fromTheme(QStringLiteral("view-refresh")), i18n("&Refresh View"), this);
1594     refresh->setShortcut(QKeySequence(QKeySequence::Refresh));
1595     connect(refresh, &QAction::triggered, this, &FolderModel::refresh);
1596 
1597     QAction *rename = KStandardAction::renameFile(this, &FolderModel::requestRename, this);
1598     QAction *trash = KStandardAction::moveToTrash(this, &FolderModel::moveSelectedToTrash, this);
1599     QAction *del = KStandardAction::deleteFile(this, &FolderModel::deleteSelected, this);
1600     RemoveAction *remove = new RemoveAction(&m_actionCollection, this);
1601 
1602     QAction *emptyTrash = new QAction(QIcon::fromTheme(QStringLiteral("trash-empty")), i18n("&Empty Trash"), this);
1603     connect(emptyTrash, &QAction::triggered, this, &FolderModel::emptyTrashBin);
1604 
1605     QAction *restoreFromTrash = new QAction(i18nc("Restore from trash", "Restore"), this);
1606     connect(restoreFromTrash, &QAction::triggered, this, &FolderModel::restoreSelectedFromTrash);
1607 
1608     QAction *actOpen = new QAction(QIcon::fromTheme(QStringLiteral("window-new")), i18n("&Open"), this);
1609     connect(actOpen, &QAction::triggered, this, &FolderModel::openSelected);
1610 
1611     m_actionCollection.addAction(QStringLiteral("open"), actOpen);
1612     m_actionCollection.addAction(QStringLiteral("cut"), cut);
1613     m_actionCollection.addAction(QStringLiteral("undo"), undo);
1614     m_actionCollection.addAction(QStringLiteral("copy"), copy);
1615     m_actionCollection.addAction(QStringLiteral("paste"), paste);
1616     m_actionCollection.addAction(QStringLiteral("pasteto"), pasteTo);
1617     m_actionCollection.addAction(QStringLiteral("refresh"), refresh);
1618     m_actionCollection.addAction(QStringLiteral("rename"), rename);
1619     m_actionCollection.addAction(QStringLiteral("remove"), remove);
1620     m_actionCollection.addAction(QStringLiteral("trash"), trash);
1621     m_actionCollection.addAction(QStringLiteral("del"), del);
1622     m_actionCollection.addAction(QStringLiteral("restoreFromTrash"), restoreFromTrash);
1623     m_actionCollection.addAction(QStringLiteral("emptyTrash"), emptyTrash);
1624 
1625     // The RemoveAction needs to be updated after adding all actions to the actionCollection
1626     remove->update();
1627 
1628     m_newMenu = new KNewFileMenu(&m_actionCollection, QStringLiteral("newMenu"), this);
1629     m_newMenu->setModal(false);
1630     connect(m_newMenu, &KNewFileMenu::directoryCreated, this, &FolderModel::newFileMenuItemCreated);
1631     connect(m_newMenu, &KNewFileMenu::fileCreated, this, &FolderModel::newFileMenuItemCreated);
1632 
1633     m_copyToMenu = new KFileCopyToMenu(nullptr);
1634 }
1635 
action(const QString & name) const1636 QAction *FolderModel::action(const QString &name) const
1637 {
1638     return m_actionCollection.action(name);
1639 }
1640 
newMenu() const1641 QObject *FolderModel::newMenu() const
1642 {
1643     return m_newMenu->menu();
1644 }
1645 
updateActions()1646 void FolderModel::updateActions()
1647 {
1648     const QModelIndexList indexes = m_selectionModel->selectedIndexes();
1649 
1650     KFileItemList items;
1651     QList<QUrl> urls;
1652     bool hasRemoteFiles = false;
1653     bool isTrashLink = false;
1654     const bool isTrash = (resolvedUrl().scheme() == QLatin1String("trash"));
1655 
1656     if (indexes.isEmpty()) {
1657         items << rootItem();
1658     } else {
1659         items.reserve(indexes.count());
1660         urls.reserve(indexes.count());
1661         for (const QModelIndex &index : indexes) {
1662             KFileItem item = itemForIndex(index);
1663             if (!item.isNull()) {
1664                 hasRemoteFiles |= item.localPath().isEmpty();
1665                 items.append(item);
1666                 urls.append(item.url());
1667             }
1668         }
1669     }
1670 
1671     KFileItemListProperties itemProperties(items);
1672     // Check if we're showing the menu for the trash link
1673     if (items.count() == 1 && items.at(0).isDesktopFile()) {
1674         KDesktopFile file(items.at(0).localPath());
1675         if (file.hasLinkType() && file.readUrl() == QLatin1String("trash:/")) {
1676             isTrashLink = true;
1677         }
1678     }
1679 
1680     if (m_newMenu) {
1681         m_newMenu->checkUpToDate();
1682         m_newMenu->setPopupFiles(QList<QUrl>() << m_dirModel->dirLister()->url());
1683         // we need to set here as well, when the menu is shown via AppletInterface::eventFilter
1684         m_menuPosition = QCursor::pos();
1685 
1686         if (QAction *newMenuAction = m_actionCollection.action(QStringLiteral("newMenu"))) {
1687             newMenuAction->setEnabled(itemProperties.supportsWriting());
1688             newMenuAction->setVisible(!isTrash);
1689         }
1690     }
1691 
1692     if (QAction *emptyTrash = m_actionCollection.action(QStringLiteral("emptyTrash"))) {
1693         if (isTrash || isTrashLink) {
1694             emptyTrash->setVisible(true);
1695             emptyTrash->setEnabled(!isTrashEmpty());
1696         } else {
1697             emptyTrash->setVisible(false);
1698         }
1699     }
1700 
1701     if (QAction *restoreFromTrash = m_actionCollection.action(QStringLiteral("restoreFromTrash"))) {
1702         restoreFromTrash->setVisible(isTrash);
1703     }
1704 
1705     if (QAction *remove = m_actionCollection.action(QStringLiteral("remove"))) {
1706         remove->setVisible(!hasRemoteFiles && itemProperties.supportsMoving() && itemProperties.supportsDeleting());
1707     }
1708 
1709     if (QAction *cut = m_actionCollection.action(QStringLiteral("cut"))) {
1710         cut->setEnabled(itemProperties.supportsDeleting());
1711         cut->setVisible(!isTrash);
1712     }
1713 
1714     if (QAction *paste = m_actionCollection.action(QStringLiteral("paste"))) {
1715         bool enable = false;
1716 
1717         const QString pasteText = KIO::pasteActionText(QApplication::clipboard()->mimeData(), &enable, rootItem());
1718 
1719         if (enable) {
1720             paste->setText(pasteText);
1721             paste->setEnabled(true);
1722         } else {
1723             paste->setText(i18n("&Paste"));
1724             paste->setEnabled(false);
1725         }
1726 
1727         if (QAction *pasteTo = m_actionCollection.action(QStringLiteral("pasteto"))) {
1728             pasteTo->setVisible(itemProperties.isDirectory() && itemProperties.supportsWriting());
1729             pasteTo->setEnabled(paste->isEnabled());
1730             pasteTo->setText(paste->text());
1731         }
1732     }
1733 
1734     if (QAction *rename = m_actionCollection.action(QStringLiteral("rename"))) {
1735         rename->setEnabled(itemProperties.supportsMoving());
1736         rename->setVisible(!isTrash);
1737     }
1738 }
1739 
openContextMenu(QQuickItem * visualParent,Qt::KeyboardModifiers modifiers)1740 void FolderModel::openContextMenu(QQuickItem *visualParent, Qt::KeyboardModifiers modifiers)
1741 {
1742     if (m_usedByContainment && !KAuthorized::authorize(QStringLiteral("action/kdesktop_rmb"))) {
1743         return;
1744     }
1745 
1746     updateActions();
1747 
1748     const QModelIndexList indexes = m_selectionModel->selectedIndexes();
1749 
1750     QMenu *menu = new QMenu();
1751     if (!m_fileItemActions) {
1752         m_fileItemActions = new KFileItemActions(this);
1753         m_fileItemActions->setParentWidget(QApplication::desktop());
1754     }
1755 
1756     if (indexes.isEmpty()) {
1757         menu->addAction(m_actionCollection.action(QStringLiteral("newMenu")));
1758         menu->addSeparator();
1759         menu->addAction(m_actionCollection.action(QStringLiteral("paste")));
1760         menu->addAction(m_actionCollection.action(QStringLiteral("undo")));
1761         menu->addAction(m_actionCollection.action(QStringLiteral("refresh")));
1762         menu->addAction(m_actionCollection.action(QStringLiteral("emptyTrash")));
1763         menu->addSeparator();
1764 
1765         KFileItemListProperties itemProperties(KFileItemList() << rootItem());
1766         m_fileItemActions->setItemListProperties(itemProperties);
1767 
1768         m_fileItemActions->insertOpenWithActionsTo(nullptr, menu, QStringList());
1769     } else {
1770         KFileItemList items;
1771         QList<QUrl> urls;
1772 
1773         items.reserve(indexes.count());
1774         urls.reserve(indexes.count());
1775         for (const QModelIndex &index : indexes) {
1776             KFileItem item = itemForIndex(index);
1777             if (!item.isNull()) {
1778                 items.append(item);
1779                 urls.append(item.url());
1780             }
1781         }
1782 
1783         KFileItemListProperties itemProperties(items);
1784 
1785         // Start adding the actions:
1786         // "Open" and "Open with" actions
1787         m_fileItemActions->setItemListProperties(itemProperties);
1788         m_fileItemActions->addOpenWithActionsTo(menu);
1789         menu->addSeparator();
1790         menu->addAction(m_actionCollection.action(QStringLiteral("cut")));
1791         menu->addAction(m_actionCollection.action(QStringLiteral("copy")));
1792         if (urls.length() == 1 && items.first().isDir()) {
1793             menu->addAction(m_actionCollection.action(QStringLiteral("pasteto")));
1794         } else {
1795             menu->addAction(m_actionCollection.action(QStringLiteral("paste")));
1796         }
1797 
1798         menu->addAction(m_actionCollection.action(QStringLiteral("rename")));
1799         menu->addSeparator();
1800         menu->addAction(m_actionCollection.action(QStringLiteral("restoreFromTrash")));
1801 
1802         if (isDeleteCommandShown()) {
1803             QAction *trashAction = m_actionCollection.action(QStringLiteral("trash"));
1804             QAction *deleteAction = m_actionCollection.action(QStringLiteral("del"));
1805             menu->addAction(trashAction);
1806             menu->addAction(deleteAction);
1807         } else {
1808             if (RemoveAction *removeAction = qobject_cast<RemoveAction *>(m_actionCollection.action(QStringLiteral("remove")))) {
1809                 removeAction->update();
1810                 menu->addAction(removeAction);
1811 
1812                 // Used to monitor Shift modifier usage while the menu is open, to
1813                 // swap the Trash and Delete actions.
1814                 menu->installEventFilter(removeAction);
1815                 QCoreApplication::instance()->installEventFilter(removeAction);
1816             }
1817         }
1818 
1819         menu->addAction(m_actionCollection.action(QStringLiteral("emptyTrash")));
1820 
1821         menu->addSeparator();
1822 
1823         m_fileItemActions->addActionsTo(menu);
1824 
1825         // Copy To, Move To
1826         KSharedConfig::Ptr dolphin = KSharedConfig::openConfig(QStringLiteral("dolphinrc"));
1827         if (KConfigGroup(dolphin, "General").readEntry("ShowCopyMoveMenu", false)) {
1828             m_copyToMenu->setUrls(urls);
1829             m_copyToMenu->setReadOnly(!itemProperties.supportsMoving());
1830             m_copyToMenu->addActionsTo(menu);
1831             menu->addSeparator();
1832         }
1833 
1834         // Properties
1835         if (KPropertiesDialog::canDisplay(items)) {
1836             menu->addSeparator();
1837             QAction *act = new QAction(QIcon::fromTheme(QStringLiteral("document-properties")), i18n("&Properties"), menu);
1838             act->setShortcuts({Qt::ALT | Qt::Key_Return, Qt::ALT | Qt::Key_Enter});
1839             QObject::connect(act, &QAction::triggered, this, &FolderModel::openPropertiesDialog);
1840             menu->addAction(act);
1841         }
1842     }
1843 
1844     menu->setAttribute(Qt::WA_TranslucentBackground);
1845     menu->winId(); // force surface creation before ensurePolish call in menu::Popup which happens before show
1846 
1847     if (visualParent && menu->windowHandle()) {
1848         menu->windowHandle()->setTransientParent(visualParent->window());
1849     }
1850 
1851     menu->popup(m_menuPosition);
1852     connect(menu, &QMenu::aboutToHide, [menu]() {
1853         menu->deleteLater();
1854     });
1855 }
1856 
openPropertiesDialog()1857 void FolderModel::openPropertiesDialog()
1858 {
1859     const QModelIndexList indexes = m_selectionModel->selectedIndexes();
1860     if (indexes.isEmpty()) {
1861         return;
1862     }
1863 
1864     KFileItemList items;
1865     items.reserve(indexes.count());
1866     for (const QModelIndex &index : indexes) {
1867         KFileItem item = itemForIndex(index);
1868         if (!item.isNull()) {
1869             items.append(item);
1870         }
1871     }
1872 
1873     if (!KPropertiesDialog::canDisplay(items)) {
1874         return;
1875     }
1876 
1877     KPropertiesDialog::showDialog(items, nullptr, false /*non modal*/);
1878 }
1879 
linkHere(const QUrl & sourceUrl)1880 void FolderModel::linkHere(const QUrl &sourceUrl)
1881 {
1882     KIO::CopyJob *job = KIO::link(sourceUrl, m_dirModel->dirLister()->url(), KIO::HideProgressInfo);
1883     KIO::FileUndoManager::self()->recordCopyJob(job);
1884 }
1885 
selectedUrls() const1886 QList<QUrl> FolderModel::selectedUrls() const
1887 {
1888     const auto indexes = m_selectionModel->selectedIndexes();
1889 
1890     QList<QUrl> urls;
1891     urls.reserve(indexes.count());
1892 
1893     for (const QModelIndex &index : indexes) {
1894         urls.append(itemForIndex(index).url());
1895     }
1896 
1897     return urls;
1898 }
1899 
copy()1900 void FolderModel::copy()
1901 {
1902     if (!m_selectionModel->hasSelection()) {
1903         return;
1904     }
1905 
1906     if (QAction *action = m_actionCollection.action(QStringLiteral("copy"))) {
1907         if (!action->isEnabled()) {
1908             return;
1909         }
1910     }
1911 
1912     QMimeData *mimeData = QSortFilterProxyModel::mimeData(m_selectionModel->selectedIndexes());
1913     QApplication::clipboard()->setMimeData(mimeData);
1914 }
1915 
cut()1916 void FolderModel::cut()
1917 {
1918     if (!m_selectionModel->hasSelection()) {
1919         return;
1920     }
1921 
1922     if (QAction *action = m_actionCollection.action(QStringLiteral("cut"))) {
1923         if (!action->isEnabled()) {
1924             return;
1925         }
1926     }
1927 
1928     QMimeData *mimeData = QSortFilterProxyModel::mimeData(m_selectionModel->selectedIndexes());
1929     KIO::setClipboardDataCut(mimeData, true);
1930     QApplication::clipboard()->setMimeData(mimeData);
1931 }
1932 
paste()1933 void FolderModel::paste()
1934 {
1935     if (QAction *action = m_actionCollection.action(QStringLiteral("paste"))) {
1936         if (!action->isEnabled()) {
1937             return;
1938         }
1939     }
1940 
1941     KIO::paste(QApplication::clipboard()->mimeData(), m_dirModel->dirLister()->url());
1942 }
1943 
pasteTo()1944 void FolderModel::pasteTo()
1945 {
1946     const QList<QUrl> urls = selectedUrls();
1947     Q_ASSERT(urls.count() == 1);
1948     KIO::paste(QApplication::clipboard()->mimeData(), urls.first());
1949 }
1950 
refresh()1951 void FolderModel::refresh()
1952 {
1953     m_errorString.clear();
1954     Q_EMIT errorStringChanged();
1955 
1956     m_dirModel->dirLister()->updateDirectory(m_dirModel->dirLister()->url());
1957 }
1958 
appletInterface() const1959 QObject *FolderModel::appletInterface() const
1960 {
1961     return m_appletInterface;
1962 }
1963 
setAppletInterface(QObject * appletInterface)1964 void FolderModel::setAppletInterface(QObject *appletInterface)
1965 {
1966     if (m_appletInterface != appletInterface) {
1967         Q_ASSERT(!m_appletInterface);
1968 
1969         m_appletInterface = appletInterface;
1970 
1971         if (appletInterface) {
1972             Plasma::Applet *applet = appletInterface->property("_plasma_applet").value<Plasma::Applet *>();
1973 
1974             if (applet) {
1975                 Plasma::Containment *containment = applet->containment();
1976 
1977                 if (containment) {
1978                     Plasma::Corona *corona = containment->corona();
1979 
1980                     if (corona) {
1981                         m_screenMapper->setCorona(corona);
1982                     }
1983                     setScreen(containment->screen());
1984                     connect(containment, &Plasma::Containment::screenChanged, this, &FolderModel::setScreen);
1985                 }
1986             }
1987         }
1988 
1989         Q_EMIT appletInterfaceChanged();
1990     }
1991 }
1992 
moveSelectedToTrash()1993 void FolderModel::moveSelectedToTrash()
1994 {
1995     if (!m_selectionModel->hasSelection()) {
1996         return;
1997     }
1998 
1999     if (!isDeleteCommandShown()) {
2000         if (RemoveAction *action = qobject_cast<RemoveAction *>(m_actionCollection.action(QStringLiteral("remove")))) {
2001             if (action->proxyAction() != m_actionCollection.action(QStringLiteral("trash"))) {
2002                 return;
2003             }
2004         }
2005     }
2006 
2007     if (QAction *action = m_actionCollection.action(QStringLiteral("trash"))) {
2008         if (!action->isEnabled()) {
2009             return;
2010         }
2011     }
2012 
2013     const QList<QUrl> urls = selectedUrls();
2014     KIO::JobUiDelegate uiDelegate;
2015 
2016     if (uiDelegate.askDeleteConfirmation(urls, KIO::JobUiDelegate::Trash, KIO::JobUiDelegate::DefaultConfirmation)) {
2017         KIO::Job *job = KIO::trash(urls);
2018         job->uiDelegate()->setAutoErrorHandlingEnabled(true);
2019         KIO::FileUndoManager::self()->recordJob(KIO::FileUndoManager::Trash, urls, QUrl(QStringLiteral("trash:/")), job);
2020     }
2021 }
2022 
deleteSelected()2023 void FolderModel::deleteSelected()
2024 {
2025     if (!m_selectionModel->hasSelection()) {
2026         return;
2027     }
2028 
2029     if (QAction *action = m_actionCollection.action(QStringLiteral("del"))) {
2030         if (!action->isEnabled()) {
2031             return;
2032         }
2033     }
2034 
2035     const QList<QUrl> urls = selectedUrls();
2036     KIO::JobUiDelegate uiDelegate;
2037 
2038     if (uiDelegate.askDeleteConfirmation(urls, KIO::JobUiDelegate::Delete, KIO::JobUiDelegate::DefaultConfirmation)) {
2039         KIO::Job *job = KIO::del(urls);
2040         job->uiDelegate()->setAutoErrorHandlingEnabled(true);
2041     }
2042 }
2043 
openSelected()2044 void FolderModel::openSelected()
2045 {
2046     if (!m_selectionModel->hasSelection()) {
2047         return;
2048     }
2049 
2050     const QList<QUrl> urls = selectedUrls();
2051     for (const QUrl &url : urls) {
2052         (void)new KRun(url, nullptr);
2053     }
2054 }
2055 
undo()2056 void FolderModel::undo()
2057 {
2058     if (QAction *action = m_actionCollection.action(QStringLiteral("undo"))) {
2059         // trigger() doesn't check enabled and would crash if invoked nonetheless.
2060         if (action->isEnabled()) {
2061             action->trigger();
2062         }
2063     }
2064 }
2065 
emptyTrashBin()2066 void FolderModel::emptyTrashBin()
2067 {
2068     KIO::JobUiDelegate uiDelegate;
2069     uiDelegate.setWindow(QApplication::desktop());
2070 
2071     if (uiDelegate.askDeleteConfirmation(QList<QUrl>(), KIO::JobUiDelegate::EmptyTrash, KIO::JobUiDelegate::DefaultConfirmation)) {
2072         KIO::Job *job = KIO::emptyTrash();
2073         job->uiDelegate()->setAutoErrorHandlingEnabled(true);
2074     }
2075 }
2076 
restoreSelectedFromTrash()2077 void FolderModel::restoreSelectedFromTrash()
2078 {
2079     if (!m_selectionModel->hasSelection()) {
2080         return;
2081     }
2082 
2083     const auto &urls = selectedUrls();
2084 
2085     KIO::RestoreJob *job = KIO::restoreFromTrash(urls);
2086     job->uiDelegate()->setAutoErrorHandlingEnabled(true);
2087 }
2088 
isTrashEmpty()2089 bool FolderModel::isTrashEmpty()
2090 {
2091     KConfig trashConfig(QStringLiteral("trashrc"), KConfig::SimpleConfig);
2092     return trashConfig.group("Status").readEntry("Empty", true);
2093 }
2094 
undoTextChanged(const QString & text)2095 void FolderModel::undoTextChanged(const QString &text)
2096 {
2097     if (QAction *action = m_actionCollection.action(QStringLiteral("undo"))) {
2098         action->setText(text);
2099     }
2100 }
2101 
createFolder()2102 void FolderModel::createFolder()
2103 {
2104     m_newMenu->setPopupFiles(QList<QUrl>() << m_dirModel->dirLister()->url());
2105     m_newMenu->createDirectory();
2106 }
2107 
isDeleteCommandShown()2108 bool FolderModel::isDeleteCommandShown()
2109 {
2110     KConfigGroup cg(KSharedConfig::openConfig(), "KDE");
2111     return cg.readEntry("ShowDeleteCommand", false);
2112 }
2113