File indexing completed on 2024-04-28 15:26:42

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