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