File indexing completed on 2025-01-26 05:06:21

0001 /*
0002     SPDX-FileCopyrightText: 2006 David Faure <faure@kde.org>
0003     SPDX-FileCopyrightText: 2008 Fredrik Höglund <fredrik@kde.org>
0004     SPDX-FileCopyrightText: 2008 Rafael Fernández López <ereslibre@kde.org>
0005     SPDX-FileCopyrightText: 2011 Marco Martin <mart@kde.org>
0006     SPDX-FileCopyrightText: 2014 Eike Hein <hein@kde.org>
0007 
0008     SPDX-License-Identifier: GPL-2.0-or-later
0009 */
0010 
0011 #include "foldermodel.h"
0012 #include "itemviewadapter.h"
0013 #include "positioner.h"
0014 #include "removeaction.h"
0015 #include "screenmapper.h"
0016 
0017 #include <QApplication>
0018 #include <QClipboard>
0019 #include <QCollator>
0020 #include <QDrag>
0021 #include <QImage>
0022 #include <QItemSelectionModel>
0023 #include <QLoggingCategory>
0024 #include <QMenu>
0025 #include <QMimeData>
0026 #include <QMimeDatabase>
0027 #include <QPainter>
0028 #include <QPixmap>
0029 #include <QQuickItem>
0030 #include <QQuickWindow>
0031 #include <QScreen>
0032 #include <QTimer>
0033 #include <qplatformdefs.h>
0034 
0035 #include <KAuthorized>
0036 #include <KConfigGroup>
0037 #include <KCoreDirLister>
0038 #include <KDesktopFile>
0039 #include <KDirModel>
0040 #include <KDirWatch>
0041 #include <KFileCopyToMenu>
0042 #include <KFileItemActions>
0043 #include <KFileItemListProperties>
0044 #include <KIO/CopyJob>
0045 #include <KIO/DeleteJob>
0046 #include <KIO/DeleteOrTrashJob>
0047 #include <KIO/DropJob>
0048 #include <KIO/EmptyTrashJob>
0049 #include <KIO/FileUndoManager>
0050 #include <KIO/JobUiDelegate>
0051 #include <KIO/JobUiDelegateFactory>
0052 #include <KIO/OpenUrlJob>
0053 #include <KIO/Paste>
0054 #include <KIO/PasteJob>
0055 #include <KIO/PreviewJob>
0056 #include <KIO/RestoreJob>
0057 #include <KIO/StatJob>
0058 #include <KLocalizedString>
0059 #include <KNotificationJobUiDelegate>
0060 #include <KPropertiesDialog>
0061 #include <KProtocolInfo>
0062 #include <KSharedConfig>
0063 #include <KShell>
0064 #include <KStringHandler>
0065 
0066 #include <Plasma/Applet>
0067 #include <Plasma/Containment>
0068 #include <Plasma/Corona>
0069 #include <PlasmaActivities/Consumer>
0070 
0071 #include <chrono>
0072 #include <sys/stat.h>
0073 #include <sys/types.h>
0074 #include <unistd.h>
0075 
0076 using namespace std::chrono_literals;
0077 
0078 Q_LOGGING_CATEGORY(FOLDERMODEL, "plasma.containments.desktop.folder.foldermodel")
0079 
0080 class DragTrackerSingleton
0081 {
0082 public:
0083     DragTracker self;
0084 };
0085 
0086 Q_GLOBAL_STATIC(DragTrackerSingleton, privateDragTrackerSelf)
0087 
0088 DragTracker::DragTracker(QObject *parent)
0089     : QObject(parent)
0090 {
0091 }
0092 
0093 DragTracker::~DragTracker()
0094 {
0095 }
0096 
0097 bool DragTracker::isDragInProgress() const
0098 {
0099     return m_dragInProgress;
0100 }
0101 
0102 void DragTracker::setDragInProgress(FolderModel *dragOwner, bool dragInProgress)
0103 {
0104     if (dragInProgress == m_dragInProgress) {
0105         return;
0106     }
0107 
0108     m_dragInProgress = dragInProgress;
0109     if (dragInProgress) {
0110         m_dragOwner = dragOwner;
0111     } else {
0112         m_dragOwner.clear();
0113     }
0114 
0115     Q_EMIT dragInProgressChanged(m_dragInProgress);
0116 }
0117 
0118 FolderModel *DragTracker::dragOwner()
0119 {
0120     return m_dragOwner;
0121 }
0122 
0123 DragTracker *DragTracker::self()
0124 {
0125     return &privateDragTrackerSelf()->self;
0126 }
0127 
0128 DirLister::DirLister(QObject *parent)
0129     : KDirLister(parent)
0130 {
0131     connect(this, &KCoreDirLister::jobError, this, &DirLister::handleJobError);
0132 }
0133 
0134 DirLister::~DirLister()
0135 {
0136 }
0137 
0138 void DirLister::handleJobError(KIO::Job *job)
0139 {
0140     if (!autoErrorHandlingEnabled()) {
0141         Q_EMIT error(job->errorString());
0142         return;
0143     }
0144 }
0145 
0146 FolderModel::FolderModel(QObject *parent)
0147     : QSortFilterProxyModel(parent)
0148     , m_dirWatch(nullptr)
0149     , m_urlChangedWhileDragging(false)
0150     , m_dropTargetPositionsCleanup(new QTimer(this))
0151     , m_previewGenerator(nullptr)
0152     , m_viewAdapter(nullptr)
0153     , m_actionCollection(this)
0154     , m_newMenu(nullptr)
0155     , m_fileItemActions(nullptr)
0156     , m_usedByContainment(false)
0157     , m_locked(true)
0158     , m_sortMode(0)
0159     , m_sortDesc(false)
0160     , m_sortDirsFirst(true)
0161     , m_parseDesktopFiles(false)
0162     , m_previews(false)
0163     , m_filterMode(NoFilter)
0164     , m_filterPatternMatchAll(true)
0165     , m_screenUsed(false)
0166     , m_screenMapper(ScreenMapper::instance())
0167     , m_complete(false)
0168     , m_currentActivity(KActivities::Consumer().currentActivity())
0169     , m_showHiddenFiles(false)
0170 {
0171     connect(DragTracker::self(), &DragTracker::dragInProgressChanged, this, &FolderModel::draggingChanged);
0172     connect(DragTracker::self(), &DragTracker::dragInProgressChanged, this, &FolderModel::dragInProgressAnywhereChanged);
0173     // needed to pass the job around with qml
0174     qmlRegisterAnonymousType<KIO::DropJob>("org.kde.private.desktopcontainment.folder", 0);
0175     DirLister *dirLister = new DirLister(this);
0176     dirLister->setDelayedMimeTypes(true);
0177     dirLister->setAutoErrorHandlingEnabled(false);
0178     connect(dirLister, &DirLister::error, this, &FolderModel::dirListFailed);
0179     connect(dirLister, &KCoreDirLister::itemsDeleted, this, &FolderModel::evictFromIsDirCache);
0180 
0181     connect(dirLister, &KCoreDirLister::started, this, std::bind(&FolderModel::setStatus, this, Status::Listing));
0182 
0183     void (KCoreDirLister::*myCompletedSignal)() = &KCoreDirLister::completed;
0184     QObject::connect(dirLister, myCompletedSignal, this, [this] {
0185         setStatus(Status::Ready);
0186         Q_EMIT listingCompleted();
0187     });
0188 
0189     void (KCoreDirLister::*myCanceledSignal)() = &KCoreDirLister::canceled;
0190     QObject::connect(dirLister, myCanceledSignal, this, [this] {
0191         setStatus(Status::Canceled);
0192         Q_EMIT listingCanceled();
0193     });
0194 
0195     m_dirModel = new KDirModel(this);
0196     m_dirModel->setDirLister(dirLister);
0197     m_dirModel->setDropsAllowed(KDirModel::DropOnDirectory | KDirModel::DropOnLocalExecutable);
0198     m_dirModel->dirLister()->setAutoUpdate(true);
0199 
0200     // If we have dropped items queued for moving, go unsorted now.
0201     connect(this, &QAbstractItemModel::rowsAboutToBeInserted, this, [this]() {
0202         if (!m_dropTargetPositions.isEmpty()) {
0203             setSortMode(-1);
0204         }
0205     });
0206 
0207     // Position dropped items at the desired target position.
0208     connect(this, &QAbstractItemModel::rowsInserted, this, [this](const QModelIndex &parent, int first, int last) {
0209         for (int i = first; i <= last; ++i) {
0210             const auto idx = index(i, 0, parent);
0211             const auto url = itemForIndex(idx).url();
0212             auto it = m_dropTargetPositions.find(url.fileName());
0213             if (it != m_dropTargetPositions.end()) {
0214                 const auto pos = it.value();
0215                 m_dropTargetPositions.erase(it);
0216                 Q_EMIT move(pos.x(), pos.y(), {url});
0217             }
0218         }
0219     });
0220 
0221     /*
0222      * Dropped files may not actually show up as new files, e.g. when we overwrite
0223      * an existing file. Or files that fail to be listed by the dirLister, or...
0224      * To ensure we don't grow the map indefinitely, clean it up periodically.
0225      * The cleanup timer is (re)started whenever we modify the map. We use a quite
0226      * high interval of 10s. This should ensure, that we don't accidentally wipe
0227      * the mapping when we actually still want to use it. Since the time between
0228      * adding an entry in the map and it showing up in the model should be
0229      * small, this should rarely, if ever happen.
0230      */
0231     m_dropTargetPositionsCleanup->setInterval(10s);
0232     m_dropTargetPositionsCleanup->setSingleShot(true);
0233     connect(m_dropTargetPositionsCleanup, &QTimer::timeout, this, [this]() {
0234         if (!m_dropTargetPositions.isEmpty()) {
0235             qCDebug(FOLDERMODEL) << "clearing drop target positions after timeout:" << m_dropTargetPositions;
0236             m_dropTargetPositions.clear();
0237         }
0238     });
0239 
0240     m_selectionModel = new QItemSelectionModel(this, this);
0241     connect(m_selectionModel, &QItemSelectionModel::selectionChanged, this, &FolderModel::changeSelection);
0242     connect(m_selectionModel, &QItemSelectionModel::selectionChanged, this, &FolderModel::selectionChanged);
0243 
0244     setSourceModel(m_dirModel);
0245 
0246     setSortLocaleAware(true);
0247     setFilterCaseSensitivity(Qt::CaseInsensitive);
0248     setDynamicSortFilter(true);
0249 
0250     sort(m_sortMode, m_sortDesc ? Qt::DescendingOrder : Qt::AscendingOrder);
0251 
0252     createActions();
0253 }
0254 
0255 FolderModel::~FolderModel()
0256 {
0257     if (m_usedByContainment) {
0258         // disconnect so we don't handle signals from the screen mapper when
0259         // removeScreen is called
0260         m_screenMapper->disconnect(this);
0261         m_screenMapper->removeScreen(m_screen, m_currentActivity, resolvedUrl());
0262     }
0263 }
0264 
0265 QHash<int, QByteArray> FolderModel::roleNames() const
0266 {
0267     return staticRoleNames();
0268 }
0269 
0270 QHash<int, QByteArray> FolderModel::staticRoleNames()
0271 {
0272     QHash<int, QByteArray> roleNames;
0273     roleNames[Qt::DisplayRole] = "display";
0274     roleNames[Qt::DecorationRole] = "decoration";
0275     roleNames[BlankRole] = "blank";
0276     roleNames[SelectedRole] = "selected";
0277     roleNames[IsDirRole] = "isDir";
0278     roleNames[IsLinkRole] = "isLink";
0279     roleNames[IsHiddenRole] = "isHidden";
0280     roleNames[UrlRole] = "url";
0281     roleNames[LinkDestinationUrl] = "linkDestinationUrl";
0282     roleNames[SizeRole] = "size";
0283     roleNames[TypeRole] = "type";
0284     roleNames[FileNameWrappedRole] = "displayWrapped";
0285 
0286     return roleNames;
0287 }
0288 
0289 QPoint FolderModel::localMenuPosition() const
0290 {
0291     QScreen *screen = nullptr;
0292     for (auto *s : qApp->screens()) {
0293         if (s->geometry().contains(m_menuPosition)) {
0294             screen = s;
0295             break;
0296         }
0297     }
0298     if (screen) {
0299         return m_menuPosition - screen->geometry().topLeft();
0300     }
0301     return m_menuPosition;
0302 }
0303 
0304 void FolderModel::classBegin()
0305 {
0306 }
0307 
0308 void FolderModel::componentComplete()
0309 {
0310     m_complete = true;
0311     invalidate();
0312 }
0313 
0314 void FolderModel::invalidateIfComplete()
0315 {
0316     if (!m_complete) {
0317         return;
0318     }
0319 
0320     invalidate();
0321 }
0322 
0323 void FolderModel::invalidateFilterIfComplete()
0324 {
0325     if (!m_complete) {
0326         return;
0327     }
0328 
0329     invalidateFilter();
0330 }
0331 
0332 void FolderModel::newFileMenuItemCreated(const QUrl &url)
0333 {
0334     if (m_usedByContainment && !m_screenMapper->sharedDesktops()) {
0335         m_screenMapper->addMapping(url, m_screen, m_currentActivity, ScreenMapper::DelayedSignal);
0336         m_dropTargetPositions.insert(url.fileName(), localMenuPosition());
0337         m_menuPosition = {};
0338         m_dropTargetPositionsCleanup->start();
0339     }
0340 }
0341 
0342 QString FolderModel::url() const
0343 {
0344     return m_url;
0345 }
0346 
0347 void FolderModel::setUrl(const QString &url)
0348 {
0349     const QUrl &resolvedNewUrl = resolve(url);
0350 
0351     if (url == m_url) {
0352         m_dirModel->dirLister()->updateDirectory(resolvedNewUrl);
0353         return;
0354     }
0355 
0356     const auto oldUrl = resolvedUrl();
0357 
0358     beginResetModel();
0359     m_url = url;
0360     m_isDirCache.clear();
0361     m_dirModel->dirLister()->openUrl(resolvedNewUrl);
0362     clearDragImages();
0363     m_dragIndexes.clear();
0364     endResetModel();
0365 
0366     Q_EMIT urlChanged();
0367     Q_EMIT resolvedUrlChanged();
0368 
0369     m_errorString.clear();
0370     Q_EMIT errorStringChanged();
0371 
0372     if (m_dirWatch) {
0373         delete m_dirWatch;
0374         m_dirWatch = nullptr;
0375     }
0376 
0377     if (resolvedNewUrl.isValid()) {
0378         m_dirWatch = new KDirWatch(this);
0379         connect(m_dirWatch, &KDirWatch::created, this, &FolderModel::iconNameChanged);
0380         connect(m_dirWatch, &KDirWatch::dirty, this, &FolderModel::iconNameChanged);
0381         m_dirWatch->addFile(resolvedNewUrl.toLocalFile() + QStringLiteral("/.directory"));
0382     }
0383 
0384     if (dragging()) {
0385         m_urlChangedWhileDragging = true;
0386     }
0387 
0388     Q_EMIT iconNameChanged();
0389 
0390     if (m_usedByContainment && !m_screenMapper->sharedDesktops()) {
0391         m_screenMapper->removeScreen(m_screen, m_currentActivity, oldUrl);
0392         m_screenMapper->addScreen(m_screen, m_currentActivity, resolvedUrl());
0393     }
0394 }
0395 
0396 QUrl FolderModel::resolvedUrl() const
0397 {
0398     return m_dirModel->dirLister()->url();
0399 }
0400 
0401 QUrl FolderModel::resolve(const QString &url)
0402 {
0403     QUrl resolvedUrl;
0404 
0405     if (url.startsWith(QLatin1Char('~'))) {
0406         resolvedUrl = QUrl::fromLocalFile(KShell::tildeExpand(url));
0407     } else {
0408         resolvedUrl = QUrl::fromUserInput(url);
0409     }
0410 
0411     return resolvedUrl;
0412 }
0413 
0414 QString FolderModel::iconName() const
0415 {
0416     const KFileItem rootItem(m_dirModel->dirLister()->url());
0417 
0418     if (!rootItem.isFinalIconKnown()) {
0419         rootItem.determineMimeType();
0420     }
0421 
0422     return rootItem.iconName();
0423 }
0424 
0425 FolderModel::Status FolderModel::status() const
0426 {
0427     return m_status;
0428 }
0429 
0430 void FolderModel::setStatus(Status status)
0431 {
0432     if (m_status != status) {
0433         m_status = status;
0434         Q_EMIT statusChanged();
0435     }
0436 }
0437 
0438 QString FolderModel::errorString() const
0439 {
0440     return m_errorString;
0441 }
0442 
0443 bool FolderModel::dragging() const
0444 {
0445     return DragTracker::self()->isDragInProgress() && DragTracker::self()->dragOwner() == this;
0446 }
0447 
0448 bool FolderModel::isDragInProgressAnywhere() const
0449 {
0450     return DragTracker::self()->isDragInProgress();
0451 }
0452 
0453 bool FolderModel::usedByContainment() const
0454 {
0455     return m_usedByContainment;
0456 }
0457 
0458 void FolderModel::setUsedByContainment(bool used)
0459 {
0460     if (m_usedByContainment != used) {
0461         m_usedByContainment = used;
0462 
0463         QAction *action = m_actionCollection.action(QStringLiteral("refresh"));
0464 
0465         if (action) {
0466             action->setText(m_usedByContainment ? i18n("&Refresh Desktop") : i18n("&Refresh View"));
0467             action->setIcon(m_usedByContainment ? QIcon::fromTheme(QStringLiteral("user-desktop")) : QIcon::fromTheme(QStringLiteral("view-refresh")));
0468         }
0469 
0470         m_screenMapper->disconnect(this);
0471         connect(m_screenMapper, &ScreenMapper::screensChanged, this, &FolderModel::invalidateFilterIfComplete);
0472         connect(m_screenMapper, &ScreenMapper::screenMappingChanged, this, &FolderModel::invalidateFilterIfComplete);
0473 
0474         Q_EMIT usedByContainmentChanged();
0475     }
0476 }
0477 
0478 bool FolderModel::locked() const
0479 {
0480     return m_locked;
0481 }
0482 
0483 void FolderModel::setLocked(bool locked)
0484 {
0485     if (m_locked != locked) {
0486         m_locked = locked;
0487 
0488         Q_EMIT lockedChanged();
0489     }
0490 }
0491 
0492 void FolderModel::dirListFailed(const QString &error)
0493 {
0494     m_errorString = error;
0495     Q_EMIT errorStringChanged();
0496 }
0497 
0498 int FolderModel::sortMode() const
0499 {
0500     return m_sortMode;
0501 }
0502 
0503 void FolderModel::setSortMode(int mode)
0504 {
0505     if (m_sortMode != mode) {
0506         m_sortMode = mode;
0507 
0508         if (mode == -1 /* Unsorted */) {
0509             setDynamicSortFilter(false);
0510         } else {
0511             invalidateIfComplete();
0512             sort(m_sortMode, m_sortDesc ? Qt::DescendingOrder : Qt::AscendingOrder);
0513             setDynamicSortFilter(true);
0514         }
0515 
0516         Q_EMIT sortModeChanged();
0517     }
0518 }
0519 
0520 bool FolderModel::sortDesc() const
0521 {
0522     return m_sortDesc;
0523 }
0524 
0525 void FolderModel::setSortDesc(bool desc)
0526 {
0527     if (m_sortDesc != desc) {
0528         m_sortDesc = desc;
0529 
0530         if (m_sortMode != -1 /* Unsorted */) {
0531             invalidateIfComplete();
0532             sort(m_sortMode, m_sortDesc ? Qt::DescendingOrder : Qt::AscendingOrder);
0533         }
0534 
0535         Q_EMIT sortDescChanged();
0536     }
0537 }
0538 
0539 bool FolderModel::sortDirsFirst() const
0540 {
0541     return m_sortDirsFirst;
0542 }
0543 
0544 void FolderModel::setSortDirsFirst(bool enable)
0545 {
0546     if (m_sortDirsFirst != enable) {
0547         m_sortDirsFirst = enable;
0548 
0549         if (m_sortMode != -1 /* Unsorted */) {
0550             invalidateIfComplete();
0551             sort(m_sortMode, m_sortDesc ? Qt::DescendingOrder : Qt::AscendingOrder);
0552         }
0553 
0554         Q_EMIT sortDirsFirstChanged();
0555     }
0556 }
0557 
0558 bool FolderModel::parseDesktopFiles() const
0559 {
0560     return m_parseDesktopFiles;
0561 }
0562 
0563 void FolderModel::setParseDesktopFiles(bool enable)
0564 {
0565     if (m_parseDesktopFiles != enable) {
0566         m_parseDesktopFiles = enable;
0567         Q_EMIT parseDesktopFilesChanged();
0568     }
0569 }
0570 
0571 QObject *FolderModel::viewAdapter() const
0572 {
0573     return m_viewAdapter;
0574 }
0575 
0576 void FolderModel::setViewAdapter(QObject *adapter)
0577 {
0578     if (m_viewAdapter != adapter) {
0579         KAbstractViewAdapter *abstractViewAdapter = dynamic_cast<KAbstractViewAdapter *>(adapter);
0580 
0581         m_viewAdapter = abstractViewAdapter;
0582 
0583         if (m_viewAdapter && !m_previewGenerator) {
0584             m_previewGenerator = new KFilePreviewGenerator(abstractViewAdapter, this);
0585             m_previewGenerator->setPreviewShown(m_previews);
0586             m_previewGenerator->setEnabledPlugins(m_effectivePreviewPlugins);
0587         }
0588 
0589         Q_EMIT viewAdapterChanged();
0590     }
0591 }
0592 
0593 bool FolderModel::previews() const
0594 {
0595     return m_previews;
0596 }
0597 
0598 void FolderModel::setPreviews(bool previews)
0599 {
0600     if (m_previews != previews) {
0601         m_previews = previews;
0602 
0603         if (m_previewGenerator) {
0604             m_previewGenerator->setPreviewShown(m_previews);
0605         }
0606 
0607         Q_EMIT previewsChanged();
0608     }
0609 }
0610 
0611 QStringList FolderModel::previewPlugins() const
0612 {
0613     return m_previewPlugins;
0614 }
0615 
0616 void FolderModel::setPreviewPlugins(const QStringList &previewPlugins)
0617 {
0618     QStringList effectivePlugins = previewPlugins;
0619     if (effectivePlugins.isEmpty()) {
0620         effectivePlugins = KIO::PreviewJob::defaultPlugins();
0621     }
0622 
0623     if (m_effectivePreviewPlugins != effectivePlugins) {
0624         m_effectivePreviewPlugins = effectivePlugins;
0625 
0626         if (m_previewGenerator) {
0627             m_previewGenerator->setPreviewShown(false);
0628             m_previewGenerator->setEnabledPlugins(m_effectivePreviewPlugins);
0629             m_previewGenerator->setPreviewShown(true);
0630         }
0631     }
0632 
0633     if (m_previewPlugins != previewPlugins) {
0634         m_previewPlugins = previewPlugins;
0635         Q_EMIT previewPluginsChanged();
0636     }
0637 }
0638 
0639 int FolderModel::filterMode() const
0640 {
0641     return m_filterMode;
0642 }
0643 
0644 void FolderModel::setFilterMode(int filterMode)
0645 {
0646     if (m_filterMode != (FilterMode)filterMode) {
0647         m_filterMode = (FilterMode)filterMode;
0648 
0649         invalidateFilterIfComplete();
0650 
0651         Q_EMIT filterModeChanged();
0652     }
0653 }
0654 
0655 QString FolderModel::filterPattern() const
0656 {
0657     return m_filterPattern;
0658 }
0659 
0660 void FolderModel::setFilterPattern(const QString &pattern)
0661 {
0662     if (m_filterPattern == pattern) {
0663         return;
0664     }
0665 
0666     m_filterPattern = pattern;
0667     m_filterPatternMatchAll = (pattern == QLatin1String("*"));
0668 
0669     const QStringList patterns = pattern.split(QLatin1Char(' '));
0670     m_regExps.clear();
0671     m_regExps.reserve(patterns.count());
0672 
0673     for (const QString &pattern : patterns) {
0674         auto rx = QRegularExpression::fromWildcard(pattern);
0675         m_regExps.append(rx);
0676     }
0677 
0678     invalidateFilterIfComplete();
0679 
0680     Q_EMIT filterPatternChanged();
0681 }
0682 
0683 QStringList FolderModel::filterMimeTypes() const
0684 {
0685     return m_mimeSet.values();
0686 }
0687 
0688 void FolderModel::setFilterMimeTypes(const QStringList &mimeList)
0689 {
0690     const QSet<QString> set(mimeList.constBegin(), mimeList.constEnd());
0691 
0692     if (m_mimeSet != set) {
0693         m_mimeSet = set;
0694 
0695         invalidateFilterIfComplete();
0696 
0697         Q_EMIT filterMimeTypesChanged();
0698     }
0699 }
0700 
0701 void FolderModel::setScreen(int screen)
0702 {
0703     m_screenUsed = (screen != -1);
0704 
0705     if (!m_screenUsed || m_screen == screen)
0706         return;
0707 
0708     m_screen = screen;
0709     if (m_usedByContainment && !m_screenMapper->sharedDesktops()) {
0710         m_screenMapper->addScreen(screen, m_currentActivity, resolvedUrl());
0711     }
0712     Q_EMIT screenChanged();
0713 }
0714 
0715 KFileItem FolderModel::rootItem() const
0716 {
0717     return m_dirModel->dirLister()->rootItem();
0718 }
0719 
0720 void FolderModel::up()
0721 {
0722     const QUrl &up = KIO::upUrl(resolvedUrl());
0723 
0724     if (up.isValid()) {
0725         setUrl(up.toString());
0726     }
0727 }
0728 
0729 void FolderModel::cd(int row)
0730 {
0731     if (row < 0) {
0732         return;
0733     }
0734 
0735     const QModelIndex idx = index(row, 0);
0736     bool isDir = data(idx, IsDirRole).toBool();
0737 
0738     if (isDir) {
0739         const KFileItem item = itemForIndex(idx);
0740         if (m_parseDesktopFiles && item.isDesktopFile()) {
0741             const KDesktopFile file(item.targetUrl().path());
0742             if (file.hasLinkType()) {
0743                 setUrl(file.readUrl());
0744             }
0745         } else {
0746             setUrl(item.targetUrl().toString());
0747         }
0748     }
0749 }
0750 
0751 void FolderModel::run(int row)
0752 {
0753     if (row < 0) {
0754         return;
0755     }
0756 
0757     KFileItem item = itemForIndex(index(row, 0));
0758 
0759     QUrl url(item.targetUrl());
0760 
0761     // FIXME TODO: This can go once we depend on a KIO w/ fe1f50caaf2.
0762     if (url.scheme().isEmpty()) {
0763         url.setScheme(QStringLiteral("file"));
0764     }
0765 
0766     auto job = new KIO::OpenUrlJob(url);
0767     job->setUiDelegate(KIO::createDefaultJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, nullptr));
0768     // On desktop:/ we want to be able to run .desktop files right away,
0769     // otherwise ask for security reasons. We also don't use the targetUrl()
0770     // from above since we don't want the resolved /home/foo/Desktop URL.
0771     job->setShowOpenOrExecuteDialog(item.url().scheme() != QLatin1String("desktop") || item.url().adjusted(QUrl::RemoveFilename).path() != QLatin1String("/")
0772                                     || !item.isDesktopFile());
0773     job->setRunExecutables(true);
0774     job->start();
0775 }
0776 
0777 void FolderModel::runSelected()
0778 {
0779     const QModelIndexList indexes = m_selectionModel->selectedIndexes();
0780 
0781     if (indexes.isEmpty()) {
0782         return;
0783     }
0784 
0785     if (indexes.count() == 1) {
0786         run(indexes.first().row());
0787         return;
0788     }
0789 
0790     KFileItemActions fileItemActions(this);
0791     KFileItemList items;
0792 
0793     for (const QModelIndex &index : indexes) {
0794         // Skip over directories.
0795         if (!index.data(IsDirRole).toBool()) {
0796             items << itemForIndex(index);
0797         }
0798     }
0799 
0800     fileItemActions.runPreferredApplications(items);
0801 }
0802 
0803 void FolderModel::rename(int row, const QString &name)
0804 {
0805     if (row < 0) {
0806         return;
0807     }
0808 
0809     QModelIndex idx = index(row, 0);
0810     m_dirModel->setData(mapToSource(idx), name, Qt::EditRole);
0811 }
0812 
0813 int FolderModel::fileExtensionBoundary(int row)
0814 {
0815     const QModelIndex idx = index(row, 0);
0816     const QString &name = data(idx, Qt::DisplayRole).toString();
0817 
0818     int boundary = name.length();
0819 
0820     if (data(idx, IsDirRole).toBool()) {
0821         return boundary;
0822     }
0823 
0824     QMimeDatabase db;
0825     const QString &ext = db.suffixForFileName(name);
0826 
0827     if (ext.isEmpty()) {
0828         boundary = name.lastIndexOf(QLatin1Char('.'));
0829 
0830         if (boundary < 1) {
0831             boundary = name.length();
0832         }
0833     } else {
0834         boundary -= ext.length() + 1;
0835     }
0836 
0837     return boundary;
0838 }
0839 
0840 bool FolderModel::hasSelection() const
0841 {
0842     return m_selectionModel->hasSelection();
0843 }
0844 
0845 bool FolderModel::isSelected(int row)
0846 {
0847     if (row < 0) {
0848         return false;
0849     }
0850 
0851     return m_selectionModel->isSelected(index(row, 0));
0852 }
0853 
0854 void FolderModel::setSelected(int row)
0855 {
0856     if (row < 0) {
0857         return;
0858     }
0859 
0860     m_selectionModel->select(index(row, 0), QItemSelectionModel::Select);
0861 }
0862 
0863 void FolderModel::toggleSelected(int row)
0864 {
0865     if (row < 0) {
0866         return;
0867     }
0868 
0869     m_selectionModel->select(index(row, 0), QItemSelectionModel::Toggle);
0870 }
0871 
0872 void FolderModel::setRangeSelected(int anchor, int to)
0873 {
0874     if (anchor < 0 || to < 0) {
0875         return;
0876     }
0877 
0878     QItemSelection selection(index(anchor, 0), index(to, 0));
0879     m_selectionModel->select(selection, QItemSelectionModel::ClearAndSelect);
0880 }
0881 
0882 void FolderModel::updateSelection(const QVariantList &rows, bool toggle)
0883 {
0884     QItemSelection newSelection;
0885 
0886     int iRow = -1;
0887 
0888     for (const QVariant &row : rows) {
0889         iRow = row.toInt();
0890 
0891         if (iRow < 0) {
0892             return;
0893         }
0894 
0895         const QModelIndex &idx = index(iRow, 0);
0896         newSelection.select(idx, idx);
0897     }
0898 
0899     if (toggle) {
0900         QItemSelection pinnedSelection = m_pinnedSelection;
0901         pinnedSelection.merge(newSelection, QItemSelectionModel::Toggle);
0902         m_selectionModel->select(pinnedSelection, QItemSelectionModel::ClearAndSelect);
0903     } else {
0904         m_selectionModel->select(newSelection, QItemSelectionModel::ClearAndSelect);
0905     }
0906 }
0907 
0908 void FolderModel::clearSelection()
0909 {
0910     if (m_selectionModel->hasSelection()) {
0911         m_selectionModel->clear();
0912     }
0913 }
0914 
0915 void FolderModel::pinSelection()
0916 {
0917     m_pinnedSelection = m_selectionModel->selection();
0918 }
0919 
0920 void FolderModel::unpinSelection()
0921 {
0922     m_pinnedSelection = QItemSelection();
0923 }
0924 
0925 void FolderModel::addItemDragImage(int row, int x, int y, int width, int height, const QVariant &image)
0926 {
0927     if (row < 0) {
0928         return;
0929     }
0930 
0931     delete m_dragImages.take(row);
0932 
0933     DragImage *dragImage = new DragImage();
0934     dragImage->row = row;
0935     dragImage->rect = QRect(x, y, width, height);
0936     dragImage->image = image.value<QImage>();
0937     dragImage->blank = false;
0938 
0939     m_dragImages.insert(row, dragImage);
0940 }
0941 
0942 void FolderModel::clearDragImages()
0943 {
0944     qDeleteAll(m_dragImages);
0945     m_dragImages.clear();
0946 }
0947 
0948 void FolderModel::setDragHotSpotScrollOffset(int x, int y)
0949 {
0950     m_dragHotSpotScrollOffset.setX(x);
0951     m_dragHotSpotScrollOffset.setY(y);
0952 }
0953 
0954 QPoint FolderModel::dragCursorOffset(int row)
0955 {
0956     DragImage *image = m_dragImages.value(row);
0957     if (!image) {
0958         return QPoint(0, 0);
0959     }
0960 
0961     return image->cursorOffset;
0962 }
0963 
0964 void FolderModel::addDragImage(QDrag *drag, int x, int y)
0965 {
0966     if (!drag || m_dragImages.isEmpty()) {
0967         return;
0968     }
0969 
0970     qreal dpr = 1.0;
0971 
0972     QRegion region;
0973 
0974     for (DragImage *image : std::as_const(m_dragImages)) {
0975         image->blank = isBlank(image->row);
0976         image->rect.translate(-m_dragHotSpotScrollOffset.x(), -m_dragHotSpotScrollOffset.y());
0977         if (!image->blank && !image->image.isNull()) {
0978             region = region.united(image->rect);
0979             dpr = std::max(dpr, image->image.devicePixelRatioF());
0980         }
0981     }
0982 
0983     QRect rect = region.boundingRect();
0984     QPoint offset = rect.topLeft();
0985     rect.translate(-offset.x(), -offset.y());
0986 
0987     QImage dragImage(rect.size() * dpr, QImage::Format_RGBA8888);
0988     dragImage.setDevicePixelRatio(dpr);
0989     dragImage.fill(Qt::transparent);
0990 
0991     QPainter painter(&dragImage);
0992 
0993     QPoint pos;
0994 
0995     for (DragImage *image : std::as_const(m_dragImages)) {
0996         if (!image->blank && !image->image.isNull()) {
0997             pos = image->rect.translated(-offset.x(), -offset.y()).topLeft();
0998             image->cursorOffset.setX(pos.x() - (x - offset.x()));
0999             image->cursorOffset.setY(pos.y() - (y - offset.y()));
1000 
1001             painter.drawImage(pos, image->image);
1002         }
1003 
1004         // FIXME HACK: Operate on copy.
1005         image->rect.translate(m_dragHotSpotScrollOffset.x(), m_dragHotSpotScrollOffset.y());
1006     }
1007 
1008     drag->setPixmap(QPixmap::fromImage(dragImage));
1009     drag->setHotSpot(QPoint(x - offset.x(), y - offset.y()));
1010 }
1011 
1012 void FolderModel::dragSelected(int x, int y)
1013 {
1014     if (dragging()) {
1015         return;
1016     }
1017 
1018     DragTracker::self()->setDragInProgress(this, true);
1019     m_urlChangedWhileDragging = false;
1020 
1021     // Avoid starting a drag synchronously in a mouse handler or interferes with
1022     // child event filtering in parent items (and thus e.g. press-and-hold hand-
1023     // ling in a containment).
1024     QMetaObject::invokeMethod(this, "dragSelectedInternal", Qt::QueuedConnection, Q_ARG(int, x), Q_ARG(int, y));
1025 }
1026 
1027 void FolderModel::dragSelectedInternal(int x, int y)
1028 {
1029     if (!m_viewAdapter || !m_selectionModel->hasSelection()) {
1030         DragTracker::self()->setDragInProgress(nullptr, false);
1031         return;
1032     }
1033 
1034     ItemViewAdapter *adapter = qobject_cast<ItemViewAdapter *>(m_viewAdapter);
1035     QQuickItem *item = qobject_cast<QQuickItem *>(adapter->adapterView());
1036 
1037     QDrag *drag = new QDrag(item);
1038 
1039     addDragImage(drag, x, y);
1040 
1041     m_dragIndexes = m_selectionModel->selectedIndexes();
1042 
1043     std::sort(m_dragIndexes.begin(), m_dragIndexes.end());
1044 
1045     // TODO: Optimize to Q_EMIT contiguous groups.
1046     Q_EMIT dataChanged(m_dragIndexes.constFirst(), m_dragIndexes.constLast(), {BlankRole});
1047 
1048     QModelIndexList sourceDragIndexes;
1049     sourceDragIndexes.reserve(m_dragIndexes.count());
1050     for (const QModelIndex &index : std::as_const(m_dragIndexes)) {
1051         sourceDragIndexes.append(mapToSource(index));
1052     }
1053 
1054     drag->setMimeData(m_dirModel->mimeData(sourceDragIndexes));
1055 
1056     // Due to spring-loading (aka auto-expand), the URL might change
1057     // while the drag is in-flight - in that case we don't want to
1058     // unnecessarily Q_EMIT dataChanged() for (possibly invalid) indices
1059     // after it ends.
1060     const QUrl currentUrl(m_dirModel->dirLister()->url());
1061 
1062     item->grabMouse();
1063     drag->exec(supportedDragActions());
1064 
1065     item->ungrabMouse();
1066 
1067     DragTracker::self()->setDragInProgress(nullptr, false);
1068     m_urlChangedWhileDragging = false;
1069 
1070     if (m_dirModel->dirLister()->url() == currentUrl) {
1071         const QModelIndex first(m_dragIndexes.first());
1072         const QModelIndex last(m_dragIndexes.last());
1073         m_dragIndexes.clear();
1074         // TODO: Optimize to Q_EMIT contiguous groups.
1075         Q_EMIT dataChanged(first, last, {BlankRole});
1076     }
1077 }
1078 
1079 static bool isDropBetweenSharedViews(const QList<QUrl> &urls, const QUrl &folderUrl)
1080 {
1081     if (urls.empty()) {
1082         return false;
1083     }
1084 
1085     for (const auto &url : urls) {
1086         if (folderUrl.adjusted(QUrl::StripTrailingSlash) != url.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash)) {
1087             return false;
1088         }
1089     }
1090     return true;
1091 }
1092 
1093 void FolderModel::drop(QQuickItem *target, QObject *dropEvent, int row, bool showMenuManually)
1094 {
1095     QMimeData *mimeData = qobject_cast<QMimeData *>(dropEvent->property("mimeData").value<QObject *>());
1096 
1097     if (!mimeData) {
1098         return;
1099     }
1100 
1101     QModelIndex idx;
1102     KFileItem item;
1103 
1104     if (row > -1 && row < rowCount()) {
1105         idx = index(row, 0);
1106         item = itemForIndex(idx);
1107     }
1108 
1109     QUrl dropTargetUrl;
1110 
1111     // So we get to run mostLocalUrl() over the current URL.
1112     if (item.isNull()) {
1113         item = rootItem();
1114     }
1115 
1116     if (item.isNull()) {
1117         dropTargetUrl = m_dirModel->dirLister()->url();
1118     } else if (m_parseDesktopFiles && item.isDesktopFile()) {
1119         const KDesktopFile file(item.targetUrl().path());
1120 
1121         if (file.hasLinkType()) {
1122             dropTargetUrl = QUrl(file.readUrl());
1123         } else {
1124             dropTargetUrl = item.mostLocalUrl();
1125         }
1126     } else {
1127         dropTargetUrl = item.mostLocalUrl();
1128     }
1129 
1130     auto dropTargetFolderUrl = dropTargetUrl;
1131     if (dropTargetFolderUrl.fileName() == QLatin1Char('.')) {
1132         // the target URL for desktop:/ is e.g. 'file://home/user/Desktop/.'
1133         dropTargetFolderUrl = dropTargetFolderUrl.adjusted(QUrl::RemoveFilename);
1134     }
1135 
1136     // use dropTargetUrl to resolve desktop:/ to the actual file location which is also used by the mime data
1137     /* QMimeData operates on local URLs, but the dir lister and thus screen mapper and positioner may
1138      * use a fancy scheme like desktop:/ instead. Ensure we always use the latter to properly map URLs,
1139      * i.e. go from file:///home/user/Desktop/file to desktop:/file
1140      */
1141     auto mappableUrl = [this, dropTargetFolderUrl](const QUrl &url) -> QUrl {
1142         if (dropTargetFolderUrl != m_dirModel->dirLister()->url()) {
1143             QString mappedUrl = url.toString();
1144             const auto local = dropTargetFolderUrl.toString();
1145             const auto internal = m_dirModel->dirLister()->url().toString();
1146             if (mappedUrl.startsWith(local)) {
1147                 mappedUrl.replace(0, local.size(), internal);
1148             }
1149             return ScreenMapper::stringToUrl(mappedUrl);
1150         }
1151         return url;
1152     };
1153 
1154     const int x = dropEvent->property("x").toInt();
1155     const int y = dropEvent->property("y").toInt();
1156     const QPoint dropPos = {x, y};
1157 
1158     if (dragging() && row == -1 && !m_urlChangedWhileDragging) {
1159         if (m_locked || mimeData->urls().isEmpty()) {
1160             return;
1161         }
1162 
1163         setSortMode(-1);
1164 
1165         for (const auto &url : mimeData->urls()) {
1166             m_dropTargetPositions.insert(url.fileName(), dropPos);
1167             m_screenMapper->addMapping(mappableUrl(url), m_screen, m_currentActivity, ScreenMapper::DelayedSignal);
1168             m_screenMapper->removeItemFromDisabledScreen(mappableUrl(url));
1169         }
1170         Q_EMIT move(x, y, mimeData->urls());
1171 
1172         return;
1173     }
1174 
1175     if (idx.isValid() && !(flags(idx) & Qt::ItemIsDropEnabled)) {
1176         return;
1177     }
1178 
1179     // Catch drops from a Task Manager and convert to usable URL.
1180     if (!mimeData->hasUrls() && mimeData->hasFormat(QStringLiteral("text/x-orgkdeplasmataskmanager_taskurl"))) {
1181         QList<QUrl> urls = {QUrl(QString::fromUtf8(mimeData->data(QStringLiteral("text/x-orgkdeplasmataskmanager_taskurl"))))};
1182         mimeData->setUrls(urls);
1183     }
1184 
1185     if (m_usedByContainment && !m_screenMapper->sharedDesktops()) {
1186         if (isDropBetweenSharedViews(mimeData->urls(), dropTargetFolderUrl)) {
1187             setSortMode(-1);
1188             const QList<QUrl> urls = mimeData->urls();
1189             for (const auto &url : urls) {
1190                 m_dropTargetPositions.insert(url.fileName(), dropPos);
1191                 m_screenMapper->addMapping(mappableUrl(url), m_screen, m_currentActivity, ScreenMapper::DelayedSignal);
1192                 m_screenMapper->removeItemFromDisabledScreen(mappableUrl(url));
1193             }
1194             m_dropTargetPositionsCleanup->start();
1195             return;
1196         }
1197     }
1198 
1199     Qt::DropAction proposedAction((Qt::DropAction)dropEvent->property("proposedAction").toInt());
1200     Qt::DropActions possibleActions(dropEvent->property("possibleActions").toInt());
1201     Qt::MouseButtons buttons(dropEvent->property("buttons").toInt());
1202     Qt::KeyboardModifiers modifiers(dropEvent->property("modifiers").toInt());
1203 
1204     auto pos = target->mapToScene(dropPos).toPoint();
1205     pos = target->window()->mapToGlobal(pos);
1206     QDropEvent ev(pos, possibleActions, mimeData, buttons, modifiers);
1207     ev.setDropAction(proposedAction);
1208 
1209     KIO::DropJobFlag flag = showMenuManually ? KIO::ShowMenuManually : KIO::DropJobDefaultFlags;
1210     KIO::DropJob *dropJob = KIO::drop(&ev, dropTargetUrl, flag);
1211     dropJob->setUiDelegate(new KNotificationJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled));
1212 
1213     // No menu will be shown so skip copying QMimeData
1214     if (mimeData->urls().empty()) {
1215         return;
1216     }
1217 
1218     // The QMimeData we extract from the DropArea's drop event is deleted as soon as this method
1219     // ends but we need to keep a copy for when popupMenuAboutToShow fires.
1220     QMimeData *mimeCopy = new QMimeData();
1221     const QStringList formats = mimeData->formats();
1222     for (const QString &format : formats) {
1223         mimeCopy->setData(format, mimeData->data(format));
1224     }
1225 
1226     connect(dropJob, &KIO::DropJob::popupMenuAboutToShow, this, [this, mimeCopy, x, y, dropJob](const KFileItemListProperties &) {
1227         Q_EMIT popupMenuAboutToShow(dropJob, mimeCopy, x, y);
1228         mimeCopy->deleteLater();
1229     });
1230 
1231     /*
1232      * Position files that come from a drag'n'drop event at the drop event
1233      * target position. To do so, we first listen to copy job to figure out
1234      * the target URL. Then we store the position of this drop event in the
1235      * hash and eventually trigger a move request when we get notified about
1236      * the new file event from the source model.
1237      */
1238     connect(dropJob, &KIO::DropJob::copyJobStarted, this, [this, dropPos, dropTargetUrl](KIO::CopyJob *copyJob) {
1239         auto map = [this, dropPos, dropTargetUrl](const QUrl &targetUrl) {
1240             // KIO::CopyJob::copyingDone is emitted recursively, ignore everything not directly in the target folder
1241             if ((dropTargetUrl.path() + QStringLiteral("/") + targetUrl.fileName()) != targetUrl.path()) {
1242                 return;
1243             }
1244             m_dropTargetPositions.insert(targetUrl.fileName(), dropPos);
1245             m_dropTargetPositionsCleanup->start();
1246 
1247             if (m_usedByContainment && !m_screenMapper->sharedDesktops()) {
1248                 // assign a screen for the item before the copy is actually done, so
1249                 // filterAcceptsRow doesn't assign the default screen to it
1250                 QUrl url = resolvedUrl();
1251                 // if the folderview's folder is a standard path, just use the targetUrl for mapping
1252                 if (targetUrl.toString().startsWith(url.toString())) {
1253                     m_screenMapper->addMapping(targetUrl, m_screen, m_currentActivity, ScreenMapper::DelayedSignal);
1254                 } else if (targetUrl.toString().startsWith(dropTargetUrl.toString())) {
1255                     // if the folderview's folder is a special path, like desktop:// , we need to convert
1256                     // the targetUrl file:// path to a desktop:/ path for mapping
1257                     auto destPath = dropTargetUrl.path();
1258                     auto filePath = targetUrl.path();
1259                     if (filePath.startsWith(destPath)) {
1260                         url.setPath(filePath.remove(0, destPath.length()));
1261                         m_screenMapper->addMapping(url, m_screen, m_currentActivity, ScreenMapper::DelayedSignal);
1262                     }
1263                 }
1264             }
1265         };
1266         // remember drop target position for target URL and forget about the source URL
1267         connect(copyJob, &KIO::CopyJob::copyingDone, this, [map](KIO::Job *, const QUrl &, const QUrl &targetUrl, const QDateTime &, bool, bool) {
1268             map(targetUrl);
1269         });
1270         connect(copyJob, &KIO::CopyJob::copyingLinkDone, this, [map](KIO::Job *, const QUrl &, const QString &, const QUrl &targetUrl) {
1271             map(targetUrl);
1272         });
1273     });
1274 }
1275 
1276 void FolderModel::dropCwd(QObject *dropEvent)
1277 {
1278     QMimeData *mimeData = qobject_cast<QMimeData *>(dropEvent->property("mimeData").value<QObject *>());
1279 
1280     if (!mimeData) {
1281         return;
1282     }
1283 
1284     Qt::DropAction proposedAction((Qt::DropAction)dropEvent->property("proposedAction").toInt());
1285     Qt::DropActions possibleActions(dropEvent->property("possibleActions").toInt());
1286     Qt::MouseButtons buttons(dropEvent->property("buttons").toInt());
1287     Qt::KeyboardModifiers modifiers(dropEvent->property("modifiers").toInt());
1288 
1289     QDropEvent ev(QPoint(), possibleActions, mimeData, buttons, modifiers);
1290     ev.setDropAction(proposedAction);
1291 
1292     KIO::DropJob *dropJob = KIO::drop(&ev, m_dirModel->dirLister()->url().adjusted(QUrl::PreferLocalFile));
1293     dropJob->setUiDelegate(new KNotificationJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled));
1294 }
1295 
1296 void FolderModel::changeSelection(const QItemSelection &selected, const QItemSelection &deselected)
1297 {
1298     QModelIndexList indices = selected.indexes();
1299     indices.append(deselected.indexes());
1300 
1301     const QList<int> roles{SelectedRole};
1302 
1303     for (const QModelIndex &index : std::as_const(indices)) {
1304         Q_EMIT dataChanged(index, index, roles);
1305     }
1306 
1307     if (!m_selectionModel->hasSelection()) {
1308         clearDragImages();
1309     } else {
1310         const QModelIndexList deselectedIndices = deselected.indexes();
1311         for (const QModelIndex &index : deselectedIndices) {
1312             delete m_dragImages.take(index.row());
1313         }
1314     }
1315 
1316     updateActions();
1317 }
1318 
1319 bool FolderModel::isBlank(int row) const
1320 {
1321     if (row < 0) {
1322         return true;
1323     }
1324 
1325     return data(index(row, 0), BlankRole).toBool();
1326 }
1327 
1328 QVariant FolderModel::data(const QModelIndex &index, int role) const
1329 {
1330     if (!index.isValid()) {
1331         return QVariant();
1332     }
1333 
1334     if (role == BlankRole) {
1335         return m_dragIndexes.contains(index);
1336     } else if (role == SelectedRole) {
1337         return m_selectionModel->isSelected(index);
1338     } else if (role == IsDirRole) {
1339         return isDir(mapToSource(index), m_dirModel);
1340     } else if (role == IsLinkRole) {
1341         const KFileItem item = itemForIndex(index);
1342         return item.isLink();
1343     } else if (role == IsHiddenRole) {
1344         const KFileItem item = itemForIndex(index);
1345         return item.isHidden();
1346     } else if (role == UrlRole) {
1347         return itemForIndex(index).url();
1348     } else if (role == LinkDestinationUrl) {
1349         const KFileItem item = itemForIndex(index);
1350 
1351         if (m_parseDesktopFiles && item.isDesktopFile()) {
1352             const KDesktopFile file(item.targetUrl().path());
1353 
1354             if (file.hasLinkType()) {
1355                 return file.readUrl();
1356             }
1357         }
1358 
1359         return item.targetUrl();
1360     } else if (role == SizeRole) {
1361         bool isDir = data(index, IsDirRole).toBool();
1362 
1363         if (!isDir) {
1364             return m_dirModel->data(mapToSource(QSortFilterProxyModel::index(index.row(), 1)), Qt::DisplayRole);
1365         }
1366     } else if (role == TypeRole) {
1367         return m_dirModel->data(mapToSource(QSortFilterProxyModel::index(index.row(), 6)), Qt::DisplayRole);
1368     } else if (role == FileNameRole) {
1369         return itemForIndex(index).url().fileName();
1370     } else if (role == FileNameWrappedRole) {
1371         return KStringHandler::preProcessWrap(itemForIndex(index).text());
1372     }
1373 
1374     return QSortFilterProxyModel::data(index, role);
1375 }
1376 
1377 int FolderModel::indexForUrl(const QUrl &url) const
1378 {
1379     return mapFromSource(m_dirModel->indexForUrl(url)).row();
1380 }
1381 
1382 KFileItem FolderModel::itemForIndex(const QModelIndex &index) const
1383 {
1384     return m_dirModel->itemForIndex(mapToSource(index));
1385 }
1386 
1387 bool FolderModel::isDir(const QModelIndex &index, const KDirModel *dirModel) const
1388 {
1389     KFileItem item = dirModel->itemForIndex(index);
1390     if (item.isDir()) {
1391         return true;
1392     }
1393 
1394     auto it = m_isDirCache.constFind(item.url());
1395     if (it != m_isDirCache.constEnd()) {
1396         return *it;
1397     }
1398 
1399     if (m_parseDesktopFiles && item.isDesktopFile()) {
1400         // Check if the desktop file is a link to a directory
1401         KDesktopFile file(item.targetUrl().path());
1402 
1403         if (!file.hasLinkType()) {
1404             return false;
1405         }
1406 
1407         const QUrl url(file.readUrl());
1408 
1409         // Check if we already have a running StatJob for this URL.
1410         if (m_isDirJobs.contains(item.url())) {
1411             return false;
1412         }
1413 
1414         // Assume the root folder of a protocol is always a folder.
1415         // This avoids spinning up e.g. trash KIO slave just to check whether trash:/ is a folder.
1416         if (url.path() == QLatin1String("/")) {
1417             m_isDirCache.insert(item.url(), true);
1418             return true;
1419         }
1420 
1421         if (KProtocolInfo::protocolClass(url.scheme()) != QLatin1String(":local")) {
1422             return false;
1423         }
1424 
1425         KIO::StatJob *job = KIO::stat(url, KIO::HideProgressInfo);
1426         job->setProperty("org.kde.plasma.folder_url", item.url());
1427         job->setSide(KIO::StatJob::SourceSide);
1428         job->setDetails(KIO::StatNoDetails);
1429         connect(job, &KJob::result, this, &FolderModel::statResult);
1430         m_isDirJobs.insert(item.url(), job);
1431     }
1432 
1433     return false;
1434 }
1435 
1436 void FolderModel::statResult(KJob *job)
1437 {
1438     KIO::StatJob *statJob = static_cast<KIO::StatJob *>(job);
1439 
1440     const QUrl &url = statJob->property("org.kde.plasma.folder_url").toUrl();
1441     const QModelIndex &idx = index(indexForUrl(url), 0);
1442 
1443     if (idx.isValid() && statJob->error() == KJob::NoError) {
1444         m_isDirCache[url] = statJob->statResult().isDir();
1445 
1446         Q_EMIT dataChanged(idx, idx, QList<int>() << IsDirRole);
1447     }
1448 
1449     m_isDirJobs.remove(url);
1450 }
1451 
1452 void FolderModel::evictFromIsDirCache(const KFileItemList &items)
1453 {
1454     for (const KFileItem &item : items) {
1455         m_screenMapper->removeFromMap(item.url(), m_currentActivity);
1456         m_isDirCache.remove(item.url());
1457     }
1458 }
1459 
1460 bool FolderModel::lessThan(const QModelIndex &left, const QModelIndex &right) const
1461 {
1462     const KDirModel *dirModel = static_cast<KDirModel *>(sourceModel());
1463 
1464     if (m_sortDirsFirst || left.column() == KDirModel::Size) {
1465         bool leftIsDir = isDir(left, dirModel);
1466         bool rightIsDir = isDir(right, dirModel);
1467 
1468         if (leftIsDir && !rightIsDir) {
1469             return (sortOrder() == Qt::AscendingOrder);
1470         }
1471 
1472         if (!leftIsDir && rightIsDir) {
1473             return (sortOrder() == Qt::DescendingOrder);
1474         }
1475     }
1476 
1477     const KFileItem leftItem = dirModel->data(left, KDirModel::FileItemRole).value<KFileItem>();
1478     const KFileItem rightItem = dirModel->data(right, KDirModel::FileItemRole).value<KFileItem>();
1479     const int column = left.column();
1480     int result = 0;
1481 
1482     switch (column) {
1483     case KDirModel::Size: {
1484         if (isDir(left, dirModel) && isDir(right, dirModel)) {
1485             const int leftChildCount = dirModel->data(left, KDirModel::ChildCountRole).toInt();
1486             const int rightChildCount = dirModel->data(right, KDirModel::ChildCountRole).toInt();
1487             if (leftChildCount < rightChildCount)
1488                 result = -1;
1489             else if (leftChildCount > rightChildCount)
1490                 result = +1;
1491         } else {
1492             const KIO::filesize_t leftSize = leftItem.size();
1493             const KIO::filesize_t rightSize = rightItem.size();
1494             if (leftSize < rightSize)
1495                 result = -1;
1496             else if (leftSize > rightSize)
1497                 result = +1;
1498         }
1499 
1500         break;
1501     }
1502     case KDirModel::ModifiedTime: {
1503         const long long leftTime = leftItem.entry().numberValue(KIO::UDSEntry::UDS_MODIFICATION_TIME, -1);
1504         const long long rightTime = rightItem.entry().numberValue(KIO::UDSEntry::UDS_MODIFICATION_TIME, -1);
1505         if (leftTime < rightTime)
1506             result = -1;
1507         else if (leftTime > rightTime)
1508             result = +1;
1509 
1510         break;
1511     }
1512     case KDirModel::Type:
1513         result = QString::compare(dirModel->data(left, Qt::DisplayRole).toString(), dirModel->data(right, Qt::DisplayRole).toString());
1514         break;
1515 
1516     default:
1517         break;
1518     }
1519 
1520     if (result != 0)
1521         return result < 0;
1522 
1523     QCollator collator;
1524 
1525     result = collator.compare(leftItem.text(), rightItem.text());
1526 
1527     if (result != 0)
1528         return result < 0;
1529 
1530     result = collator.compare(leftItem.name(), rightItem.name());
1531 
1532     if (result != 0)
1533         return result < 0;
1534 
1535     return QString::compare(leftItem.url().url(), rightItem.url().url(), Qt::CaseSensitive);
1536 }
1537 
1538 Qt::DropActions FolderModel::supportedDragActions() const
1539 {
1540     return Qt::CopyAction | Qt::MoveAction | Qt::LinkAction;
1541 }
1542 
1543 Qt::DropActions FolderModel::supportedDropActions() const
1544 {
1545     return Qt::CopyAction | Qt::MoveAction | Qt::LinkAction;
1546 }
1547 
1548 inline bool FolderModel::matchMimeType(const KFileItem &item) const
1549 {
1550     if (m_mimeSet.isEmpty()) {
1551         return false;
1552     }
1553 
1554     if (m_mimeSet.contains(QLatin1String("all/all")) || m_mimeSet.contains(QLatin1String("all/allfiles"))) {
1555         return true;
1556     }
1557 
1558     const QString mimeType = item.determineMimeType().name();
1559     return m_mimeSet.contains(mimeType);
1560 }
1561 
1562 inline bool FolderModel::matchPattern(const KFileItem &item) const
1563 {
1564     if (m_filterPatternMatchAll) {
1565         return true;
1566     }
1567 
1568     const QString name = item.name();
1569     QListIterator<QRegularExpression> i(m_regExps);
1570     while (i.hasNext()) {
1571         if (i.next().match(name).hasMatch()) {
1572             return true;
1573         }
1574     }
1575 
1576     return false;
1577 }
1578 
1579 bool FolderModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const
1580 {
1581     const KDirModel *dirModel = static_cast<KDirModel *>(sourceModel());
1582     const KFileItem item = dirModel->itemForIndex(dirModel->index(sourceRow, KDirModel::Name, sourceParent));
1583 
1584     if (m_usedByContainment && !m_screenMapper->sharedDesktops()) {
1585         const QUrl url = item.url();
1586         const int screen = m_screenMapper->screenForItem(url, m_currentActivity);
1587         // don't do anything if the folderview is not associated with a screen
1588         if (m_screenUsed && screen == -1) {
1589             // The item is not associated with a screen, probably because this is the first
1590             // time we see it or the folderview was previously used as a regular applet.
1591             // Associated with this folderview if the view is on the first available screen
1592             if (m_screen == m_screenMapper->firstAvailableScreen(resolvedUrl(), m_currentActivity)) {
1593                 m_screenMapper->addMapping(url, m_screen, m_currentActivity, ScreenMapper::DelayedSignal);
1594             } else {
1595                 return false;
1596             }
1597         } else if (m_screen != screen) {
1598             // the item belongs to a different screen, filter it out
1599             return false;
1600         }
1601     }
1602 
1603     if (m_filterMode == NoFilter) {
1604         return true;
1605     }
1606 
1607     if (m_filterMode == FilterShowMatches) {
1608         return (matchPattern(item) && matchMimeType(item));
1609     } else {
1610         return !(matchPattern(item) && matchMimeType(item));
1611     }
1612 }
1613 
1614 void FolderModel::createActions()
1615 {
1616     KIO::FileUndoManager *manager = KIO::FileUndoManager::self();
1617 
1618     QAction *cut = KStandardAction::cut(this, &FolderModel::cut, this);
1619     QAction *copy = KStandardAction::copy(this, &FolderModel::copy, this);
1620 
1621     QAction *undo = KStandardAction::undo(manager, &KIO::FileUndoManager::undo, this);
1622     undo->setEnabled(manager->isUndoAvailable());
1623     undo->setShortcutContext(Qt::WidgetShortcut);
1624     connect(manager, SIGNAL(undoAvailable(bool)), undo, SLOT(setEnabled(bool)));
1625     connect(manager, &KIO::FileUndoManager::undoTextChanged, this, &FolderModel::undoTextChanged);
1626 
1627     QAction *paste = KStandardAction::paste(this, &FolderModel::paste, this);
1628     QAction *pasteTo = KStandardAction::paste(this, &FolderModel::pasteTo, this);
1629 
1630     QAction *refresh = new QAction(QIcon::fromTheme(QStringLiteral("view-refresh")), i18n("&Refresh View"), this);
1631     refresh->setShortcut(QKeySequence(QKeySequence::Refresh));
1632     connect(refresh, &QAction::triggered, this, &FolderModel::refresh);
1633 
1634     QAction *rename = KStandardAction::renameFile(this, &FolderModel::requestRename, this);
1635     QAction *trash = KStandardAction::moveToTrash(this, &FolderModel::moveSelectedToTrash, this);
1636     QAction *del = KStandardAction::deleteFile(this, &FolderModel::deleteSelected, this);
1637     RemoveAction *remove = new RemoveAction(&m_actionCollection, this);
1638 
1639     QAction *emptyTrash = new QAction(QIcon::fromTheme(QStringLiteral("trash-empty")), i18n("&Empty Trash"), this);
1640     connect(emptyTrash, &QAction::triggered, this, &FolderModel::emptyTrashBin);
1641 
1642     QAction *restoreFromTrash = new QAction(i18nc("Restore from trash", "Restore"), this);
1643     connect(restoreFromTrash, &QAction::triggered, this, &FolderModel::restoreSelectedFromTrash);
1644 
1645     QAction *actOpen = new QAction(QIcon::fromTheme(QStringLiteral("window-new")), i18n("&Open"), this);
1646     connect(actOpen, &QAction::triggered, this, &FolderModel::runSelected);
1647 
1648     m_actionCollection.addAction(QStringLiteral("open"), actOpen);
1649     m_actionCollection.addAction(QStringLiteral("cut"), cut);
1650     m_actionCollection.addAction(QStringLiteral("undo"), undo);
1651     m_actionCollection.addAction(QStringLiteral("copy"), copy);
1652     m_actionCollection.addAction(QStringLiteral("paste"), paste);
1653     m_actionCollection.addAction(QStringLiteral("pasteto"), pasteTo);
1654     m_actionCollection.addAction(QStringLiteral("refresh"), refresh);
1655     m_actionCollection.addAction(QStringLiteral("rename"), rename);
1656     m_actionCollection.addAction(QStringLiteral("remove"), remove);
1657     m_actionCollection.addAction(QStringLiteral("trash"), trash);
1658     m_actionCollection.addAction(QStringLiteral("del"), del);
1659     m_actionCollection.addAction(QStringLiteral("restoreFromTrash"), restoreFromTrash);
1660     m_actionCollection.addAction(QStringLiteral("emptyTrash"), emptyTrash);
1661 
1662     // The RemoveAction needs to be updated after adding all actions to the actionCollection
1663     remove->update();
1664 
1665     m_newMenu = new KNewFileMenu(this);
1666     m_newMenu->setModal(false);
1667     connect(m_newMenu, &KNewFileMenu::directoryCreated, this, &FolderModel::newFileMenuItemCreated);
1668     connect(m_newMenu, &KNewFileMenu::fileCreated, this, &FolderModel::newFileMenuItemCreated);
1669 
1670     m_actionCollection.addAction(QStringLiteral("newMenu"), m_newMenu);
1671 
1672     m_copyToMenu = new KFileCopyToMenu(nullptr);
1673 }
1674 
1675 QAction *FolderModel::action(const QString &name) const
1676 {
1677     return m_actionCollection.action(name);
1678 }
1679 
1680 QObject *FolderModel::newMenu() const
1681 {
1682     return m_newMenu->menu();
1683 }
1684 
1685 void FolderModel::updateActions()
1686 {
1687     const QModelIndexList indexes = m_selectionModel->selectedIndexes();
1688 
1689     KFileItemList items;
1690     QList<QUrl> urls;
1691     bool hasRemoteFiles = false;
1692     bool isTrashLink = false;
1693     const bool isTrash = (resolvedUrl().scheme() == QLatin1String("trash"));
1694 
1695     if (indexes.isEmpty()) {
1696         items << rootItem();
1697     } else {
1698         items.reserve(indexes.count());
1699         urls.reserve(indexes.count());
1700         for (const QModelIndex &index : indexes) {
1701             KFileItem item = itemForIndex(index);
1702             if (!item.isNull()) {
1703                 hasRemoteFiles |= item.localPath().isEmpty();
1704                 items.append(item);
1705                 urls.append(item.url());
1706             }
1707         }
1708     }
1709 
1710     KFileItemListProperties itemProperties(items);
1711     // Check if we're showing the menu for the trash link
1712     if (items.count() == 1 && items.at(0).isDesktopFile()) {
1713         KDesktopFile file(items.at(0).localPath());
1714         if (file.hasLinkType() && file.readUrl() == QLatin1String("trash:/")) {
1715             isTrashLink = true;
1716         }
1717     }
1718 
1719     if (m_newMenu) {
1720         m_newMenu->checkUpToDate();
1721         m_newMenu->setWorkingDirectory(m_dirModel->dirLister()->url());
1722         // we need to set here as well, when the menu is shown via AppletInterface::eventFilter
1723         m_menuPosition = QCursor::pos();
1724 
1725         if (QAction *newMenuAction = m_actionCollection.action(QStringLiteral("newMenu"))) {
1726             newMenuAction->setEnabled(itemProperties.supportsWriting());
1727             newMenuAction->setVisible(!isTrash);
1728         }
1729     }
1730 
1731     if (QAction *emptyTrash = m_actionCollection.action(QStringLiteral("emptyTrash"))) {
1732         if (isTrash || isTrashLink) {
1733             emptyTrash->setVisible(true);
1734             emptyTrash->setEnabled(!isTrashEmpty());
1735         } else {
1736             emptyTrash->setVisible(false);
1737         }
1738     }
1739 
1740     if (QAction *restoreFromTrash = m_actionCollection.action(QStringLiteral("restoreFromTrash"))) {
1741         restoreFromTrash->setVisible(isTrash);
1742     }
1743 
1744     if (QAction *remove = m_actionCollection.action(QStringLiteral("remove"))) {
1745         remove->setVisible(!hasRemoteFiles && itemProperties.supportsMoving() && itemProperties.supportsDeleting());
1746     }
1747 
1748     if (QAction *cut = m_actionCollection.action(QStringLiteral("cut"))) {
1749         cut->setEnabled(itemProperties.supportsDeleting());
1750         cut->setVisible(!isTrash);
1751     }
1752 
1753     if (QAction *paste = m_actionCollection.action(QStringLiteral("paste"))) {
1754         bool enable = false;
1755 
1756         const QString pasteText = KIO::pasteActionText(QApplication::clipboard()->mimeData(), &enable, rootItem());
1757 
1758         if (enable) {
1759             paste->setText(pasteText);
1760             paste->setEnabled(true);
1761         } else {
1762             paste->setText(i18n("&Paste"));
1763             paste->setEnabled(false);
1764         }
1765 
1766         if (QAction *pasteTo = m_actionCollection.action(QStringLiteral("pasteto"))) {
1767             pasteTo->setVisible(itemProperties.isDirectory() && itemProperties.supportsWriting());
1768             pasteTo->setEnabled(paste->isEnabled());
1769             pasteTo->setText(paste->text());
1770         }
1771     }
1772 
1773     if (QAction *rename = m_actionCollection.action(QStringLiteral("rename"))) {
1774         rename->setEnabled(itemProperties.supportsMoving());
1775         rename->setVisible(!isTrash);
1776     }
1777 }
1778 
1779 void FolderModel::openContextMenu(QQuickItem *visualParent, Qt::KeyboardModifiers modifiers)
1780 {
1781     Q_UNUSED(modifiers)
1782 
1783     if (m_usedByContainment && !KAuthorized::authorize(QStringLiteral("action/kdesktop_rmb"))) {
1784         return;
1785     }
1786 
1787     updateActions();
1788 
1789     const QModelIndexList indexes = m_selectionModel->selectedIndexes();
1790 
1791     QMenu *menu = new QMenu();
1792     if (!m_fileItemActions) {
1793         m_fileItemActions = new KFileItemActions(this);
1794     }
1795 
1796     if (indexes.isEmpty()) {
1797         menu->addAction(m_actionCollection.action(QStringLiteral("newMenu")));
1798         menu->addSeparator();
1799         menu->addAction(m_actionCollection.action(QStringLiteral("paste")));
1800         menu->addAction(m_actionCollection.action(QStringLiteral("undo")));
1801         menu->addAction(m_actionCollection.action(QStringLiteral("refresh")));
1802         menu->addAction(m_actionCollection.action(QStringLiteral("emptyTrash")));
1803         menu->addSeparator();
1804 
1805         KFileItemListProperties itemProperties(KFileItemList() << rootItem());
1806         m_fileItemActions->setItemListProperties(itemProperties);
1807 
1808         m_fileItemActions->insertOpenWithActionsTo(nullptr, menu, QStringList());
1809     } else {
1810         KFileItemList items;
1811         QList<QUrl> urls;
1812 
1813         items.reserve(indexes.count());
1814         urls.reserve(indexes.count());
1815         for (const QModelIndex &index : indexes) {
1816             KFileItem item = itemForIndex(index);
1817             if (!item.isNull()) {
1818                 items.append(item);
1819                 urls.append(item.url());
1820             }
1821         }
1822 
1823         KFileItemListProperties itemProperties(items);
1824 
1825         // Start adding the actions:
1826         // "Open" and "Open with" actions
1827         m_fileItemActions->setItemListProperties(itemProperties);
1828         m_fileItemActions->insertOpenWithActionsTo(nullptr, menu, QStringList());
1829         menu->addSeparator();
1830         menu->addAction(m_actionCollection.action(QStringLiteral("cut")));
1831         menu->addAction(m_actionCollection.action(QStringLiteral("copy")));
1832         if (urls.length() == 1) {
1833             if (items.first().isDir()) {
1834                 menu->addAction(m_actionCollection.action(QStringLiteral("pasteto")));
1835             }
1836         } else {
1837             menu->addAction(m_actionCollection.action(QStringLiteral("paste")));
1838         }
1839 
1840         menu->addAction(m_actionCollection.action(QStringLiteral("rename")));
1841         menu->addSeparator();
1842         menu->addAction(m_actionCollection.action(QStringLiteral("restoreFromTrash")));
1843 
1844         if (isDeleteCommandShown()) {
1845             QAction *trashAction = m_actionCollection.action(QStringLiteral("trash"));
1846             QAction *deleteAction = m_actionCollection.action(QStringLiteral("del"));
1847             menu->addAction(trashAction);
1848             menu->addAction(deleteAction);
1849         } else {
1850             if (RemoveAction *removeAction = qobject_cast<RemoveAction *>(m_actionCollection.action(QStringLiteral("remove")))) {
1851                 removeAction->update();
1852                 menu->addAction(removeAction);
1853 
1854                 // Used to monitor Shift modifier usage while the menu is open, to
1855                 // swap the Trash and Delete actions.
1856                 menu->installEventFilter(removeAction);
1857                 QCoreApplication::instance()->installEventFilter(removeAction);
1858             }
1859         }
1860 
1861         menu->addAction(m_actionCollection.action(QStringLiteral("emptyTrash")));
1862 
1863         menu->addSeparator();
1864 
1865         m_fileItemActions->addActionsTo(menu);
1866 
1867         // Copy To, Move To
1868         KSharedConfig::Ptr dolphin = KSharedConfig::openConfig(QStringLiteral("dolphinrc"));
1869         if (KConfigGroup(dolphin, QStringLiteral("General")).readEntry("ShowCopyMoveMenu", false)) {
1870             m_copyToMenu->setUrls(urls);
1871             m_copyToMenu->setReadOnly(!itemProperties.supportsMoving());
1872             m_copyToMenu->addActionsTo(menu);
1873             menu->addSeparator();
1874         }
1875 
1876         // Properties
1877         if (KPropertiesDialog::canDisplay(items)) {
1878             menu->addSeparator();
1879             QAction *act = new QAction(QIcon::fromTheme(QStringLiteral("document-properties")), i18n("&Properties"), menu);
1880             act->setShortcuts({Qt::ALT | Qt::Key_Return, Qt::ALT | Qt::Key_Enter});
1881             QObject::connect(act, &QAction::triggered, this, &FolderModel::openPropertiesDialog);
1882             menu->addAction(act);
1883         }
1884     }
1885 
1886     menu->setAttribute(Qt::WA_TranslucentBackground);
1887     menu->winId(); // force surface creation before ensurePolish call in menu::Popup which happens before show
1888 
1889     if (visualParent && menu->windowHandle()) {
1890         menu->windowHandle()->setTransientParent(visualParent->window());
1891     }
1892 
1893     menu->popup(m_menuPosition);
1894     connect(menu, &QMenu::aboutToHide, [this, menu]() {
1895         menu->deleteLater();
1896 
1897         // Remove the event filter for swapping delete and trash action from the QCoreApplication as it is no longer needed
1898         if (RemoveAction *removeAction = qobject_cast<RemoveAction *>(m_actionCollection.action(QStringLiteral("remove"))))
1899             QCoreApplication::instance()->removeEventFilter(removeAction);
1900     });
1901 }
1902 
1903 void FolderModel::openPropertiesDialog()
1904 {
1905     const QModelIndexList indexes = m_selectionModel->selectedIndexes();
1906 
1907     if (indexes.isEmpty()) {
1908         return;
1909     }
1910 
1911     KFileItemList items;
1912     items.reserve(indexes.count());
1913     for (const QModelIndex &index : indexes) {
1914         KFileItem item = itemForIndex(index);
1915         if (!item.isNull()) {
1916             items.append(item);
1917         }
1918     }
1919 
1920     if (!KPropertiesDialog::canDisplay(items)) {
1921         return;
1922     }
1923 
1924     KPropertiesDialog::showDialog(items, nullptr, false /*non modal*/);
1925 }
1926 
1927 void FolderModel::linkHere(const QUrl &sourceUrl)
1928 {
1929     KIO::CopyJob *job = KIO::link(sourceUrl, m_dirModel->dirLister()->url(), KIO::HideProgressInfo);
1930     KIO::FileUndoManager::self()->recordCopyJob(job);
1931 }
1932 
1933 QList<QUrl> FolderModel::selectedUrls() const
1934 {
1935     const auto indexes = m_selectionModel->selectedIndexes();
1936 
1937     QList<QUrl> urls;
1938     urls.reserve(indexes.count());
1939 
1940     for (const QModelIndex &index : indexes) {
1941         urls.append(itemForIndex(index).url());
1942     }
1943 
1944     return urls;
1945 }
1946 
1947 void FolderModel::copy()
1948 {
1949     if (!m_selectionModel->hasSelection()) {
1950         return;
1951     }
1952 
1953     if (QAction *action = m_actionCollection.action(QStringLiteral("copy"))) {
1954         if (!action->isEnabled()) {
1955             return;
1956         }
1957     }
1958 
1959     QMimeData *mimeData = QSortFilterProxyModel::mimeData(m_selectionModel->selectedIndexes());
1960     QApplication::clipboard()->setMimeData(mimeData);
1961 }
1962 
1963 void FolderModel::cut()
1964 {
1965     if (!m_selectionModel->hasSelection()) {
1966         return;
1967     }
1968 
1969     if (QAction *action = m_actionCollection.action(QStringLiteral("cut"))) {
1970         if (!action->isEnabled()) {
1971             return;
1972         }
1973     }
1974 
1975     QMimeData *mimeData = QSortFilterProxyModel::mimeData(m_selectionModel->selectedIndexes());
1976     KIO::setClipboardDataCut(mimeData, true);
1977     QApplication::clipboard()->setMimeData(mimeData);
1978 }
1979 
1980 void FolderModel::paste()
1981 {
1982     if (QAction *action = m_actionCollection.action(QStringLiteral("paste"))) {
1983         if (!action->isEnabled()) {
1984             return;
1985         }
1986     }
1987 
1988     KIO::PasteJob *job = KIO::paste(QApplication::clipboard()->mimeData(), m_dirModel->dirLister()->url());
1989     auto delegate = new KNotificationJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled);
1990     job->setUiDelegate(delegate);
1991 }
1992 
1993 void FolderModel::pasteTo()
1994 {
1995     const QList<QUrl> urls = selectedUrls();
1996     Q_ASSERT(urls.count() == 1);
1997     KIO::paste(QApplication::clipboard()->mimeData(), urls.first());
1998 }
1999 
2000 void FolderModel::refresh()
2001 {
2002     m_errorString.clear();
2003     Q_EMIT errorStringChanged();
2004 
2005     m_dirModel->dirLister()->updateDirectory(m_dirModel->dirLister()->url());
2006 }
2007 
2008 Plasma::Applet *FolderModel::applet() const
2009 {
2010     return m_applet;
2011 }
2012 
2013 void FolderModel::setApplet(Plasma::Applet *applet)
2014 {
2015     if (m_applet != applet) {
2016         Q_ASSERT(!m_applet);
2017 
2018         m_applet = applet;
2019 
2020         if (applet) {
2021             Plasma::Containment *containment = applet->containment();
2022 
2023             if (containment) {
2024                 Plasma::Corona *corona = containment->corona();
2025 
2026                 if (corona) {
2027                     connect(corona, &Plasma::Corona::screenRemoved, this, [this](int screenId) {
2028                         if (m_screen == screenId) {
2029                             m_screenMapper->removeScreen(screenId, m_currentActivity, resolvedUrl());
2030                         }
2031                     });
2032                     connect(corona, &Plasma::Corona::screenAdded, this, [this](int screenId) {
2033                         if (m_screen == screenId) {
2034                             m_screenMapper->addScreen(screenId, m_currentActivity, resolvedUrl());
2035                         }
2036                     });
2037                     m_screenMapper->setCorona(corona);
2038                 }
2039                 setScreen(containment->screen());
2040                 connect(containment, &Plasma::Containment::screenChanged, this, &FolderModel::setScreen);
2041             }
2042         }
2043 
2044         Q_EMIT appletChanged();
2045     }
2046 }
2047 
2048 bool FolderModel::showHiddenFiles() const
2049 {
2050     return m_showHiddenFiles;
2051 }
2052 
2053 void FolderModel::setShowHiddenFiles(bool enable)
2054 {
2055     if (m_showHiddenFiles != enable) {
2056         m_showHiddenFiles = enable;
2057         m_dirModel->dirLister()->setShowHiddenFiles(enable);
2058         m_dirModel->dirLister()->emitChanges();
2059 
2060         Q_EMIT showHiddenFilesChanged();
2061     }
2062 }
2063 
2064 void FolderModel::moveSelectedToTrash()
2065 {
2066     if (!m_selectionModel->hasSelection()) {
2067         return;
2068     }
2069 
2070     if (!isDeleteCommandShown()) {
2071         if (RemoveAction *action = qobject_cast<RemoveAction *>(m_actionCollection.action(QStringLiteral("remove")))) {
2072             if (action->proxyAction() != m_actionCollection.action(QStringLiteral("trash"))) {
2073                 return;
2074             }
2075         }
2076     }
2077 
2078     if (QAction *action = m_actionCollection.action(QStringLiteral("trash"))) {
2079         if (!action->isEnabled()) {
2080             return;
2081         }
2082     }
2083 
2084     using Iface = KIO::AskUserActionInterface;
2085     auto *job = new KIO::DeleteOrTrashJob(selectedUrls(), Iface::Trash, Iface::DefaultConfirmation, this);
2086     job->start();
2087 }
2088 
2089 void FolderModel::deleteSelected()
2090 {
2091     if (!m_selectionModel->hasSelection()) {
2092         return;
2093     }
2094 
2095     if (QAction *action = m_actionCollection.action(QStringLiteral("del"))) {
2096         if (!action->isEnabled()) {
2097             return;
2098         }
2099     }
2100 
2101     using Iface = KIO::AskUserActionInterface;
2102     auto *job = new KIO::DeleteOrTrashJob(selectedUrls(), Iface::Delete, Iface::DefaultConfirmation, this);
2103     job->start();
2104 }
2105 
2106 void FolderModel::undo()
2107 {
2108     if (QAction *action = m_actionCollection.action(QStringLiteral("undo"))) {
2109         // trigger() doesn't check enabled and would crash if invoked nonetheless.
2110         if (action->isEnabled()) {
2111             action->trigger();
2112         }
2113     }
2114 }
2115 
2116 void FolderModel::emptyTrashBin()
2117 {
2118     using Iface = KIO::AskUserActionInterface;
2119     auto *job = new KIO::DeleteOrTrashJob({}, Iface::EmptyTrash, Iface::DefaultConfirmation, this);
2120     job->start();
2121 }
2122 
2123 void FolderModel::restoreSelectedFromTrash()
2124 {
2125     if (!m_selectionModel->hasSelection()) {
2126         return;
2127     }
2128 
2129     const auto &urls = selectedUrls();
2130 
2131     KIO::RestoreJob *job = KIO::restoreFromTrash(urls);
2132     job->uiDelegate()->setAutoErrorHandlingEnabled(true);
2133 }
2134 
2135 bool FolderModel::isTrashEmpty()
2136 {
2137     KConfig trashConfig(QStringLiteral("trashrc"), KConfig::SimpleConfig);
2138     return trashConfig.group(QStringLiteral("Status")).readEntry("Empty", true);
2139 }
2140 
2141 void FolderModel::undoTextChanged(const QString &text)
2142 {
2143     if (QAction *action = m_actionCollection.action(QStringLiteral("undo"))) {
2144         action->setText(text);
2145     }
2146 }
2147 
2148 void FolderModel::createFolder()
2149 {
2150     m_newMenu->setWorkingDirectory(m_dirModel->dirLister()->url());
2151     m_newMenu->createDirectory();
2152 }
2153 
2154 bool FolderModel::isDeleteCommandShown()
2155 {
2156     KConfigGroup cg(KSharedConfig::openConfig(), QStringLiteral("KDE"));
2157     return cg.readEntry("ShowDeleteCommand", false);
2158 }