File indexing completed on 2024-04-21 03:55:20

0001 /*
0002     This file is part of the KDE project
0003     SPDX-FileCopyrightText: 2007 Kevin Ottens <ervin@kde.org>
0004     SPDX-FileCopyrightText: 2007 David Faure <faure@kde.org>
0005     SPDX-FileCopyrightText: 2023 Harald Sitter <sitter@kde.org>
0006 
0007     SPDX-License-Identifier: LGPL-2.0-only
0008 */
0009 
0010 #include "kfileplacesmodel.h"
0011 #include "kfileplacesitem_p.h"
0012 #include "kfileplacesmodel_p.h"
0013 
0014 #include <KCoreDirLister>
0015 #include <KLazyLocalizedString>
0016 #include <KListOpenFilesJob>
0017 #include <KLocalizedString>
0018 #include <commandlauncherjob.h>
0019 #include <kfileitem.h>
0020 #include <kio/statjob.h>
0021 #include <kprotocolinfo.h>
0022 
0023 #include <KBookmarkManager>
0024 #include <KConfig>
0025 #include <KConfigGroup>
0026 #include <KUrlMimeData>
0027 
0028 #include <solid/block.h>
0029 #include <solid/devicenotifier.h>
0030 #include <solid/opticaldisc.h>
0031 #include <solid/opticaldrive.h>
0032 #include <solid/portablemediaplayer.h>
0033 #include <solid/predicate.h>
0034 #include <solid/storageaccess.h>
0035 #include <solid/storagedrive.h>
0036 #include <solid/storagevolume.h>
0037 
0038 #include <QAction>
0039 #include <QCoreApplication>
0040 #include <QDebug>
0041 #include <QDir>
0042 #include <QFile>
0043 #include <QMimeData>
0044 #include <QMimeDatabase>
0045 #include <QStandardPaths>
0046 #include <QTimer>
0047 
0048 namespace
0049 {
0050 QString stateNameForGroupType(KFilePlacesModel::GroupType type)
0051 {
0052     switch (type) {
0053     case KFilePlacesModel::PlacesType:
0054         return QStringLiteral("GroupState-Places-IsHidden");
0055     case KFilePlacesModel::RemoteType:
0056         return QStringLiteral("GroupState-Remote-IsHidden");
0057     case KFilePlacesModel::RecentlySavedType:
0058         return QStringLiteral("GroupState-RecentlySaved-IsHidden");
0059     case KFilePlacesModel::SearchForType:
0060         return QStringLiteral("GroupState-SearchFor-IsHidden");
0061     case KFilePlacesModel::DevicesType:
0062         return QStringLiteral("GroupState-Devices-IsHidden");
0063     case KFilePlacesModel::RemovableDevicesType:
0064         return QStringLiteral("GroupState-RemovableDevices-IsHidden");
0065     case KFilePlacesModel::TagsType:
0066         return QStringLiteral("GroupState-Tags-IsHidden");
0067     default:
0068         Q_UNREACHABLE();
0069     }
0070 }
0071 
0072 static bool isFileIndexingEnabled()
0073 {
0074     KConfig config(QStringLiteral("baloofilerc"));
0075     KConfigGroup basicSettings = config.group(QStringLiteral("Basic Settings"));
0076     return basicSettings.readEntry("Indexing-Enabled", true);
0077 }
0078 
0079 static QString timelineDateString(int year, int month, int day = 0)
0080 {
0081     const QString dateFormat = QStringLiteral("%1-%2");
0082 
0083     QString date = dateFormat.arg(year).arg(month, 2, 10, QLatin1Char('0'));
0084     if (day > 0) {
0085         date += QStringLiteral("-%1").arg(day, 2, 10, QLatin1Char('0'));
0086     }
0087     return date;
0088 }
0089 
0090 static QUrl createTimelineUrl(const QUrl &url)
0091 {
0092     // based on dolphin urls
0093     const QString timelinePrefix = QLatin1String("timeline:") + QLatin1Char('/');
0094     QUrl timelineUrl;
0095 
0096     const QString path = url.toDisplayString(QUrl::PreferLocalFile);
0097     if (path.endsWith(QLatin1String("/yesterday"))) {
0098         const QDate date = QDate::currentDate().addDays(-1);
0099         const int year = date.year();
0100         const int month = date.month();
0101         const int day = date.day();
0102 
0103         timelineUrl = QUrl(timelinePrefix + timelineDateString(year, month) + QLatin1Char('/') + timelineDateString(year, month, day));
0104     } else if (path.endsWith(QLatin1String("/thismonth"))) {
0105         const QDate date = QDate::currentDate();
0106         timelineUrl = QUrl(timelinePrefix + timelineDateString(date.year(), date.month()));
0107     } else if (path.endsWith(QLatin1String("/lastmonth"))) {
0108         const QDate date = QDate::currentDate().addMonths(-1);
0109         timelineUrl = QUrl(timelinePrefix + timelineDateString(date.year(), date.month()));
0110     } else {
0111         Q_ASSERT(path.endsWith(QLatin1String("/today")));
0112         timelineUrl = url;
0113     }
0114 
0115     return timelineUrl;
0116 }
0117 
0118 static QUrl createSearchUrl(const QUrl &url)
0119 {
0120     QUrl searchUrl = url;
0121 
0122     const QString path = url.toDisplayString(QUrl::PreferLocalFile);
0123 
0124     const QStringList validSearchPaths = {QStringLiteral("/documents"), QStringLiteral("/images"), QStringLiteral("/audio"), QStringLiteral("/videos")};
0125 
0126     for (const QString &validPath : validSearchPaths) {
0127         if (path.endsWith(validPath)) {
0128             searchUrl.setScheme(QStringLiteral("baloosearch"));
0129             return searchUrl;
0130         }
0131     }
0132 
0133     qWarning() << "Invalid search url:" << url;
0134 
0135     return searchUrl;
0136 }
0137 }
0138 
0139 KFilePlacesModelPrivate::KFilePlacesModelPrivate(KFilePlacesModel *qq)
0140     : q(qq)
0141     , fileIndexingEnabled(isFileIndexingEnabled())
0142     , tagsLister(new KCoreDirLister(q))
0143 {
0144     if (KProtocolInfo::isKnownProtocol(QStringLiteral("tags"))) {
0145         QObject::connect(tagsLister, &KCoreDirLister::itemsAdded, q, [this](const QUrl &, const KFileItemList &items) {
0146             if (tags.isEmpty()) {
0147                 QList<QUrl> existingBookmarks;
0148 
0149                 KBookmarkGroup root = bookmarkManager->root();
0150                 KBookmark bookmark = root.first();
0151 
0152                 while (!bookmark.isNull()) {
0153                     existingBookmarks.append(bookmark.url());
0154                     bookmark = root.next(bookmark);
0155                 }
0156 
0157                 if (!existingBookmarks.contains(QUrl(tagsUrlBase))) {
0158                     KBookmark alltags = KFilePlacesItem::createSystemBookmark(bookmarkManager,
0159                                                                               kli18nc("KFile System Bookmarks", "All tags").untranslatedText(),
0160                                                                               QUrl(tagsUrlBase),
0161                                                                               QStringLiteral("tag"));
0162                 }
0163             }
0164 
0165             for (const KFileItem &item : items) {
0166                 const QString name = item.name();
0167 
0168                 if (!tags.contains(name)) {
0169                     tags.append(name);
0170                 }
0171             }
0172             reloadBookmarks();
0173         });
0174 
0175         QObject::connect(tagsLister, &KCoreDirLister::itemsDeleted, q, [this](const KFileItemList &items) {
0176             for (const KFileItem &item : items) {
0177                 tags.removeAll(item.name());
0178             }
0179             reloadBookmarks();
0180         });
0181 
0182         tagsLister->openUrl(QUrl(tagsUrlBase), KCoreDirLister::OpenUrlFlag::Reload);
0183     }
0184 }
0185 
0186 QString KFilePlacesModelPrivate::ignoreMimeType()
0187 {
0188     return QStringLiteral("application/x-kfileplacesmodel-ignore");
0189 }
0190 
0191 QString KFilePlacesModelPrivate::internalMimeType(const KFilePlacesModel *model)
0192 {
0193     return QStringLiteral("application/x-kfileplacesmodel-") + QString::number(reinterpret_cast<qptrdiff>(model));
0194 }
0195 
0196 KBookmark KFilePlacesModel::bookmarkForUrl(const QUrl &searchUrl) const
0197 {
0198     KBookmarkGroup root = d->bookmarkManager->root();
0199     KBookmark current = root.first();
0200     while (!current.isNull()) {
0201         if (current.url() == searchUrl) {
0202             return current;
0203         }
0204         current = root.next(current);
0205     }
0206     return KBookmark();
0207 }
0208 
0209 static inline QString versionKey()
0210 {
0211     return QStringLiteral("kde_places_version");
0212 }
0213 
0214 KFilePlacesModel::KFilePlacesModel(QObject *parent)
0215     : QAbstractItemModel(parent)
0216     , d(new KFilePlacesModelPrivate(this))
0217 {
0218     const QString file = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1String("/user-places.xbel");
0219     d->bookmarkManager = new KBookmarkManager(file, this);
0220 
0221     // Let's put some places in there if it's empty.
0222     KBookmarkGroup root = d->bookmarkManager->root();
0223 
0224     const auto setDefaultMetadataItemForGroup = [&root](KFilePlacesModel::GroupType type) {
0225         root.setMetaDataItem(stateNameForGroupType(type), QStringLiteral("false"));
0226     };
0227 
0228     // Increase this version number and use the following logic to handle the update process for existing installations.
0229     static const int s_currentVersion = 4;
0230 
0231     const bool newFile = root.first().isNull() || !QFile::exists(file);
0232     const int fileVersion = root.metaDataItem(versionKey()).toInt();
0233 
0234     if (newFile || fileVersion < s_currentVersion) {
0235         root.setMetaDataItem(versionKey(), QString::number(s_currentVersion));
0236 
0237         const QList<QUrl> seenUrls = root.groupUrlList();
0238 
0239         /* clang-format off */
0240         auto createSystemBookmark =
0241             [this, &seenUrls](const char *untranslatedLabel,
0242                               const QUrl &url,
0243                               const QString &iconName,
0244                               const KBookmark &after) {
0245                 if (!seenUrls.contains(url)) {
0246                     return KFilePlacesItem::createSystemBookmark(d->bookmarkManager, untranslatedLabel, url, iconName, after);
0247                 }
0248                 return KBookmark();
0249             };
0250         /* clang-format on */
0251 
0252         if (fileVersion < 2) {
0253             // NOTE: The context for these kli18nc calls has to be "KFile System Bookmarks".
0254             // The real i18nc call is made later, with this context, so the two must match.
0255             createSystemBookmark(kli18nc("KFile System Bookmarks", "Home").untranslatedText(),
0256                                  QUrl::fromLocalFile(QDir::homePath()),
0257                                  QStringLiteral("user-home"),
0258                                  KBookmark());
0259 
0260             // Some distros may not create various standard XDG folders by default
0261             // so check for their existence before adding bookmarks for them
0262             const QString desktopFolder = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation);
0263             if (QDir(desktopFolder).exists()) {
0264                 createSystemBookmark(kli18nc("KFile System Bookmarks", "Desktop").untranslatedText(),
0265                                      QUrl::fromLocalFile(desktopFolder),
0266                                      QStringLiteral("user-desktop"),
0267                                      KBookmark());
0268             }
0269             const QString documentsFolder = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation);
0270             if (QDir(documentsFolder).exists()) {
0271                 createSystemBookmark(kli18nc("KFile System Bookmarks", "Documents").untranslatedText(),
0272                                      QUrl::fromLocalFile(documentsFolder),
0273                                      QStringLiteral("folder-documents"),
0274                                      KBookmark());
0275             }
0276             const QString downloadFolder = QStandardPaths::writableLocation(QStandardPaths::DownloadLocation);
0277             if (QDir(downloadFolder).exists()) {
0278                 createSystemBookmark(kli18nc("KFile System Bookmarks", "Downloads").untranslatedText(),
0279                                      QUrl::fromLocalFile(downloadFolder),
0280                                      QStringLiteral("folder-downloads"),
0281                                      KBookmark());
0282             }
0283             createSystemBookmark(kli18nc("KFile System Bookmarks", "Network").untranslatedText(),
0284                                  QUrl(QStringLiteral("remote:/")),
0285                                  QStringLiteral("folder-network"),
0286                                  KBookmark());
0287 
0288             createSystemBookmark(kli18nc("KFile System Bookmarks", "Trash").untranslatedText(),
0289                                  QUrl(QStringLiteral("trash:/")),
0290                                  QStringLiteral("user-trash"),
0291                                  KBookmark());
0292         }
0293 
0294         if (!newFile && fileVersion < 3) {
0295             KBookmarkGroup rootGroup = d->bookmarkManager->root();
0296             KBookmark bItem = rootGroup.first();
0297             while (!bItem.isNull()) {
0298                 KBookmark nextbItem = rootGroup.next(bItem);
0299                 const bool isSystemItem = bItem.metaDataItem(QStringLiteral("isSystemItem")) == QLatin1String("true");
0300                 if (isSystemItem) {
0301                     const QString text = bItem.fullText();
0302                     // Because of b8a4c2223453932202397d812a0c6b30c6186c70 we need to find the system bookmark named Audio Files
0303                     // and rename it to Audio, otherwise users are getting untranslated strings
0304                     if (text == QLatin1String("Audio Files")) {
0305                         bItem.setFullText(QStringLiteral("Audio"));
0306                     } else if (text == QLatin1String("Today")) {
0307                         // Because of 19feef732085b444515da3f6c66f3352bbcb1824 we need to find the system bookmark named Today
0308                         // and rename it to Modified Today, otherwise users are getting untranslated strings
0309                         bItem.setFullText(QStringLiteral("Modified Today"));
0310                     } else if (text == QLatin1String("Yesterday")) {
0311                         // Because of 19feef732085b444515da3f6c66f3352bbcb1824 we need to find the system bookmark named Yesterday
0312                         // and rename it to Modified Yesterday, otherwise users are getting untranslated strings
0313                         bItem.setFullText(QStringLiteral("Modified Yesterday"));
0314                     } else if (text == QLatin1String("This Month")) {
0315                         // Because of 7e1d2fb84546506c91684dd222c2485f0783848f we need to find the system bookmark named This Month
0316                         // and remove it, otherwise users are getting untranslated strings
0317                         rootGroup.deleteBookmark(bItem);
0318                     } else if (text == QLatin1String("Last Month")) {
0319                         // Because of 7e1d2fb84546506c91684dd222c2485f0783848f we need to find the system bookmark named Last Month
0320                         // and remove it, otherwise users are getting untranslated strings
0321                         rootGroup.deleteBookmark(bItem);
0322                     }
0323                 }
0324 
0325                 bItem = nextbItem;
0326             }
0327         }
0328         if (fileVersion < 4) {
0329             auto findSystemBookmark = [this](const QString &untranslatedText) {
0330                 KBookmarkGroup root = d->bookmarkManager->root();
0331                 KBookmark bItem = root.first();
0332                 while (!bItem.isNull()) {
0333                     const bool isSystemItem = bItem.metaDataItem(QStringLiteral("isSystemItem")) == QLatin1String("true");
0334                     if (isSystemItem && bItem.fullText() == untranslatedText) {
0335                         return bItem;
0336                     }
0337                     bItem = root.next(bItem);
0338                 }
0339                 return KBookmark();
0340             };
0341             // This variable is used to insert the new bookmarks at the correct place starting after the "Downloads"
0342             // bookmark. When the user already has some of the bookmarks set up manually, the createSystemBookmark()
0343             // function returns an empty KBookmark so the following entries will be added at the end of the bookmark
0344             // section to not mess with the users setup.
0345             KBookmark after = findSystemBookmark(QLatin1String("Downloads"));
0346 
0347             const QString musicFolder = QStandardPaths::writableLocation(QStandardPaths::MusicLocation);
0348             if (QDir(musicFolder).exists()) {
0349                 after = createSystemBookmark(kli18nc("KFile System Bookmarks", "Music").untranslatedText(),
0350                                              QUrl::fromLocalFile(musicFolder),
0351                                              QStringLiteral("folder-music"),
0352                                              after);
0353             }
0354             const QString pictureFolder = QStandardPaths::writableLocation(QStandardPaths::PicturesLocation);
0355             if (QDir(pictureFolder).exists()) {
0356                 after = createSystemBookmark(kli18nc("KFile System Bookmarks", "Pictures").untranslatedText(),
0357                                              QUrl::fromLocalFile(pictureFolder),
0358                                              QStringLiteral("folder-pictures"),
0359                                              after);
0360             }
0361             // Choosing the name "Videos" instead of "Movies", since that is how the folder
0362             // is called normally on Linux: https://cgit.freedesktop.org/xdg/xdg-user-dirs/tree/user-dirs.defaults
0363             const QString videoFolder = QStandardPaths::writableLocation(QStandardPaths::MoviesLocation);
0364             if (QDir(videoFolder).exists()) {
0365                 after = createSystemBookmark(kli18nc("KFile System Bookmarks", "Videos").untranslatedText(),
0366                                              QUrl::fromLocalFile(videoFolder),
0367                                              QStringLiteral("folder-videos"),
0368                                              after);
0369             }
0370         }
0371 
0372         if (newFile) {
0373             setDefaultMetadataItemForGroup(PlacesType);
0374             setDefaultMetadataItemForGroup(RemoteType);
0375             setDefaultMetadataItemForGroup(DevicesType);
0376             setDefaultMetadataItemForGroup(RemovableDevicesType);
0377             setDefaultMetadataItemForGroup(TagsType);
0378         }
0379 
0380         // Force bookmarks to be saved. If on open/save dialog and the bookmarks are not saved, QFile::exists
0381         // will always return false, which opening/closing all the time the open/save dialog would cause the
0382         // bookmarks to be added once each time, having lots of times each bookmark. (ereslibre)
0383         d->bookmarkManager->saveAs(file);
0384     }
0385 
0386     // Add a Recently Used entry if available (it comes from kio-extras)
0387     if (qEnvironmentVariableIsSet("KDE_FULL_SESSION") && KProtocolInfo::isKnownProtocol(QStringLiteral("recentlyused"))
0388         && root.metaDataItem(QStringLiteral("withRecentlyUsed")) != QLatin1String("true")) {
0389         root.setMetaDataItem(QStringLiteral("withRecentlyUsed"), QStringLiteral("true"));
0390 
0391         KBookmark recentFilesBookmark = KFilePlacesItem::createSystemBookmark(d->bookmarkManager,
0392                                                                               kli18nc("KFile System Bookmarks", "Recent Files").untranslatedText(),
0393                                                                               QUrl(QStringLiteral("recentlyused:/files")),
0394                                                                               QStringLiteral("document-open-recent"));
0395 
0396         KBookmark recentDirectoriesBookmark = KFilePlacesItem::createSystemBookmark(d->bookmarkManager,
0397                                                                                     kli18nc("KFile System Bookmarks", "Recent Locations").untranslatedText(),
0398                                                                                     QUrl(QStringLiteral("recentlyused:/locations")),
0399                                                                                     QStringLiteral("folder-open-recent"));
0400 
0401         setDefaultMetadataItemForGroup(RecentlySavedType);
0402 
0403         // Move The recently used bookmarks below the trash, making it the first element in the Recent group
0404         KBookmark trashBookmark = bookmarkForUrl(QUrl(QStringLiteral("trash:/")));
0405         if (!trashBookmark.isNull()) {
0406             root.moveBookmark(recentFilesBookmark, trashBookmark);
0407             root.moveBookmark(recentDirectoriesBookmark, recentFilesBookmark);
0408         }
0409 
0410         d->bookmarkManager->save();
0411     }
0412 
0413     // if baloo is enabled, add new urls even if the bookmark file is not empty
0414     if (d->fileIndexingEnabled && root.metaDataItem(QStringLiteral("withBaloo")) != QLatin1String("true")) {
0415         root.setMetaDataItem(QStringLiteral("withBaloo"), QStringLiteral("true"));
0416 
0417         // don't add by default "Modified Today" and "Modified Yesterday" when recentlyused:/ is present
0418         if (root.metaDataItem(QStringLiteral("withRecentlyUsed")) != QLatin1String("true")) {
0419             KFilePlacesItem::createSystemBookmark(d->bookmarkManager,
0420                                                   kli18nc("KFile System Bookmarks", "Modified Today").untranslatedText(),
0421                                                   QUrl(QStringLiteral("timeline:/today")),
0422                                                   QStringLiteral("go-jump-today"));
0423             KFilePlacesItem::createSystemBookmark(d->bookmarkManager,
0424                                                   kli18nc("KFile System Bookmarks", "Modified Yesterday").untranslatedText(),
0425                                                   QUrl(QStringLiteral("timeline:/yesterday")),
0426                                                   QStringLiteral("view-calendar-day"));
0427         }
0428 
0429         setDefaultMetadataItemForGroup(SearchForType);
0430         setDefaultMetadataItemForGroup(RecentlySavedType);
0431 
0432         d->bookmarkManager->save();
0433     }
0434 
0435     QString predicate(
0436         QString::fromLatin1("[[[[ StorageVolume.ignored == false AND [ StorageVolume.usage == 'FileSystem' OR StorageVolume.usage == 'Encrypted' ]]"
0437                             " OR "
0438                             "[ IS StorageAccess AND StorageDrive.driveType == 'Floppy' ]]"
0439                             " OR "
0440                             "OpticalDisc.availableContent & 'Audio' ]"
0441                             " OR "
0442                             "StorageAccess.ignored == false ]"));
0443 
0444     if (KProtocolInfo::isKnownProtocol(QStringLiteral("mtp"))) {
0445         predicate = QLatin1Char('[') + predicate + QLatin1String(" OR PortableMediaPlayer.supportedProtocols == 'mtp']");
0446     }
0447     if (KProtocolInfo::isKnownProtocol(QStringLiteral("afc"))) {
0448         predicate = QLatin1Char('[') + predicate + QLatin1String(" OR PortableMediaPlayer.supportedProtocols == 'afc']");
0449     }
0450 
0451     d->predicate = Solid::Predicate::fromString(predicate);
0452 
0453     Q_ASSERT(d->predicate.isValid());
0454 
0455     connect(d->bookmarkManager, &KBookmarkManager::changed, this, [this]() {
0456         d->reloadBookmarks();
0457     });
0458 
0459     d->reloadBookmarks();
0460     QTimer::singleShot(0, this, [this]() {
0461         d->initDeviceList();
0462     });
0463 }
0464 
0465 KFilePlacesModel::~KFilePlacesModel() = default;
0466 
0467 QUrl KFilePlacesModel::url(const QModelIndex &index) const
0468 {
0469     return data(index, UrlRole).toUrl();
0470 }
0471 
0472 bool KFilePlacesModel::setupNeeded(const QModelIndex &index) const
0473 {
0474     return data(index, SetupNeededRole).toBool();
0475 }
0476 
0477 QIcon KFilePlacesModel::icon(const QModelIndex &index) const
0478 {
0479     return data(index, Qt::DecorationRole).value<QIcon>();
0480 }
0481 
0482 QString KFilePlacesModel::text(const QModelIndex &index) const
0483 {
0484     return data(index, Qt::DisplayRole).toString();
0485 }
0486 
0487 bool KFilePlacesModel::isHidden(const QModelIndex &index) const
0488 {
0489     // Note: we do not want to show an index if its parent is hidden
0490     return data(index, HiddenRole).toBool() || isGroupHidden(index);
0491 }
0492 
0493 bool KFilePlacesModel::isGroupHidden(const GroupType type) const
0494 {
0495     const QString hidden = d->bookmarkManager->root().metaDataItem(stateNameForGroupType(type));
0496     return hidden == QLatin1String("true");
0497 }
0498 
0499 bool KFilePlacesModel::isGroupHidden(const QModelIndex &index) const
0500 {
0501     if (!index.isValid()) {
0502         return false;
0503     }
0504 
0505     KFilePlacesItem *item = static_cast<KFilePlacesItem *>(index.internalPointer());
0506     return isGroupHidden(item->groupType());
0507 }
0508 
0509 bool KFilePlacesModel::isDevice(const QModelIndex &index) const
0510 {
0511     if (!index.isValid()) {
0512         return false;
0513     }
0514 
0515     KFilePlacesItem *item = static_cast<KFilePlacesItem *>(index.internalPointer());
0516 
0517     return item->isDevice();
0518 }
0519 
0520 bool KFilePlacesModel::isTeardownAllowed(const QModelIndex &index) const
0521 {
0522     if (!index.isValid()) {
0523         return false;
0524     }
0525 
0526     KFilePlacesItem *item = static_cast<KFilePlacesItem *>(index.internalPointer());
0527     return item->isTeardownAllowed();
0528 }
0529 
0530 bool KFilePlacesModel::isEjectAllowed(const QModelIndex &index) const
0531 {
0532     if (!index.isValid()) {
0533         return false;
0534     }
0535 
0536     KFilePlacesItem *item = static_cast<KFilePlacesItem *>(index.internalPointer());
0537     return item->isEjectAllowed();
0538 }
0539 
0540 bool KFilePlacesModel::isTeardownOverlayRecommended(const QModelIndex &index) const
0541 {
0542     if (!index.isValid()) {
0543         return false;
0544     }
0545 
0546     KFilePlacesItem *item = static_cast<KFilePlacesItem *>(index.internalPointer());
0547     return item->isTeardownOverlayRecommended();
0548 }
0549 
0550 KFilePlacesModel::DeviceAccessibility KFilePlacesModel::deviceAccessibility(const QModelIndex &index) const
0551 {
0552     if (!index.isValid()) {
0553         return KFilePlacesModel::Accessible;
0554     }
0555 
0556     KFilePlacesItem *item = static_cast<KFilePlacesItem *>(index.internalPointer());
0557     return item->deviceAccessibility();
0558 }
0559 
0560 Solid::Device KFilePlacesModel::deviceForIndex(const QModelIndex &index) const
0561 {
0562     if (!index.isValid()) {
0563         return Solid::Device();
0564     }
0565 
0566     KFilePlacesItem *item = static_cast<KFilePlacesItem *>(index.internalPointer());
0567 
0568     if (item->isDevice()) {
0569         return item->device();
0570     } else {
0571         return Solid::Device();
0572     }
0573 }
0574 
0575 KBookmark KFilePlacesModel::bookmarkForIndex(const QModelIndex &index) const
0576 {
0577     if (!index.isValid()) {
0578         return KBookmark();
0579     }
0580 
0581     KFilePlacesItem *item = static_cast<KFilePlacesItem *>(index.internalPointer());
0582     return item->bookmark();
0583 }
0584 
0585 KFilePlacesModel::GroupType KFilePlacesModel::groupType(const QModelIndex &index) const
0586 {
0587     if (!index.isValid()) {
0588         return UnknownType;
0589     }
0590 
0591     KFilePlacesItem *item = static_cast<KFilePlacesItem *>(index.internalPointer());
0592     return item->groupType();
0593 }
0594 
0595 QModelIndexList KFilePlacesModel::groupIndexes(const KFilePlacesModel::GroupType type) const
0596 {
0597     if (type == UnknownType) {
0598         return QModelIndexList();
0599     }
0600 
0601     QModelIndexList indexes;
0602     const int rows = rowCount();
0603     for (int row = 0; row < rows; ++row) {
0604         const QModelIndex current = index(row, 0);
0605         if (groupType(current) == type) {
0606             indexes << current;
0607         }
0608     }
0609 
0610     return indexes;
0611 }
0612 
0613 QVariant KFilePlacesModel::data(const QModelIndex &index, int role) const
0614 {
0615     if (!index.isValid()) {
0616         return QVariant();
0617     }
0618 
0619     KFilePlacesItem *item = static_cast<KFilePlacesItem *>(index.internalPointer());
0620     if (role == KFilePlacesModel::GroupHiddenRole) {
0621         return isGroupHidden(item->groupType());
0622     } else {
0623         return item->data(role);
0624     }
0625 }
0626 
0627 QModelIndex KFilePlacesModel::index(int row, int column, const QModelIndex &parent) const
0628 {
0629     if (row < 0 || column != 0 || row >= d->items.size()) {
0630         return QModelIndex();
0631     }
0632 
0633     if (parent.isValid()) {
0634         return QModelIndex();
0635     }
0636 
0637     return createIndex(row, column, d->items.at(row));
0638 }
0639 
0640 QModelIndex KFilePlacesModel::parent(const QModelIndex &child) const
0641 {
0642     Q_UNUSED(child);
0643     return QModelIndex();
0644 }
0645 
0646 QHash<int, QByteArray> KFilePlacesModel::roleNames() const
0647 {
0648     auto super = QAbstractItemModel::roleNames();
0649 
0650     super[UrlRole] = "url";
0651     super[HiddenRole] = "isHidden";
0652     super[SetupNeededRole] = "isSetupNeeded";
0653     super[FixedDeviceRole] = "isFixedDevice";
0654     super[CapacityBarRecommendedRole] = "isCapacityBarRecommended";
0655     super[GroupRole] = "group";
0656     super[IconNameRole] = "iconName";
0657     super[GroupHiddenRole] = "isGroupHidden";
0658     super[TeardownAllowedRole] = "isTeardownAllowed";
0659     super[EjectAllowedRole] = "isEjectAllowed";
0660     super[TeardownOverlayRecommendedRole] = "isTeardownOverlayRecommended";
0661     super[DeviceAccessibilityRole] = "deviceAccessibility";
0662 
0663     return super;
0664 }
0665 
0666 int KFilePlacesModel::rowCount(const QModelIndex &parent) const
0667 {
0668     if (parent.isValid()) {
0669         return 0;
0670     } else {
0671         return d->items.size();
0672     }
0673 }
0674 
0675 int KFilePlacesModel::columnCount(const QModelIndex &parent) const
0676 {
0677     Q_UNUSED(parent)
0678     // We only know 1 piece of information for a particular entry
0679     return 1;
0680 }
0681 
0682 QModelIndex KFilePlacesModel::closestItem(const QUrl &url) const
0683 {
0684     int foundRow = -1;
0685     int maxLength = 0;
0686 
0687     // Search the item which is equal to the URL or at least is a parent URL.
0688     // If there are more than one possible item URL candidates, choose the item
0689     // which covers the bigger range of the URL.
0690     for (int row = 0; row < d->items.size(); ++row) {
0691         KFilePlacesItem *item = d->items[row];
0692 
0693         if (item->isHidden() || isGroupHidden(item->groupType())) {
0694             continue;
0695         }
0696 
0697         const QUrl itemUrl = convertedUrl(item->data(UrlRole).toUrl());
0698 
0699         if (itemUrl.matches(url, QUrl::StripTrailingSlash)
0700             || (itemUrl.isParentOf(url) && itemUrl.query() == url.query() && itemUrl.fragment() == url.fragment())) {
0701             const int length = itemUrl.toString().length();
0702             if (length > maxLength) {
0703                 foundRow = row;
0704                 maxLength = length;
0705             }
0706         }
0707     }
0708 
0709     if (foundRow == -1) {
0710         return QModelIndex();
0711     } else {
0712         return createIndex(foundRow, 0, d->items[foundRow]);
0713     }
0714 }
0715 
0716 void KFilePlacesModelPrivate::initDeviceList()
0717 {
0718     Solid::DeviceNotifier *notifier = Solid::DeviceNotifier::instance();
0719 
0720     QObject::connect(notifier, &Solid::DeviceNotifier::deviceAdded, q, [this](const QString &device) {
0721         deviceAdded(device);
0722     });
0723     QObject::connect(notifier, &Solid::DeviceNotifier::deviceRemoved, q, [this](const QString &device) {
0724         deviceRemoved(device);
0725     });
0726 
0727     availableDevices = Solid::Device::listFromQuery(predicate);
0728 
0729     reloadBookmarks();
0730 }
0731 
0732 void KFilePlacesModelPrivate::deviceAdded(const QString &udi)
0733 {
0734     Solid::Device d(udi);
0735 
0736     if (predicate.matches(d)) {
0737         availableDevices << d;
0738         reloadBookmarks();
0739     }
0740 }
0741 
0742 void KFilePlacesModelPrivate::deviceRemoved(const QString &udi)
0743 {
0744     auto it = std::find_if(availableDevices.begin(), availableDevices.end(), [udi](const Solid::Device &device) {
0745         return device.udi() == udi;
0746     });
0747     if (it != availableDevices.end()) {
0748         availableDevices.erase(it);
0749         reloadBookmarks();
0750     }
0751 }
0752 
0753 void KFilePlacesModelPrivate::itemChanged(const QString &id, const QList<int> &roles)
0754 {
0755     for (int row = 0; row < items.size(); ++row) {
0756         if (items.at(row)->id() == id) {
0757             QModelIndex index = q->index(row, 0);
0758             Q_EMIT q->dataChanged(index, index, roles);
0759         }
0760     }
0761 }
0762 
0763 void KFilePlacesModelPrivate::reloadBookmarks()
0764 {
0765     QList<KFilePlacesItem *> currentItems = loadBookmarkList();
0766 
0767     QList<KFilePlacesItem *>::Iterator it_i = items.begin();
0768     QList<KFilePlacesItem *>::Iterator it_c = currentItems.begin();
0769 
0770     QList<KFilePlacesItem *>::Iterator end_i = items.end();
0771     QList<KFilePlacesItem *>::Iterator end_c = currentItems.end();
0772 
0773     while (it_i != end_i || it_c != end_c) {
0774         if (it_i == end_i && it_c != end_c) {
0775             int row = items.count();
0776 
0777             q->beginInsertRows(QModelIndex(), row, row);
0778             it_i = items.insert(it_i, *it_c);
0779             ++it_i;
0780             it_c = currentItems.erase(it_c);
0781 
0782             end_i = items.end();
0783             end_c = currentItems.end();
0784             q->endInsertRows();
0785 
0786         } else if (it_i != end_i && it_c == end_c) {
0787             int row = items.indexOf(*it_i);
0788 
0789             q->beginRemoveRows(QModelIndex(), row, row);
0790             delete *it_i;
0791             it_i = items.erase(it_i);
0792 
0793             end_i = items.end();
0794             end_c = currentItems.end();
0795             q->endRemoveRows();
0796 
0797         } else if ((*it_i)->id() == (*it_c)->id()) {
0798             bool shouldEmit = !((*it_i)->bookmark() == (*it_c)->bookmark());
0799             (*it_i)->setBookmark((*it_c)->bookmark());
0800             if (shouldEmit) {
0801                 int row = items.indexOf(*it_i);
0802                 QModelIndex idx = q->index(row, 0);
0803                 Q_EMIT q->dataChanged(idx, idx);
0804             }
0805             ++it_i;
0806             ++it_c;
0807         } else {
0808             int row = items.indexOf(*it_i);
0809 
0810             if (it_i + 1 != end_i && (*(it_i + 1))->id() == (*it_c)->id()) { // if the next one matches, it's a remove
0811                 q->beginRemoveRows(QModelIndex(), row, row);
0812                 delete *it_i;
0813                 it_i = items.erase(it_i);
0814 
0815                 end_i = items.end();
0816                 end_c = currentItems.end();
0817                 q->endRemoveRows();
0818             } else {
0819                 q->beginInsertRows(QModelIndex(), row, row);
0820                 it_i = items.insert(it_i, *it_c);
0821                 ++it_i;
0822                 it_c = currentItems.erase(it_c);
0823 
0824                 end_i = items.end();
0825                 end_c = currentItems.end();
0826                 q->endInsertRows();
0827             }
0828         }
0829     }
0830 
0831     qDeleteAll(currentItems);
0832     currentItems.clear();
0833 
0834     Q_EMIT q->reloaded();
0835 }
0836 
0837 bool KFilePlacesModelPrivate::isBalooUrl(const QUrl &url) const
0838 {
0839     const QString scheme = url.scheme();
0840     return ((scheme == QLatin1String("timeline")) || (scheme == QLatin1String("search")));
0841 }
0842 
0843 QList<KFilePlacesItem *> KFilePlacesModelPrivate::loadBookmarkList()
0844 {
0845     QList<KFilePlacesItem *> items;
0846 
0847     KBookmarkGroup root = bookmarkManager->root();
0848     KBookmark bookmark = root.first();
0849     QList<Solid::Device> devices{availableDevices};
0850     QList<QString> tagsList = tags;
0851 
0852     while (!bookmark.isNull()) {
0853         KFilePlacesItem *item = nullptr;
0854 
0855         if (const QString udi = bookmark.metaDataItem(QStringLiteral("UDI")); !udi.isEmpty()) {
0856             const QString uuid = bookmark.metaDataItem(QStringLiteral("uuid"));
0857             auto it = std::find_if(devices.begin(), devices.end(), [udi, uuid](const Solid::Device &device) {
0858                 if (!uuid.isEmpty()) {
0859                     auto storageVolume = device.as<Solid::StorageVolume>();
0860                     if (storageVolume && !storageVolume->uuid().isEmpty()) {
0861                         return storageVolume->uuid() == uuid;
0862                     }
0863                 }
0864 
0865                 return device.udi() == udi;
0866             });
0867             if (it != devices.end()) {
0868                 item = new KFilePlacesItem(bookmarkManager, bookmark.address(), it->udi(), q);
0869                 if (!item->hasSupportedScheme(supportedSchemes)) {
0870                     delete item;
0871                     item = nullptr;
0872                 }
0873                 devices.erase(it);
0874             }
0875         } else if (const QString tag = bookmark.metaDataItem(QStringLiteral("tag")); !tag.isEmpty()) {
0876             auto it = std::find(tagsList.begin(), tagsList.end(), tag);
0877             if (it != tagsList.end()) {
0878                 tagsList.erase(it);
0879                 item = new KFilePlacesItem(bookmarkManager, bookmark.address(), QString(), q);
0880             }
0881         } else if (const QUrl url = bookmark.url(); url.isValid()) {
0882             QString appName = bookmark.metaDataItem(QStringLiteral("OnlyInApp"));
0883             bool allowedHere = appName.isEmpty() || appName == QCoreApplication::instance()->applicationName();
0884             bool isSupportedUrl = isBalooUrl(url) ? fileIndexingEnabled : true;
0885             bool isSupportedScheme = supportedSchemes.isEmpty() || supportedSchemes.contains(url.scheme());
0886 
0887             if (isSupportedScheme && isSupportedUrl && allowedHere) {
0888                 // TODO: Update bookmark internal element
0889                 item = new KFilePlacesItem(bookmarkManager, bookmark.address(), QString(), q);
0890             }
0891         }
0892 
0893         if (item) {
0894             QObject::connect(item, &KFilePlacesItem::itemChanged, q, [this](const QString &id, const QList<int> &roles) {
0895                 itemChanged(id, roles);
0896             });
0897             items << item;
0898         }
0899 
0900         bookmark = root.next(bookmark);
0901     }
0902 
0903     // Add bookmarks for the remaining devices, they were previously unknown
0904     for (const Solid::Device &device : std::as_const(devices)) {
0905         bookmark = KFilePlacesItem::createDeviceBookmark(bookmarkManager, device);
0906         if (!bookmark.isNull()) {
0907             KFilePlacesItem *item = new KFilePlacesItem(bookmarkManager, bookmark.address(), device.udi(), q);
0908             QObject::connect(item, &KFilePlacesItem::itemChanged, q, [this](const QString &id, const QList<int> &roles) {
0909                 itemChanged(id, roles);
0910             });
0911             // TODO: Update bookmark internal element
0912             items << item;
0913         }
0914     }
0915 
0916     for (const QString &tag : tagsList) {
0917         bookmark = KFilePlacesItem::createTagBookmark(bookmarkManager, tag);
0918         if (!bookmark.isNull()) {
0919             KFilePlacesItem *item = new KFilePlacesItem(bookmarkManager, bookmark.address(), tag, q);
0920             QObject::connect(item, &KFilePlacesItem::itemChanged, q, [this](const QString &id, const QList<int> &roles) {
0921                 itemChanged(id, roles);
0922             });
0923             items << item;
0924         }
0925     }
0926 
0927     // return a sorted list based on groups
0928     std::stable_sort(items.begin(), items.end(), [](KFilePlacesItem *itemA, KFilePlacesItem *itemB) {
0929         return (itemA->groupType() < itemB->groupType());
0930     });
0931 
0932     return items;
0933 }
0934 
0935 int KFilePlacesModelPrivate::findNearestPosition(int source, int target)
0936 {
0937     const KFilePlacesItem *item = items.at(source);
0938     const KFilePlacesModel::GroupType groupType = item->groupType();
0939     int newTarget = qMin(target, items.count() - 1);
0940 
0941     // moving inside the same group is ok
0942     if ((items.at(newTarget)->groupType() == groupType)) {
0943         return target;
0944     }
0945 
0946     if (target > source) { // moving down, move it to the end of the group
0947         int groupFooter = source;
0948         while (items.at(groupFooter)->groupType() == groupType) {
0949             groupFooter++;
0950             // end of the list move it there
0951             if (groupFooter == items.count()) {
0952                 break;
0953             }
0954         }
0955         target = groupFooter;
0956     } else { // moving up, move it to beginning of the group
0957         int groupHead = source;
0958         while (items.at(groupHead)->groupType() == groupType) {
0959             groupHead--;
0960             // beginning of the list move it there
0961             if (groupHead == 0) {
0962                 break;
0963             }
0964         }
0965         target = groupHead;
0966     }
0967     return target;
0968 }
0969 
0970 void KFilePlacesModelPrivate::reloadAndSignal()
0971 {
0972     bookmarkManager->emitChanged(bookmarkManager->root()); // ... we'll get relisted anyway
0973 }
0974 
0975 Qt::DropActions KFilePlacesModel::supportedDropActions() const
0976 {
0977     return Qt::ActionMask;
0978 }
0979 
0980 Qt::ItemFlags KFilePlacesModel::flags(const QModelIndex &index) const
0981 {
0982     Qt::ItemFlags res;
0983 
0984     if (index.isValid()) {
0985         res |= Qt::ItemIsDragEnabled | Qt::ItemIsSelectable | Qt::ItemIsEnabled;
0986     }
0987 
0988     if (!index.isValid()) {
0989         res |= Qt::ItemIsDropEnabled;
0990     }
0991 
0992     return res;
0993 }
0994 
0995 QStringList KFilePlacesModel::mimeTypes() const
0996 {
0997     QStringList types;
0998 
0999     types << KFilePlacesModelPrivate::internalMimeType(this) << QStringLiteral("text/uri-list");
1000 
1001     return types;
1002 }
1003 
1004 QMimeData *KFilePlacesModel::mimeData(const QModelIndexList &indexes) const
1005 {
1006     QList<QUrl> urls;
1007     QByteArray itemData;
1008 
1009     QDataStream stream(&itemData, QIODevice::WriteOnly);
1010 
1011     for (const QModelIndex &index : std::as_const(indexes)) {
1012         QUrl itemUrl = url(index);
1013         if (itemUrl.isValid()) {
1014             urls << itemUrl;
1015         }
1016         stream << index.row();
1017     }
1018 
1019     QMimeData *mimeData = new QMimeData();
1020 
1021     if (!urls.isEmpty()) {
1022         mimeData->setUrls(urls);
1023     }
1024 
1025     mimeData->setData(KFilePlacesModelPrivate::internalMimeType(this), itemData);
1026 
1027     return mimeData;
1028 }
1029 
1030 bool KFilePlacesModel::dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent)
1031 {
1032     if (action == Qt::IgnoreAction) {
1033         return true;
1034     }
1035 
1036     if (column > 0) {
1037         return false;
1038     }
1039 
1040     if (row == -1 && parent.isValid()) {
1041         return false; // Don't allow to move an item onto another one,
1042         // too easy for the user to mess something up
1043         // If we really really want to allow copying files this way,
1044         // let's do it in the views to get the good old drop menu
1045     }
1046 
1047     if (data->hasFormat(KFilePlacesModelPrivate::ignoreMimeType())) {
1048         return false;
1049     }
1050 
1051     if (data->hasFormat(KFilePlacesModelPrivate::internalMimeType(this))) {
1052         // The operation is an internal move
1053         QByteArray itemData = data->data(KFilePlacesModelPrivate::internalMimeType(this));
1054         QDataStream stream(&itemData, QIODevice::ReadOnly);
1055         int itemRow;
1056 
1057         stream >> itemRow;
1058 
1059         if (!movePlace(itemRow, row)) {
1060             return false;
1061         }
1062 
1063     } else if (data->hasFormat(QStringLiteral("text/uri-list"))) {
1064         // The operation is an add
1065 
1066         QMimeDatabase db;
1067         KBookmark afterBookmark;
1068 
1069         if (row == -1) {
1070             // The dropped item is moved or added to the last position
1071 
1072             KFilePlacesItem *lastItem = d->items.last();
1073             afterBookmark = lastItem->bookmark();
1074 
1075         } else {
1076             // The dropped item is moved or added before position 'row', ie after position 'row-1'
1077 
1078             if (row > 0) {
1079                 KFilePlacesItem *afterItem = d->items[row - 1];
1080                 afterBookmark = afterItem->bookmark();
1081             }
1082         }
1083 
1084         const QList<QUrl> urls = KUrlMimeData::urlsFromMimeData(data);
1085 
1086         KBookmarkGroup group = d->bookmarkManager->root();
1087 
1088         for (const QUrl &url : urls) {
1089             KIO::StatJob *job = KIO::stat(url, KIO::StatJob::SourceSide, KIO::StatBasic);
1090 
1091             if (!job->exec()) {
1092                 Q_EMIT errorMessage(i18nc("Placeholder is error message", "Could not add to the Places panel: %1", job->errorString()));
1093                 continue;
1094             }
1095 
1096             KFileItem item(job->statResult(), url, true /*delayed mime types*/);
1097 
1098             if (!item.isDir()) {
1099                 Q_EMIT errorMessage(i18n("Only folders can be added to the Places panel."));
1100                 continue;
1101             }
1102 
1103             KBookmark bookmark = KFilePlacesItem::createBookmark(d->bookmarkManager, item.text(), url, KIO::iconNameForUrl(url));
1104 
1105             group.moveBookmark(bookmark, afterBookmark);
1106             afterBookmark = bookmark;
1107         }
1108 
1109     } else {
1110         // Oops, shouldn't happen thanks to mimeTypes()
1111         qWarning() << ": received wrong mimedata, " << data->formats();
1112         return false;
1113     }
1114 
1115     refresh();
1116 
1117     return true;
1118 }
1119 
1120 void KFilePlacesModel::refresh() const
1121 {
1122     d->reloadAndSignal();
1123 }
1124 
1125 QUrl KFilePlacesModel::convertedUrl(const QUrl &url)
1126 {
1127     QUrl newUrl = url;
1128     if (url.scheme() == QLatin1String("timeline")) {
1129         newUrl = createTimelineUrl(url);
1130     } else if (url.scheme() == QLatin1String("search")) {
1131         newUrl = createSearchUrl(url);
1132     }
1133 
1134     return newUrl;
1135 }
1136 
1137 void KFilePlacesModel::addPlace(const QString &text, const QUrl &url, const QString &iconName, const QString &appName)
1138 {
1139     addPlace(text, url, iconName, appName, QModelIndex());
1140 }
1141 
1142 void KFilePlacesModel::addPlace(const QString &text, const QUrl &url, const QString &iconName, const QString &appName, const QModelIndex &after)
1143 {
1144     KBookmark bookmark = KFilePlacesItem::createBookmark(d->bookmarkManager, text, url, iconName);
1145 
1146     if (!appName.isEmpty()) {
1147         bookmark.setMetaDataItem(QStringLiteral("OnlyInApp"), appName);
1148     }
1149 
1150     if (after.isValid()) {
1151         KFilePlacesItem *item = static_cast<KFilePlacesItem *>(after.internalPointer());
1152         d->bookmarkManager->root().moveBookmark(bookmark, item->bookmark());
1153     }
1154 
1155     refresh();
1156 }
1157 
1158 void KFilePlacesModel::editPlace(const QModelIndex &index, const QString &text, const QUrl &url, const QString &iconName, const QString &appName)
1159 {
1160     if (!index.isValid()) {
1161         return;
1162     }
1163 
1164     KFilePlacesItem *item = static_cast<KFilePlacesItem *>(index.internalPointer());
1165 
1166     if (item->isDevice()) {
1167         return;
1168     }
1169 
1170     KBookmark bookmark = item->bookmark();
1171 
1172     if (bookmark.isNull()) {
1173         return;
1174     }
1175 
1176     QList<int> changedRoles;
1177     bool changed = false;
1178 
1179     if (text != bookmark.fullText()) {
1180         bookmark.setFullText(text);
1181         changed = true;
1182         changedRoles << Qt::DisplayRole;
1183     }
1184 
1185     if (url != bookmark.url()) {
1186         bookmark.setUrl(url);
1187         changed = true;
1188         changedRoles << KFilePlacesModel::UrlRole;
1189     }
1190 
1191     if (iconName != bookmark.icon()) {
1192         bookmark.setIcon(iconName);
1193         changed = true;
1194         changedRoles << Qt::DecorationRole;
1195     }
1196 
1197     const QString onlyInApp = bookmark.metaDataItem(QStringLiteral("OnlyInApp"));
1198     if (appName != onlyInApp) {
1199         bookmark.setMetaDataItem(QStringLiteral("OnlyInApp"), appName);
1200         changed = true;
1201     }
1202 
1203     if (changed) {
1204         refresh();
1205         Q_EMIT dataChanged(index, index, changedRoles);
1206     }
1207 }
1208 
1209 void KFilePlacesModel::removePlace(const QModelIndex &index) const
1210 {
1211     if (!index.isValid()) {
1212         return;
1213     }
1214 
1215     KFilePlacesItem *item = static_cast<KFilePlacesItem *>(index.internalPointer());
1216 
1217     if (item->isDevice()) {
1218         return;
1219     }
1220 
1221     KBookmark bookmark = item->bookmark();
1222 
1223     if (bookmark.isNull()) {
1224         return;
1225     }
1226 
1227     d->bookmarkManager->root().deleteBookmark(bookmark);
1228     refresh();
1229 }
1230 
1231 void KFilePlacesModel::setPlaceHidden(const QModelIndex &index, bool hidden)
1232 {
1233     if (!index.isValid()) {
1234         return;
1235     }
1236 
1237     KFilePlacesItem *item = static_cast<KFilePlacesItem *>(index.internalPointer());
1238 
1239     if (item->bookmark().isNull() || item->isHidden() == hidden) {
1240         return;
1241     }
1242 
1243     const bool groupHidden = isGroupHidden(item->groupType());
1244     const bool hidingChildOnShownParent = hidden && !groupHidden;
1245     const bool showingChildOnShownParent = !hidden && !groupHidden;
1246 
1247     if (hidingChildOnShownParent || showingChildOnShownParent) {
1248         item->setHidden(hidden);
1249 
1250         d->reloadAndSignal();
1251         Q_EMIT dataChanged(index, index, {KFilePlacesModel::HiddenRole});
1252     }
1253 }
1254 
1255 void KFilePlacesModel::setGroupHidden(const GroupType type, bool hidden)
1256 {
1257     if (isGroupHidden(type) == hidden) {
1258         return;
1259     }
1260 
1261     d->bookmarkManager->root().setMetaDataItem(stateNameForGroupType(type), (hidden ? QStringLiteral("true") : QStringLiteral("false")));
1262     d->reloadAndSignal();
1263     Q_EMIT groupHiddenChanged(type, hidden);
1264 }
1265 
1266 bool KFilePlacesModel::movePlace(int itemRow, int row)
1267 {
1268     KBookmark afterBookmark;
1269 
1270     if ((itemRow < 0) || (itemRow >= d->items.count())) {
1271         return false;
1272     }
1273 
1274     if (row >= d->items.count()) {
1275         row = -1;
1276     }
1277 
1278     if (row == -1) {
1279         // The dropped item is moved or added to the last position
1280 
1281         KFilePlacesItem *lastItem = d->items.last();
1282         afterBookmark = lastItem->bookmark();
1283 
1284     } else {
1285         // The dropped item is moved or added before position 'row', ie after position 'row-1'
1286 
1287         if (row > 0) {
1288             KFilePlacesItem *afterItem = d->items[row - 1];
1289             afterBookmark = afterItem->bookmark();
1290         }
1291     }
1292 
1293     KFilePlacesItem *item = d->items[itemRow];
1294     KBookmark bookmark = item->bookmark();
1295 
1296     int destRow = row == -1 ? d->items.count() : row;
1297 
1298     // avoid move item away from its group
1299     destRow = d->findNearestPosition(itemRow, destRow);
1300 
1301     // The item is not moved when the drop indicator is on either item edge
1302     if (itemRow == destRow || itemRow + 1 == destRow) {
1303         return false;
1304     }
1305 
1306     beginMoveRows(QModelIndex(), itemRow, itemRow, QModelIndex(), destRow);
1307     d->bookmarkManager->root().moveBookmark(bookmark, afterBookmark);
1308     // Move item ourselves so that reloadBookmarks() does not consider
1309     // the move as a remove + insert.
1310     //
1311     // 2nd argument of QList::move() expects the final destination index,
1312     // but 'row' is the value of the destination index before the moved
1313     // item has been removed from its original position. That is why we
1314     // adjust if necessary.
1315     d->items.move(itemRow, itemRow < destRow ? (destRow - 1) : destRow);
1316     endMoveRows();
1317 
1318     return true;
1319 }
1320 
1321 int KFilePlacesModel::hiddenCount() const
1322 {
1323     int rows = rowCount();
1324     int hidden = 0;
1325 
1326     for (int i = 0; i < rows; ++i) {
1327         if (isHidden(index(i, 0))) {
1328             hidden++;
1329         }
1330     }
1331 
1332     return hidden;
1333 }
1334 
1335 QAction *KFilePlacesModel::teardownActionForIndex(const QModelIndex &index) const
1336 {
1337     Solid::Device device = deviceForIndex(index);
1338 
1339     QAction *action = nullptr;
1340 
1341     if (device.is<Solid::StorageAccess>() && device.as<Solid::StorageAccess>()->isAccessible()) {
1342         Solid::StorageDrive *drive = device.as<Solid::StorageDrive>();
1343 
1344         if (drive == nullptr) {
1345             drive = device.parent().as<Solid::StorageDrive>();
1346         }
1347 
1348         const bool teardownInProgress = deviceAccessibility(index) == KFilePlacesModel::TeardownInProgress;
1349 
1350         bool hotpluggable = false;
1351         bool removable = false;
1352 
1353         if (drive != nullptr) {
1354             hotpluggable = drive->isHotpluggable();
1355             removable = drive->isRemovable();
1356         }
1357 
1358         QString iconName;
1359         QString text;
1360 
1361         if (device.is<Solid::OpticalDisc>()) {
1362             if (teardownInProgress) {
1363                 text = i18nc("@action:inmenu", "Releasing…");
1364             } else {
1365                 text = i18nc("@action:inmenu", "&Release");
1366             }
1367         } else if (removable || hotpluggable) {
1368             if (teardownInProgress) {
1369                 text = i18nc("@action:inmenu", "Safely Removing…");
1370             } else {
1371                 text = i18nc("@action:inmenu", "&Safely Remove");
1372             }
1373             iconName = QStringLiteral("media-eject");
1374         } else {
1375             if (teardownInProgress) {
1376                 text = i18nc("@action:inmenu", "Unmounting…");
1377             } else {
1378                 text = i18nc("@action:inmenu", "&Unmount");
1379             }
1380             iconName = QStringLiteral("media-eject");
1381         }
1382 
1383         if (!iconName.isEmpty()) {
1384             action = new QAction(QIcon::fromTheme(iconName), text, nullptr);
1385         } else {
1386             action = new QAction(text, nullptr);
1387         }
1388 
1389         if (teardownInProgress) {
1390             action->setEnabled(false);
1391         }
1392     }
1393 
1394     return action;
1395 }
1396 
1397 QAction *KFilePlacesModel::ejectActionForIndex(const QModelIndex &index) const
1398 {
1399     Solid::Device device = deviceForIndex(index);
1400 
1401     if (device.is<Solid::OpticalDisc>()) {
1402         QString text = i18nc("@action:inmenu", "&Eject");
1403 
1404         return new QAction(QIcon::fromTheme(QStringLiteral("media-eject")), text, nullptr);
1405     }
1406 
1407     return nullptr;
1408 }
1409 
1410 void KFilePlacesModel::requestTeardown(const QModelIndex &index)
1411 {
1412     Solid::Device device = deviceForIndex(index);
1413     Solid::StorageAccess *access = device.as<Solid::StorageAccess>();
1414 
1415     if (access != nullptr) {
1416         d->teardownInProgress[access] = index;
1417 
1418         const QString filePath = access->filePath();
1419         connect(access, &Solid::StorageAccess::teardownDone, this, [this, access, filePath](Solid::ErrorType error, QVariant errorData) {
1420             d->storageTeardownDone(filePath, error, errorData, access);
1421         });
1422 
1423         access->teardown();
1424     }
1425 }
1426 
1427 void KFilePlacesModel::requestEject(const QModelIndex &index)
1428 {
1429     Solid::Device device = deviceForIndex(index);
1430 
1431     Solid::OpticalDrive *drive = device.parent().as<Solid::OpticalDrive>();
1432 
1433     if (drive != nullptr) {
1434         d->teardownInProgress[drive] = index;
1435 
1436         QString filePath;
1437         Solid::StorageAccess *access = device.as<Solid::StorageAccess>();
1438         if (access) {
1439             filePath = access->filePath();
1440         }
1441 
1442         connect(drive, &Solid::OpticalDrive::ejectDone, this, [this, filePath, drive](Solid::ErrorType error, QVariant errorData) {
1443             d->storageTeardownDone(filePath, error, errorData, drive);
1444         });
1445 
1446         drive->eject();
1447     } else {
1448         QString label = data(index, Qt::DisplayRole).toString().replace(QLatin1Char('&'), QLatin1String("&&"));
1449         QString message = i18n("The device '%1' is not a disk and cannot be ejected.", label);
1450         Q_EMIT errorMessage(message);
1451     }
1452 }
1453 
1454 void KFilePlacesModel::requestSetup(const QModelIndex &index)
1455 {
1456     Solid::Device device = deviceForIndex(index);
1457 
1458     if (device.is<Solid::StorageAccess>() && !d->setupInProgress.contains(device.as<Solid::StorageAccess>())
1459         && !device.as<Solid::StorageAccess>()->isAccessible()) {
1460         Solid::StorageAccess *access = device.as<Solid::StorageAccess>();
1461 
1462         d->setupInProgress[access] = index;
1463 
1464         connect(access, &Solid::StorageAccess::setupDone, this, [this, access](Solid::ErrorType error, QVariant errorData) {
1465             d->storageSetupDone(error, errorData, access);
1466         });
1467 
1468         access->setup();
1469     }
1470 }
1471 
1472 void KFilePlacesModelPrivate::storageSetupDone(Solid::ErrorType error, const QVariant &errorData, Solid::StorageAccess *sender)
1473 {
1474     QPersistentModelIndex index = setupInProgress.take(sender);
1475 
1476     if (!index.isValid()) {
1477         return;
1478     }
1479 
1480     if (!error) {
1481         Q_EMIT q->setupDone(index, true);
1482     } else {
1483         if (errorData.isValid()) {
1484             Q_EMIT q->errorMessage(i18n("An error occurred while accessing '%1', the system responded: %2", q->text(index), errorData.toString()));
1485         } else {
1486             Q_EMIT q->errorMessage(i18n("An error occurred while accessing '%1'", q->text(index)));
1487         }
1488         Q_EMIT q->setupDone(index, false);
1489     }
1490 }
1491 
1492 void KFilePlacesModelPrivate::storageTeardownDone(const QString &filePath, Solid::ErrorType error, const QVariant &errorData, QObject *sender)
1493 {
1494     QPersistentModelIndex index = teardownInProgress.take(sender);
1495     if (!index.isValid()) {
1496         return;
1497     }
1498 
1499     if (error == Solid::ErrorType::DeviceBusy && !filePath.isEmpty()) {
1500         auto *listOpenFilesJob = new KListOpenFilesJob(filePath);
1501         QObject::connect(listOpenFilesJob, &KIO::Job::result, q, [this, index, error, errorData, listOpenFilesJob]() {
1502             const auto blockingProcesses = listOpenFilesJob->processInfoList();
1503 
1504             QStringList blockingApps;
1505             blockingApps.reserve(blockingProcesses.count());
1506             for (const auto &process : blockingProcesses) {
1507                 blockingApps << process.name();
1508             }
1509 
1510             Q_EMIT q->teardownDone(index, error, errorData);
1511             if (blockingProcesses.isEmpty()) {
1512                 Q_EMIT q->errorMessage(i18n("One or more files on this device are open within an application."));
1513             } else {
1514                 blockingApps.removeDuplicates();
1515                 Q_EMIT q->errorMessage(xi18np("One or more files on this device are opened in application <application>\"%2\"</application>.",
1516                                               "One or more files on this device are opened in following applications: <application>%2</application>.",
1517                                               blockingApps.count(),
1518                                               blockingApps.join(i18nc("separator in list of apps blocking device unmount", ", "))));
1519             }
1520         });
1521         listOpenFilesJob->start();
1522         return;
1523     }
1524 
1525     Q_EMIT q->teardownDone(index, error, errorData);
1526     if (error != Solid::ErrorType::NoError && error != Solid::ErrorType::UserCanceled) {
1527         Q_EMIT q->errorMessage(errorData.toString());
1528     }
1529 }
1530 
1531 void KFilePlacesModel::setSupportedSchemes(const QStringList &schemes)
1532 {
1533     d->supportedSchemes = schemes;
1534     d->reloadBookmarks();
1535     Q_EMIT supportedSchemesChanged();
1536 }
1537 
1538 QStringList KFilePlacesModel::supportedSchemes() const
1539 {
1540     return d->supportedSchemes;
1541 }
1542 
1543 namespace {
1544 QString partitionManagerPath()
1545 {
1546     static const QString path = QStandardPaths::findExecutable(QStringLiteral("partitionmanager"));
1547     return path;
1548 }
1549 } // namespace
1550 
1551 QAction *KFilePlacesModel::partitionActionForIndex(const QModelIndex &index) const
1552 {
1553     const auto device = deviceForIndex(index);
1554     if (!device.is<Solid::Block>()) {
1555         return nullptr;
1556     }
1557 
1558     // Not using kservice to find partitionmanager because we need to manually invoke it so we can pass the --device argument.
1559     if (partitionManagerPath().isEmpty()) {
1560         return nullptr;
1561     }
1562 
1563     auto action = new QAction(QIcon::fromTheme(QStringLiteral("partitionmanager")),
1564                               i18nc("@action:inmenu", "Reformat or Edit with Partition Manager"),
1565                               nullptr);
1566     connect(action, &QAction::triggered, this, [device] {
1567         const auto block = device.as<Solid::Block>();
1568         auto job = new KIO::CommandLauncherJob(partitionManagerPath(), {QStringLiteral("--device"), block->device()});
1569         job->start();
1570     });
1571     return action;
1572 }
1573 
1574 #include "moc_kfileplacesmodel.cpp"