1 /*
2     This file is part of the KDE project
3     SPDX-FileCopyrightText: 2007 Kevin Ottens <ervin@kde.org>
4     SPDX-FileCopyrightText: 2007 David Faure <faure@kde.org>
5 
6     SPDX-License-Identifier: LGPL-2.0-only
7 */
8 
9 #include "kfileplacesmodel.h"
10 #include "kfileplacesitem_p.h"
11 
12 #ifdef _WIN32_WCE
13 #include "WinBase.h"
14 #include "Windows.h"
15 #endif
16 
17 #include <QAction>
18 #include <QCoreApplication>
19 #include <QDir>
20 #include <QFile>
21 #include <QMimeData>
22 #include <QMimeDatabase>
23 #include <QTimer>
24 
25 #include <KLocalizedString>
26 #include <kfileitem.h>
27 
28 #include <KUrlMimeData>
29 #include <QDebug>
30 
31 #include <KBookmarkManager>
32 
33 #include <KCoreDirLister>
34 #include <kio/job.h>
35 #include <kprotocolinfo.h>
36 
37 #include <KConfig>
38 #include <KConfigGroup>
39 
40 #include <QStandardPaths>
41 #include <solid/devicenotifier.h>
42 #include <solid/opticaldisc.h>
43 #include <solid/opticaldrive.h>
44 #include <solid/portablemediaplayer.h>
45 #include <solid/predicate.h>
46 #include <solid/storageaccess.h>
47 #include <solid/storagedrive.h>
48 #include <solid/storagevolume.h>
49 
50 namespace
51 {
stateNameForGroupType(KFilePlacesModel::GroupType type)52 QString stateNameForGroupType(KFilePlacesModel::GroupType type)
53 {
54     switch (type) {
55     case KFilePlacesModel::PlacesType:
56         return QStringLiteral("GroupState-Places-IsHidden");
57     case KFilePlacesModel::RemoteType:
58         return QStringLiteral("GroupState-Remote-IsHidden");
59     case KFilePlacesModel::RecentlySavedType:
60         return QStringLiteral("GroupState-RecentlySaved-IsHidden");
61     case KFilePlacesModel::SearchForType:
62         return QStringLiteral("GroupState-SearchFor-IsHidden");
63     case KFilePlacesModel::DevicesType:
64         return QStringLiteral("GroupState-Devices-IsHidden");
65     case KFilePlacesModel::RemovableDevicesType:
66         return QStringLiteral("GroupState-RemovableDevices-IsHidden");
67     case KFilePlacesModel::TagsType:
68         return QStringLiteral("GroupState-Tags-IsHidden");
69     default:
70         Q_UNREACHABLE();
71     }
72 }
73 
isFileIndexingEnabled()74 static bool isFileIndexingEnabled()
75 {
76     KConfig config(QStringLiteral("baloofilerc"));
77     KConfigGroup basicSettings = config.group("Basic Settings");
78     return basicSettings.readEntry("Indexing-Enabled", true);
79 }
80 
timelineDateString(int year,int month,int day=0)81 static QString timelineDateString(int year, int month, int day = 0)
82 {
83     const QString dateFormat = QStringLiteral("%1-%2");
84 
85     QString date = dateFormat.arg(year).arg(month, 2, 10, QLatin1Char('0'));
86     if (day > 0) {
87         date += QStringLiteral("-%1").arg(day, 2, 10, QLatin1Char('0'));
88     }
89     return date;
90 }
91 
createTimelineUrl(const QUrl & url)92 static QUrl createTimelineUrl(const QUrl &url)
93 {
94     // based on dolphin urls
95     const QString timelinePrefix = QLatin1String("timeline:") + QLatin1Char('/');
96     QUrl timelineUrl;
97 
98     const QString path = url.toDisplayString(QUrl::PreferLocalFile);
99     if (path.endsWith(QLatin1String("/yesterday"))) {
100         const QDate date = QDate::currentDate().addDays(-1);
101         const int year = date.year();
102         const int month = date.month();
103         const int day = date.day();
104 
105         timelineUrl = QUrl(timelinePrefix + timelineDateString(year, month) + QLatin1Char('/') + timelineDateString(year, month, day));
106     } else if (path.endsWith(QLatin1String("/thismonth"))) {
107         const QDate date = QDate::currentDate();
108         timelineUrl = QUrl(timelinePrefix + timelineDateString(date.year(), date.month()));
109     } else if (path.endsWith(QLatin1String("/lastmonth"))) {
110         const QDate date = QDate::currentDate().addMonths(-1);
111         timelineUrl = QUrl(timelinePrefix + timelineDateString(date.year(), date.month()));
112     } else {
113         Q_ASSERT(path.endsWith(QLatin1String("/today")));
114         timelineUrl = url;
115     }
116 
117     return timelineUrl;
118 }
119 
createSearchUrl(const QUrl & url)120 static QUrl createSearchUrl(const QUrl &url)
121 {
122     QUrl searchUrl = url;
123 
124     const QString path = url.toDisplayString(QUrl::PreferLocalFile);
125 
126     const QStringList validSearchPaths = {QStringLiteral("/documents"), QStringLiteral("/images"), QStringLiteral("/audio"), QStringLiteral("/videos")};
127 
128     for (const QString &validPath : validSearchPaths) {
129         if (path.endsWith(validPath)) {
130             searchUrl.setScheme(QStringLiteral("baloosearch"));
131             return searchUrl;
132         }
133     }
134 
135     qWarning() << "Invalid search url:" << url;
136 
137     return searchUrl;
138 }
139 }
140 
141 class KFilePlacesModelPrivate
142 {
143 public:
KFilePlacesModelPrivate(KFilePlacesModel * qq)144     explicit KFilePlacesModelPrivate(KFilePlacesModel *qq)
145         : q(qq)
146         , bookmarkManager(nullptr)
147         , fileIndexingEnabled(isFileIndexingEnabled())
148         , tags()
149         , tagsLister(new KCoreDirLister(q))
150     {
151         if (KProtocolInfo::isKnownProtocol(QStringLiteral("tags"))) {
152             QObject::connect(tagsLister, &KCoreDirLister::itemsAdded, q, [this](const QUrl &, const KFileItemList &items) {
153                 if (tags.isEmpty()) {
154                     QList<QUrl> existingBookmarks;
155 
156                     KBookmarkGroup root = bookmarkManager->root();
157                     KBookmark bookmark = root.first();
158 
159                     while (!bookmark.isNull()) {
160                         existingBookmarks.append(bookmark.url());
161                         bookmark = root.next(bookmark);
162                     }
163 
164                     if (!existingBookmarks.contains(QUrl(tagsUrlBase))) {
165                         KBookmark alltags = KFilePlacesItem::createSystemBookmark(bookmarkManager,
166                                                                                   "All tags",
167                                                                                   i18n("All tags").toUtf8().data(),
168                                                                                   QUrl(tagsUrlBase),
169                                                                                   QStringLiteral("tag"));
170                     }
171                 }
172 
173                 for (const KFileItem &item : items) {
174                     const QString name = item.name();
175 
176                     if (!tags.contains(name)) {
177                         tags.append(name);
178                     }
179                 }
180                 reloadBookmarks();
181             });
182 
183             QObject::connect(tagsLister, &KCoreDirLister::itemsDeleted, q, [this](const KFileItemList &items) {
184                 for (const KFileItem &item : items) {
185                     tags.removeAll(item.name());
186                 }
187                 reloadBookmarks();
188             });
189 
190             tagsLister->openUrl(QUrl(tagsUrlBase), KCoreDirLister::OpenUrlFlag::Reload);
191         }
192     }
193 
194     KFilePlacesModel *const q;
195 
196     QList<KFilePlacesItem *> items;
197     QVector<QString> availableDevices;
198     QMap<QObject *, QPersistentModelIndex> setupInProgress;
199     QStringList supportedSchemes;
200 
201     Solid::Predicate predicate;
202     KBookmarkManager *bookmarkManager;
203 
204     const bool fileIndexingEnabled;
205 
206     QString alternativeApplicationName;
207 
208     void reloadAndSignal();
209     QList<KFilePlacesItem *> loadBookmarkList();
210     int findNearestPosition(int source, int target);
211 
212     QVector<QString> tags;
213     const QString tagsUrlBase = QStringLiteral("tags:/");
214     KCoreDirLister *tagsLister;
215 
216     void initDeviceList();
217     void deviceAdded(const QString &udi);
218     void deviceRemoved(const QString &udi);
219     void itemChanged(const QString &udi);
220     void reloadBookmarks();
221     void storageSetupDone(Solid::ErrorType error, const QVariant &errorData, Solid::StorageAccess *sender);
222     void storageTeardownDone(Solid::ErrorType error, const QVariant &errorData);
223 
224 private:
225     bool isBalooUrl(const QUrl &url) const;
226 };
227 
bookmarkForUrl(const QUrl & searchUrl) const228 KBookmark KFilePlacesModel::bookmarkForUrl(const QUrl &searchUrl) const
229 {
230     KBookmarkGroup root = d->bookmarkManager->root();
231     KBookmark current = root.first();
232     while (!current.isNull()) {
233         if (current.url() == searchUrl) {
234             return current;
235         }
236         current = root.next(current);
237     }
238     return KBookmark();
239 }
240 
versionKey()241 static inline QString versionKey()
242 {
243     return QStringLiteral("kde_places_version");
244 }
245 
KFilePlacesModel(const QString & alternativeApplicationName,QObject * parent)246 KFilePlacesModel::KFilePlacesModel(const QString &alternativeApplicationName, QObject *parent)
247     : QAbstractItemModel(parent)
248     , d(new KFilePlacesModelPrivate(this))
249 {
250     const QString file = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1String("/user-places.xbel");
251     d->bookmarkManager = KBookmarkManager::managerForExternalFile(file);
252     d->alternativeApplicationName = alternativeApplicationName;
253 
254     // Let's put some places in there if it's empty.
255     KBookmarkGroup root = d->bookmarkManager->root();
256 
257     const auto setDefaultMetadataItemForGroup = [&root](KFilePlacesModel::GroupType type) {
258         root.setMetaDataItem(stateNameForGroupType(type), QStringLiteral("false"));
259     };
260 
261     // Increase this version number and use the following logic to handle the update process for existing installations.
262     static const int s_currentVersion = 4;
263 
264     const bool newFile = root.first().isNull() || !QFile::exists(file);
265     const int fileVersion = root.metaDataItem(versionKey()).toInt();
266 
267     if (newFile || fileVersion < s_currentVersion) {
268         root.setMetaDataItem(versionKey(), QString::number(s_currentVersion));
269 
270         const QList<QUrl> seenUrls = root.groupUrlList();
271 
272         /* clang-format off */
273         auto createSystemBookmark =
274             [this, &seenUrls](const char *translationContext,
275                               const QByteArray &untranslatedLabel,
276                               const QUrl &url,
277                               const QString &iconName,
278                               const KBookmark &after) {
279                 if (!seenUrls.contains(url)) {
280                     return KFilePlacesItem::createSystemBookmark(d->bookmarkManager, translationContext, untranslatedLabel, url, iconName, after);
281                 }
282                 return KBookmark();
283             };
284         /* clang-format on */
285 
286         if (fileVersion < 2) {
287             // NOTE: The context for these I18NC_NOOP calls has to be "KFile System Bookmarks".
288             // The real i18nc call is made later, with this context, so the two must match.
289             // createSystemBookmark actually does nothing with its second argument, the context.
290             createSystemBookmark(I18NC_NOOP("KFile System Bookmarks", "Home"), QUrl::fromLocalFile(QDir::homePath()), QStringLiteral("user-home"), KBookmark());
291 
292             // Some distros may not create various standard XDG folders by default
293             // so check for their existence before adding bookmarks for them
294             const QString desktopFolder = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation);
295             if (QDir(desktopFolder).exists()) {
296                 createSystemBookmark(I18NC_NOOP("KFile System Bookmarks", "Desktop"),
297                                      QUrl::fromLocalFile(desktopFolder),
298                                      QStringLiteral("user-desktop"),
299                                      KBookmark());
300             }
301             const QString documentsFolder = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation);
302             if (QDir(documentsFolder).exists()) {
303                 createSystemBookmark(I18NC_NOOP("KFile System Bookmarks", "Documents"),
304                                      QUrl::fromLocalFile(documentsFolder),
305                                      QStringLiteral("folder-documents"),
306                                      KBookmark());
307             }
308             const QString downloadFolder = QStandardPaths::writableLocation(QStandardPaths::DownloadLocation);
309             if (QDir(downloadFolder).exists()) {
310                 createSystemBookmark(I18NC_NOOP("KFile System Bookmarks", "Downloads"),
311                                      QUrl::fromLocalFile(downloadFolder),
312                                      QStringLiteral("folder-downloads"),
313                                      KBookmark());
314             }
315             createSystemBookmark(I18NC_NOOP("KFile System Bookmarks", "Network"),
316                                  QUrl(QStringLiteral("remote:/")),
317                                  QStringLiteral("folder-network"),
318                                  KBookmark());
319 
320             createSystemBookmark(I18NC_NOOP("KFile System Bookmarks", "Trash"), QUrl(QStringLiteral("trash:/")), QStringLiteral("user-trash"), KBookmark());
321         }
322 
323         if (!newFile && fileVersion < 3) {
324             KBookmarkGroup root = d->bookmarkManager->root();
325             KBookmark bItem = root.first();
326             while (!bItem.isNull()) {
327                 KBookmark nextbItem = root.next(bItem);
328                 const bool isSystemItem = bItem.metaDataItem(QStringLiteral("isSystemItem")) == QLatin1String("true");
329                 if (isSystemItem) {
330                     const QString text = bItem.fullText();
331                     // Because of b8a4c2223453932202397d812a0c6b30c6186c70 we need to find the system bookmark named Audio Files
332                     // and rename it to Audio, otherwise users are getting untranslated strings
333                     if (text == QLatin1String("Audio Files")) {
334                         bItem.setFullText(QStringLiteral("Audio"));
335                     } else if (text == QLatin1String("Today")) {
336                         // Because of 19feef732085b444515da3f6c66f3352bbcb1824 we need to find the system bookmark named Today
337                         // and rename it to Modified Today, otherwise users are getting untranslated strings
338                         bItem.setFullText(QStringLiteral("Modified Today"));
339                     } else if (text == QLatin1String("Yesterday")) {
340                         // Because of 19feef732085b444515da3f6c66f3352bbcb1824 we need to find the system bookmark named Yesterday
341                         // and rename it to Modified Yesterday, otherwise users are getting untranslated strings
342                         bItem.setFullText(QStringLiteral("Modified Yesterday"));
343                     } else if (text == QLatin1String("This Month")) {
344                         // Because of 7e1d2fb84546506c91684dd222c2485f0783848f we need to find the system bookmark named This Month
345                         // and remove it, otherwise users are getting untranslated strings
346                         root.deleteBookmark(bItem);
347                     } else if (text == QLatin1String("Last Month")) {
348                         // Because of 7e1d2fb84546506c91684dd222c2485f0783848f we need to find the system bookmark named Last Month
349                         // and remove it, otherwise users are getting untranslated strings
350                         root.deleteBookmark(bItem);
351                     }
352                 }
353 
354                 bItem = nextbItem;
355             }
356         }
357         if (fileVersion < 4) {
358             auto findSystemBookmark = [this](const QString &untranslatedText) {
359                 KBookmarkGroup root = d->bookmarkManager->root();
360                 KBookmark bItem = root.first();
361                 while (!bItem.isNull()) {
362                     const bool isSystemItem = bItem.metaDataItem(QStringLiteral("isSystemItem")) == QLatin1String("true");
363                     if (isSystemItem && bItem.fullText() == untranslatedText) {
364                         return bItem;
365                     }
366                     bItem = root.next(bItem);
367                 }
368                 return KBookmark();
369             };
370             // This variable is used to insert the new bookmarks at the correct place starting after the "Downloads"
371             // bookmark. When the user already has some of the bookmarks set up manually, the createSystemBookmark()
372             // function returns an empty KBookmark so the following entries will be added at the end of the bookmark
373             // section to not mess with the users setup.
374             KBookmark after = findSystemBookmark(QLatin1String("Downloads"));
375 
376             const QString musicFolder = QStandardPaths::writableLocation(QStandardPaths::MusicLocation);
377             if (QDir(musicFolder).exists()) {
378                 after = createSystemBookmark(I18NC_NOOP("KFile System Bookmarks", "Music"),
379                                              QUrl::fromLocalFile(musicFolder),
380                                              QStringLiteral("folder-music"),
381                                              after);
382             }
383             const QString pictureFolder = QStandardPaths::writableLocation(QStandardPaths::PicturesLocation);
384             if (QDir(pictureFolder).exists()) {
385                 after = createSystemBookmark(I18NC_NOOP("KFile System Bookmarks", "Pictures"),
386                                              QUrl::fromLocalFile(pictureFolder),
387                                              QStringLiteral("folder-pictures"),
388                                              after);
389             }
390             // Choosing the name "Videos" instead of "Movies", since that is how the folder
391             // is called normally on Linux: https://cgit.freedesktop.org/xdg/xdg-user-dirs/tree/user-dirs.defaults
392             const QString videoFolder = QStandardPaths::writableLocation(QStandardPaths::MoviesLocation);
393             if (QDir(videoFolder).exists()) {
394                 after = createSystemBookmark(I18NC_NOOP("KFile System Bookmarks", "Videos"),
395                                              QUrl::fromLocalFile(videoFolder),
396                                              QStringLiteral("folder-videos"),
397                                              after);
398             }
399         }
400 
401         if (newFile) {
402             setDefaultMetadataItemForGroup(PlacesType);
403             setDefaultMetadataItemForGroup(RemoteType);
404             setDefaultMetadataItemForGroup(DevicesType);
405             setDefaultMetadataItemForGroup(RemovableDevicesType);
406             setDefaultMetadataItemForGroup(TagsType);
407         }
408 
409         // Force bookmarks to be saved. If on open/save dialog and the bookmarks are not saved, QFile::exists
410         // will always return false, which opening/closing all the time the open/save dialog would cause the
411         // bookmarks to be added once each time, having lots of times each bookmark. (ereslibre)
412         d->bookmarkManager->saveAs(file);
413     }
414 
415     // Add a Recently Used entry if available (it comes from kio-extras)
416     if (qEnvironmentVariableIsSet("KDE_FULL_SESSION") && KProtocolInfo::isKnownProtocol(QStringLiteral("recentlyused"))
417         && root.metaDataItem(QStringLiteral("withRecentlyUsed")) != QLatin1String("true")) {
418         root.setMetaDataItem(QStringLiteral("withRecentlyUsed"), QStringLiteral("true"));
419 
420         KBookmark recentFilesBookmark = KFilePlacesItem::createSystemBookmark(d->bookmarkManager,
421                                                                               I18NC_NOOP("KFile System Bookmarks", "Recent Files"),
422                                                                               QUrl(QStringLiteral("recentlyused:/files")),
423                                                                               QStringLiteral("document-open-recent"));
424 
425         KBookmark recentDirectoriesBookmark = KFilePlacesItem::createSystemBookmark(d->bookmarkManager,
426                                                                                     I18NC_NOOP("KFile System Bookmarks", "Recent Locations"),
427                                                                                     QUrl(QStringLiteral("recentlyused:/locations")),
428                                                                                     QStringLiteral("folder-open-recent"));
429 
430         setDefaultMetadataItemForGroup(RecentlySavedType);
431 
432         // Move The recently used bookmarks below the trash, making it the first element in the Recent group
433         KBookmark trashBookmark = bookmarkForUrl(QUrl(QStringLiteral("trash:/")));
434         if (!trashBookmark.isNull()) {
435             root.moveBookmark(recentFilesBookmark, trashBookmark);
436             root.moveBookmark(recentDirectoriesBookmark, recentFilesBookmark);
437         }
438 
439         d->bookmarkManager->save();
440     }
441 
442     // if baloo is enabled, add new urls even if the bookmark file is not empty
443     if (d->fileIndexingEnabled && root.metaDataItem(QStringLiteral("withBaloo")) != QLatin1String("true")) {
444         root.setMetaDataItem(QStringLiteral("withBaloo"), QStringLiteral("true"));
445 
446         // don't add by default "Modified Today" and "Modified Yesterday" when recentlyused:/ is present
447         if (root.metaDataItem(QStringLiteral("withRecentlyUsed")) != QLatin1String("true")) {
448             KFilePlacesItem::createSystemBookmark(d->bookmarkManager,
449                                                   I18NC_NOOP("KFile System Bookmarks", "Modified Today"),
450                                                   QUrl(QStringLiteral("timeline:/today")),
451                                                   QStringLiteral("go-jump-today"));
452             KFilePlacesItem::createSystemBookmark(d->bookmarkManager,
453                                                   I18NC_NOOP("KFile System Bookmarks", "Modified Yesterday"),
454                                                   QUrl(QStringLiteral("timeline:/yesterday")),
455                                                   QStringLiteral("view-calendar-day"));
456         }
457 
458         KFilePlacesItem::createSystemBookmark(d->bookmarkManager,
459                                               I18NC_NOOP("KFile System Bookmarks", "Documents"),
460                                               QUrl(QStringLiteral("search:/documents")),
461                                               QStringLiteral("folder-text"));
462         KFilePlacesItem::createSystemBookmark(d->bookmarkManager,
463                                               I18NC_NOOP("KFile System Bookmarks", "Images"),
464                                               QUrl(QStringLiteral("search:/images")),
465                                               QStringLiteral("folder-images"));
466         KFilePlacesItem::createSystemBookmark(d->bookmarkManager,
467                                               I18NC_NOOP("KFile System Bookmarks", "Audio"),
468                                               QUrl(QStringLiteral("search:/audio")),
469                                               QStringLiteral("folder-sound"));
470         KFilePlacesItem::createSystemBookmark(d->bookmarkManager,
471                                               I18NC_NOOP("KFile System Bookmarks", "Videos"),
472                                               QUrl(QStringLiteral("search:/videos")),
473                                               QStringLiteral("folder-videos"));
474 
475         setDefaultMetadataItemForGroup(SearchForType);
476         setDefaultMetadataItemForGroup(RecentlySavedType);
477 
478         d->bookmarkManager->save();
479     }
480 
481     QString predicate(
482         QString::fromLatin1("[[[[ StorageVolume.ignored == false AND [ StorageVolume.usage == 'FileSystem' OR StorageVolume.usage == 'Encrypted' ]]"
483                             " OR "
484                             "[ IS StorageAccess AND StorageDrive.driveType == 'Floppy' ]]"
485                             " OR "
486                             "OpticalDisc.availableContent & 'Audio' ]"
487                             " OR "
488                             "StorageAccess.ignored == false ]"));
489 
490     if (KProtocolInfo::isKnownProtocol(QStringLiteral("mtp"))) {
491         predicate = QLatin1Char('[') + predicate + QLatin1String(" OR PortableMediaPlayer.supportedProtocols == 'mtp']");
492     }
493     if (KProtocolInfo::isKnownProtocol(QStringLiteral("afc"))) {
494         predicate = QLatin1Char('[') + predicate + QLatin1String(" OR PortableMediaPlayer.supportedProtocols == 'afc']");
495     }
496 
497     d->predicate = Solid::Predicate::fromString(predicate);
498 
499     Q_ASSERT(d->predicate.isValid());
500 
501     connect(d->bookmarkManager, &KBookmarkManager::changed, this, [this]() {
502         d->reloadBookmarks();
503     });
504     connect(d->bookmarkManager, &KBookmarkManager::bookmarksChanged, this, [this]() {
505         d->reloadBookmarks();
506     });
507 
508     d->reloadBookmarks();
509     QTimer::singleShot(0, this, [this]() {
510         d->initDeviceList();
511     });
512 }
513 
KFilePlacesModel(QObject * parent)514 KFilePlacesModel::KFilePlacesModel(QObject *parent)
515     : KFilePlacesModel({}, parent)
516 {
517 }
518 
519 KFilePlacesModel::~KFilePlacesModel() = default;
520 
url(const QModelIndex & index) const521 QUrl KFilePlacesModel::url(const QModelIndex &index) const
522 {
523     return data(index, UrlRole).toUrl();
524 }
525 
setupNeeded(const QModelIndex & index) const526 bool KFilePlacesModel::setupNeeded(const QModelIndex &index) const
527 {
528     return data(index, SetupNeededRole).toBool();
529 }
530 
icon(const QModelIndex & index) const531 QIcon KFilePlacesModel::icon(const QModelIndex &index) const
532 {
533     return data(index, Qt::DecorationRole).value<QIcon>();
534 }
535 
text(const QModelIndex & index) const536 QString KFilePlacesModel::text(const QModelIndex &index) const
537 {
538     return data(index, Qt::DisplayRole).toString();
539 }
540 
isHidden(const QModelIndex & index) const541 bool KFilePlacesModel::isHidden(const QModelIndex &index) const
542 {
543     // Note: we do not want to show an index if its parent is hidden
544     return data(index, HiddenRole).toBool() || isGroupHidden(index);
545 }
546 
isGroupHidden(const GroupType type) const547 bool KFilePlacesModel::isGroupHidden(const GroupType type) const
548 {
549     const QString hidden = d->bookmarkManager->root().metaDataItem(stateNameForGroupType(type));
550     return hidden == QLatin1String("true");
551 }
552 
isGroupHidden(const QModelIndex & index) const553 bool KFilePlacesModel::isGroupHidden(const QModelIndex &index) const
554 {
555     if (!index.isValid()) {
556         return false;
557     }
558 
559     KFilePlacesItem *item = static_cast<KFilePlacesItem *>(index.internalPointer());
560     return isGroupHidden(item->groupType());
561 }
562 
isDevice(const QModelIndex & index) const563 bool KFilePlacesModel::isDevice(const QModelIndex &index) const
564 {
565     if (!index.isValid()) {
566         return false;
567     }
568 
569     KFilePlacesItem *item = static_cast<KFilePlacesItem *>(index.internalPointer());
570 
571     return item->isDevice();
572 }
573 
deviceForIndex(const QModelIndex & index) const574 Solid::Device KFilePlacesModel::deviceForIndex(const QModelIndex &index) const
575 {
576     if (!index.isValid()) {
577         return Solid::Device();
578     }
579 
580     KFilePlacesItem *item = static_cast<KFilePlacesItem *>(index.internalPointer());
581 
582     if (item->isDevice()) {
583         return item->device();
584     } else {
585         return Solid::Device();
586     }
587 }
588 
bookmarkForIndex(const QModelIndex & index) const589 KBookmark KFilePlacesModel::bookmarkForIndex(const QModelIndex &index) const
590 {
591     if (!index.isValid()) {
592         return KBookmark();
593     }
594 
595     KFilePlacesItem *item = static_cast<KFilePlacesItem *>(index.internalPointer());
596     return item->bookmark();
597 }
598 
groupType(const QModelIndex & index) const599 KFilePlacesModel::GroupType KFilePlacesModel::groupType(const QModelIndex &index) const
600 {
601     if (!index.isValid()) {
602         return UnknownType;
603     }
604 
605     KFilePlacesItem *item = static_cast<KFilePlacesItem *>(index.internalPointer());
606     return item->groupType();
607 }
608 
groupIndexes(const KFilePlacesModel::GroupType type) const609 QModelIndexList KFilePlacesModel::groupIndexes(const KFilePlacesModel::GroupType type) const
610 {
611     if (type == UnknownType) {
612         return QModelIndexList();
613     }
614 
615     QModelIndexList indexes;
616     const int rows = rowCount();
617     for (int row = 0; row < rows; ++row) {
618         const QModelIndex current = index(row, 0);
619         if (groupType(current) == type) {
620             indexes << current;
621         }
622     }
623 
624     return indexes;
625 }
626 
data(const QModelIndex & index,int role) const627 QVariant KFilePlacesModel::data(const QModelIndex &index, int role) const
628 {
629     if (!index.isValid()) {
630         return QVariant();
631     }
632 
633     KFilePlacesItem *item = static_cast<KFilePlacesItem *>(index.internalPointer());
634     if (role == KFilePlacesModel::GroupHiddenRole) {
635         return isGroupHidden(item->groupType());
636     } else {
637         return item->data(role);
638     }
639 }
640 
index(int row,int column,const QModelIndex & parent) const641 QModelIndex KFilePlacesModel::index(int row, int column, const QModelIndex &parent) const
642 {
643     if (row < 0 || column != 0 || row >= d->items.size()) {
644         return QModelIndex();
645     }
646 
647     if (parent.isValid()) {
648         return QModelIndex();
649     }
650 
651     return createIndex(row, column, d->items.at(row));
652 }
653 
parent(const QModelIndex & child) const654 QModelIndex KFilePlacesModel::parent(const QModelIndex &child) const
655 {
656     Q_UNUSED(child);
657     return QModelIndex();
658 }
659 
rowCount(const QModelIndex & parent) const660 int KFilePlacesModel::rowCount(const QModelIndex &parent) const
661 {
662     if (parent.isValid()) {
663         return 0;
664     } else {
665         return d->items.size();
666     }
667 }
668 
columnCount(const QModelIndex & parent) const669 int KFilePlacesModel::columnCount(const QModelIndex &parent) const
670 {
671     Q_UNUSED(parent)
672     // We only know 1 piece of information for a particular entry
673     return 1;
674 }
675 
closestItem(const QUrl & url) const676 QModelIndex KFilePlacesModel::closestItem(const QUrl &url) const
677 {
678     int foundRow = -1;
679     int maxLength = 0;
680 
681     // Search the item which is equal to the URL or at least is a parent URL.
682     // If there are more than one possible item URL candidates, choose the item
683     // which covers the bigger range of the URL.
684     for (int row = 0; row < d->items.size(); ++row) {
685         KFilePlacesItem *item = d->items[row];
686 
687         if (item->isHidden() || isGroupHidden(item->groupType())) {
688             continue;
689         }
690 
691         const QUrl itemUrl(item->data(UrlRole).toUrl());
692 
693         if (itemUrl.matches(url, QUrl::StripTrailingSlash) || itemUrl.isParentOf(url)) {
694             const int length = itemUrl.toString().length();
695             if (length > maxLength) {
696                 foundRow = row;
697                 maxLength = length;
698             }
699         }
700     }
701 
702     if (foundRow == -1) {
703         return QModelIndex();
704     } else {
705         return createIndex(foundRow, 0, d->items[foundRow]);
706     }
707 }
708 
initDeviceList()709 void KFilePlacesModelPrivate::initDeviceList()
710 {
711     Solid::DeviceNotifier *notifier = Solid::DeviceNotifier::instance();
712 
713     QObject::connect(notifier, &Solid::DeviceNotifier::deviceAdded, q, [this](const QString &device) {
714         deviceAdded(device);
715     });
716     QObject::connect(notifier, &Solid::DeviceNotifier::deviceRemoved, q, [this](const QString &device) {
717         deviceRemoved(device);
718     });
719 
720     const QList<Solid::Device> &deviceList = Solid::Device::listFromQuery(predicate);
721 
722     availableDevices.reserve(deviceList.size());
723     for (const Solid::Device &device : deviceList) {
724         availableDevices << device.udi();
725     }
726 
727     reloadBookmarks();
728 }
729 
deviceAdded(const QString & udi)730 void KFilePlacesModelPrivate::deviceAdded(const QString &udi)
731 {
732     Solid::Device d(udi);
733 
734     if (predicate.matches(d)) {
735         availableDevices << udi;
736         reloadBookmarks();
737     }
738 }
739 
deviceRemoved(const QString & udi)740 void KFilePlacesModelPrivate::deviceRemoved(const QString &udi)
741 {
742     auto it = std::find(availableDevices.begin(), availableDevices.end(), udi);
743     if (it != availableDevices.end()) {
744         availableDevices.erase(it);
745         reloadBookmarks();
746     }
747 }
748 
itemChanged(const QString & id)749 void KFilePlacesModelPrivate::itemChanged(const QString &id)
750 {
751     for (int row = 0; row < items.size(); ++row) {
752         if (items.at(row)->id() == id) {
753             QModelIndex index = q->index(row, 0);
754             Q_EMIT q->dataChanged(index, index);
755         }
756     }
757 }
758 
reloadBookmarks()759 void KFilePlacesModelPrivate::reloadBookmarks()
760 {
761     QList<KFilePlacesItem *> currentItems = loadBookmarkList();
762 
763     QList<KFilePlacesItem *>::Iterator it_i = items.begin();
764     QList<KFilePlacesItem *>::Iterator it_c = currentItems.begin();
765 
766     QList<KFilePlacesItem *>::Iterator end_i = items.end();
767     QList<KFilePlacesItem *>::Iterator end_c = currentItems.end();
768 
769     while (it_i != end_i || it_c != end_c) {
770         if (it_i == end_i && it_c != end_c) {
771             int row = items.count();
772 
773             q->beginInsertRows(QModelIndex(), row, row);
774             it_i = items.insert(it_i, *it_c);
775             ++it_i;
776             it_c = currentItems.erase(it_c);
777 
778             end_i = items.end();
779             end_c = currentItems.end();
780             q->endInsertRows();
781 
782         } else if (it_i != end_i && it_c == end_c) {
783             int row = items.indexOf(*it_i);
784 
785             q->beginRemoveRows(QModelIndex(), row, row);
786             delete *it_i;
787             it_i = items.erase(it_i);
788 
789             end_i = items.end();
790             end_c = currentItems.end();
791             q->endRemoveRows();
792 
793         } else if ((*it_i)->id() == (*it_c)->id()) {
794             bool shouldEmit = !((*it_i)->bookmark() == (*it_c)->bookmark());
795             (*it_i)->setBookmark((*it_c)->bookmark());
796             if (shouldEmit) {
797                 int row = items.indexOf(*it_i);
798                 QModelIndex idx = q->index(row, 0);
799                 Q_EMIT q->dataChanged(idx, idx);
800             }
801             ++it_i;
802             ++it_c;
803         } else {
804             int row = items.indexOf(*it_i);
805 
806             if (it_i + 1 != end_i && (*(it_i + 1))->id() == (*it_c)->id()) { // if the next one matches, it's a remove
807                 q->beginRemoveRows(QModelIndex(), row, row);
808                 delete *it_i;
809                 it_i = items.erase(it_i);
810 
811                 end_i = items.end();
812                 end_c = currentItems.end();
813                 q->endRemoveRows();
814             } else {
815                 q->beginInsertRows(QModelIndex(), row, row);
816                 it_i = items.insert(it_i, *it_c);
817                 ++it_i;
818                 it_c = currentItems.erase(it_c);
819 
820                 end_i = items.end();
821                 end_c = currentItems.end();
822                 q->endInsertRows();
823             }
824         }
825     }
826 
827     qDeleteAll(currentItems);
828     currentItems.clear();
829 
830     Q_EMIT q->reloaded();
831 }
832 
isBalooUrl(const QUrl & url) const833 bool KFilePlacesModelPrivate::isBalooUrl(const QUrl &url) const
834 {
835     const QString scheme = url.scheme();
836     return ((scheme == QLatin1String("timeline")) || (scheme == QLatin1String("search")));
837 }
838 
loadBookmarkList()839 QList<KFilePlacesItem *> KFilePlacesModelPrivate::loadBookmarkList()
840 {
841     QList<KFilePlacesItem *> items;
842 
843     KBookmarkGroup root = bookmarkManager->root();
844     KBookmark bookmark = root.first();
845     QVector<QString> devices = availableDevices;
846     QVector<QString> tagsList = tags;
847 
848     while (!bookmark.isNull()) {
849         const QString udi = bookmark.metaDataItem(QStringLiteral("UDI"));
850         const QUrl url = bookmark.url();
851         const QString tag = bookmark.metaDataItem(QStringLiteral("tag"));
852         if (!udi.isEmpty() || url.isValid()) {
853             QString appName = bookmark.metaDataItem(QStringLiteral("OnlyInApp"));
854 
855             // If it's not a tag it's a device
856             if (tag.isEmpty()) {
857                 auto it = std::find(devices.begin(), devices.end(), udi);
858                 bool deviceAvailable = (it != devices.end());
859                 if (deviceAvailable) {
860                     devices.erase(it);
861                 }
862 
863                 bool allowedHere =
864                     appName.isEmpty() || ((appName == QCoreApplication::instance()->applicationName()) || (appName == alternativeApplicationName));
865                 bool isSupportedUrl = isBalooUrl(url) ? fileIndexingEnabled : true;
866                 bool isSupportedScheme = supportedSchemes.isEmpty() || supportedSchemes.contains(url.scheme());
867 
868                 KFilePlacesItem *item = nullptr;
869                 if (deviceAvailable) {
870                     item = new KFilePlacesItem(bookmarkManager, bookmark.address(), udi, q);
871                     if (!item->hasSupportedScheme(supportedSchemes)) {
872                         delete item;
873                         item = nullptr;
874                     }
875                 } else if (isSupportedScheme && isSupportedUrl && udi.isEmpty() && allowedHere) {
876                     // TODO: Update bookmark internal element
877                     item = new KFilePlacesItem(bookmarkManager, bookmark.address(), QString(), q);
878                 }
879 
880                 if (item) {
881                     QObject::connect(item, &KFilePlacesItem::itemChanged, q, [this](const QString &id) {
882                         itemChanged(id);
883                     });
884 
885                     items << item;
886                 }
887             } else {
888                 auto it = std::find(tagsList.begin(), tagsList.end(), tag);
889                 if (it != tagsList.end()) {
890                     tagsList.removeAll(tag);
891                     KFilePlacesItem *item = new KFilePlacesItem(bookmarkManager, bookmark.address(), QString(), q);
892                     items << item;
893                     QObject::connect(item, &KFilePlacesItem::itemChanged, q, [this](const QString &id) {
894                         itemChanged(id);
895                     });
896                 }
897             }
898         }
899 
900         bookmark = root.next(bookmark);
901     }
902 
903     // Add bookmarks for the remaining devices, they were previously unknown
904     for (const QString &udi : std::as_const(devices)) {
905         bookmark = KFilePlacesItem::createDeviceBookmark(bookmarkManager, udi);
906         if (!bookmark.isNull()) {
907             KFilePlacesItem *item = new KFilePlacesItem(bookmarkManager, bookmark.address(), udi, q);
908             QObject::connect(item, &KFilePlacesItem::itemChanged, q, [this](const QString &id) {
909                 itemChanged(id);
910             });
911             // TODO: Update bookmark internal element
912             items << item;
913         }
914     }
915 
916     for (const QString &tag : tagsList) {
917         bookmark = KFilePlacesItem::createTagBookmark(bookmarkManager, tag);
918         if (!bookmark.isNull()) {
919             KFilePlacesItem *item = new KFilePlacesItem(bookmarkManager, bookmark.address(), tag, q);
920             QObject::connect(item, &KFilePlacesItem::itemChanged, q, [this](const QString &id) {
921                 itemChanged(id);
922             });
923             items << item;
924         }
925     }
926 
927     // return a sorted list based on groups
928     std::stable_sort(items.begin(), items.end(), [](KFilePlacesItem *itemA, KFilePlacesItem *itemB) {
929         return (itemA->groupType() < itemB->groupType());
930     });
931 
932     return items;
933 }
934 
findNearestPosition(int source,int target)935 int KFilePlacesModelPrivate::findNearestPosition(int source, int target)
936 {
937     const KFilePlacesItem *item = items.at(source);
938     const KFilePlacesModel::GroupType groupType = item->groupType();
939     int newTarget = qMin(target, items.count() - 1);
940 
941     // moving inside the same group is ok
942     if ((items.at(newTarget)->groupType() == groupType)) {
943         return target;
944     }
945 
946     if (target > source) { // moving down, move it to the end of the group
947         int groupFooter = source;
948         while (items.at(groupFooter)->groupType() == groupType) {
949             groupFooter++;
950             // end of the list move it there
951             if (groupFooter == items.count()) {
952                 break;
953             }
954         }
955         target = groupFooter;
956     } else { // moving up, move it to beginning of the group
957         int groupHead = source;
958         while (items.at(groupHead)->groupType() == groupType) {
959             groupHead--;
960             // beginning of the list move it there
961             if (groupHead == 0) {
962                 break;
963             }
964         }
965         target = groupHead;
966     }
967     return target;
968 }
969 
reloadAndSignal()970 void KFilePlacesModelPrivate::reloadAndSignal()
971 {
972     bookmarkManager->emitChanged(bookmarkManager->root()); // ... we'll get relisted anyway
973 }
974 
supportedDropActions() const975 Qt::DropActions KFilePlacesModel::supportedDropActions() const
976 {
977     return Qt::ActionMask;
978 }
979 
flags(const QModelIndex & index) const980 Qt::ItemFlags KFilePlacesModel::flags(const QModelIndex &index) const
981 {
982     Qt::ItemFlags res;
983 
984     if (index.isValid()) {
985         res |= Qt::ItemIsDragEnabled | Qt::ItemIsSelectable | Qt::ItemIsEnabled;
986     }
987 
988     if (!index.isValid()) {
989         res |= Qt::ItemIsDropEnabled;
990     }
991 
992     return res;
993 }
994 
_k_internalMimetype(const KFilePlacesModel * const self)995 static QString _k_internalMimetype(const KFilePlacesModel *const self)
996 {
997     return QStringLiteral("application/x-kfileplacesmodel-") + QString::number(reinterpret_cast<qptrdiff>(self));
998 }
999 
mimeTypes() const1000 QStringList KFilePlacesModel::mimeTypes() const
1001 {
1002     QStringList types;
1003 
1004     types << _k_internalMimetype(this) << QStringLiteral("text/uri-list");
1005 
1006     return types;
1007 }
1008 
mimeData(const QModelIndexList & indexes) const1009 QMimeData *KFilePlacesModel::mimeData(const QModelIndexList &indexes) const
1010 {
1011     QList<QUrl> urls;
1012     QByteArray itemData;
1013 
1014     QDataStream stream(&itemData, QIODevice::WriteOnly);
1015 
1016     for (const QModelIndex &index : std::as_const(indexes)) {
1017         QUrl itemUrl = url(index);
1018         if (itemUrl.isValid()) {
1019             urls << itemUrl;
1020         }
1021         stream << index.row();
1022     }
1023 
1024     QMimeData *mimeData = new QMimeData();
1025 
1026     if (!urls.isEmpty()) {
1027         mimeData->setUrls(urls);
1028     }
1029 
1030     mimeData->setData(_k_internalMimetype(this), itemData);
1031 
1032     return mimeData;
1033 }
1034 
dropMimeData(const QMimeData * data,Qt::DropAction action,int row,int column,const QModelIndex & parent)1035 bool KFilePlacesModel::dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent)
1036 {
1037     if (action == Qt::IgnoreAction) {
1038         return true;
1039     }
1040 
1041     if (column > 0) {
1042         return false;
1043     }
1044 
1045     if (row == -1 && parent.isValid()) {
1046         return false; // Don't allow to move an item onto another one,
1047         // too easy for the user to mess something up
1048         // If we really really want to allow copying files this way,
1049         // let's do it in the views to get the good old drop menu
1050     }
1051 
1052     if (data->hasFormat(_k_internalMimetype(this))) {
1053         // The operation is an internal move
1054         QByteArray itemData = data->data(_k_internalMimetype(this));
1055         QDataStream stream(&itemData, QIODevice::ReadOnly);
1056         int itemRow;
1057 
1058         stream >> itemRow;
1059 
1060         if (!movePlace(itemRow, row)) {
1061             return false;
1062         }
1063 
1064     } else if (data->hasFormat(QStringLiteral("text/uri-list"))) {
1065         // The operation is an add
1066 
1067         QMimeDatabase db;
1068         KBookmark afterBookmark;
1069 
1070         if (row == -1) {
1071             // The dropped item is moved or added to the last position
1072 
1073             KFilePlacesItem *lastItem = d->items.last();
1074             afterBookmark = lastItem->bookmark();
1075 
1076         } else {
1077             // The dropped item is moved or added before position 'row', ie after position 'row-1'
1078 
1079             if (row > 0) {
1080                 KFilePlacesItem *afterItem = d->items[row - 1];
1081                 afterBookmark = afterItem->bookmark();
1082             }
1083         }
1084 
1085         const QList<QUrl> urls = KUrlMimeData::urlsFromMimeData(data);
1086 
1087         KBookmarkGroup group = d->bookmarkManager->root();
1088 
1089         for (const QUrl &url : urls) {
1090             // TODO: use KIO::stat in order to get the UDS_DISPLAY_NAME too
1091             KIO::MimetypeJob *job = KIO::mimetype(url);
1092 
1093             QString mimeString;
1094             if (!job->exec()) {
1095                 mimeString = QStringLiteral("unknown");
1096             } else {
1097                 mimeString = job->mimetype();
1098             }
1099 
1100             QMimeType mimetype = db.mimeTypeForName(mimeString);
1101 
1102             if (!mimetype.isValid()) {
1103                 qWarning() << "URL not added to Places as MIME type could not be determined!";
1104                 continue;
1105             }
1106 
1107             if (!mimetype.inherits(QStringLiteral("inode/directory"))) {
1108                 // Only directories are allowed
1109                 continue;
1110             }
1111 
1112             KFileItem item(url, mimetype.name(), S_IFDIR);
1113 
1114             KBookmark bookmark = KFilePlacesItem::createBookmark(d->bookmarkManager, url.fileName(), url, item.iconName());
1115             group.moveBookmark(bookmark, afterBookmark);
1116             afterBookmark = bookmark;
1117         }
1118 
1119     } else {
1120         // Oops, shouldn't happen thanks to mimeTypes()
1121         qWarning() << ": received wrong mimedata, " << data->formats();
1122         return false;
1123     }
1124 
1125     refresh();
1126 
1127     return true;
1128 }
1129 
refresh() const1130 void KFilePlacesModel::refresh() const
1131 {
1132     d->reloadAndSignal();
1133 }
1134 
convertedUrl(const QUrl & url)1135 QUrl KFilePlacesModel::convertedUrl(const QUrl &url)
1136 {
1137     QUrl newUrl = url;
1138     if (url.scheme() == QLatin1String("timeline")) {
1139         newUrl = createTimelineUrl(url);
1140     } else if (url.scheme() == QLatin1String("search")) {
1141         newUrl = createSearchUrl(url);
1142     }
1143 
1144     return newUrl;
1145 }
1146 
addPlace(const QString & text,const QUrl & url,const QString & iconName,const QString & appName)1147 void KFilePlacesModel::addPlace(const QString &text, const QUrl &url, const QString &iconName, const QString &appName)
1148 {
1149     addPlace(text, url, iconName, appName, QModelIndex());
1150 }
1151 
addPlace(const QString & text,const QUrl & url,const QString & iconName,const QString & appName,const QModelIndex & after)1152 void KFilePlacesModel::addPlace(const QString &text, const QUrl &url, const QString &iconName, const QString &appName, const QModelIndex &after)
1153 {
1154     KBookmark bookmark = KFilePlacesItem::createBookmark(d->bookmarkManager, text, url, iconName);
1155 
1156     if (!appName.isEmpty()) {
1157         bookmark.setMetaDataItem(QStringLiteral("OnlyInApp"), appName);
1158     }
1159 
1160     if (after.isValid()) {
1161         KFilePlacesItem *item = static_cast<KFilePlacesItem *>(after.internalPointer());
1162         d->bookmarkManager->root().moveBookmark(bookmark, item->bookmark());
1163     }
1164 
1165     refresh();
1166 }
1167 
editPlace(const QModelIndex & index,const QString & text,const QUrl & url,const QString & iconName,const QString & appName)1168 void KFilePlacesModel::editPlace(const QModelIndex &index, const QString &text, const QUrl &url, const QString &iconName, const QString &appName)
1169 {
1170     if (!index.isValid()) {
1171         return;
1172     }
1173 
1174     KFilePlacesItem *item = static_cast<KFilePlacesItem *>(index.internalPointer());
1175 
1176     if (item->isDevice()) {
1177         return;
1178     }
1179 
1180     KBookmark bookmark = item->bookmark();
1181 
1182     if (bookmark.isNull()) {
1183         return;
1184     }
1185 
1186     QVector<int> changedRoles;
1187     bool changed = false;
1188 
1189     if (text != bookmark.fullText()) {
1190         bookmark.setFullText(text);
1191         changed = true;
1192         changedRoles << Qt::DisplayRole;
1193     }
1194 
1195     if (url != bookmark.url()) {
1196         bookmark.setUrl(url);
1197         changed = true;
1198         changedRoles << KFilePlacesModel::UrlRole;
1199     }
1200 
1201     if (iconName != bookmark.icon()) {
1202         bookmark.setIcon(iconName);
1203         changed = true;
1204         changedRoles << Qt::DecorationRole;
1205     }
1206 
1207     const QString onlyInApp = bookmark.metaDataItem(QStringLiteral("OnlyInApp"));
1208     if (appName != onlyInApp) {
1209         bookmark.setMetaDataItem(QStringLiteral("OnlyInApp"), appName);
1210         changed = true;
1211     }
1212 
1213     if (changed) {
1214         refresh();
1215         Q_EMIT dataChanged(index, index, changedRoles);
1216     }
1217 }
1218 
removePlace(const QModelIndex & index) const1219 void KFilePlacesModel::removePlace(const QModelIndex &index) const
1220 {
1221     if (!index.isValid()) {
1222         return;
1223     }
1224 
1225     KFilePlacesItem *item = static_cast<KFilePlacesItem *>(index.internalPointer());
1226 
1227     if (item->isDevice()) {
1228         return;
1229     }
1230 
1231     KBookmark bookmark = item->bookmark();
1232 
1233     if (bookmark.isNull()) {
1234         return;
1235     }
1236 
1237     d->bookmarkManager->root().deleteBookmark(bookmark);
1238     refresh();
1239 }
1240 
setPlaceHidden(const QModelIndex & index,bool hidden)1241 void KFilePlacesModel::setPlaceHidden(const QModelIndex &index, bool hidden)
1242 {
1243     if (!index.isValid()) {
1244         return;
1245     }
1246 
1247     KFilePlacesItem *item = static_cast<KFilePlacesItem *>(index.internalPointer());
1248 
1249     if (item->bookmark().isNull() || item->isHidden() == hidden) {
1250         return;
1251     }
1252 
1253     const bool groupHidden = isGroupHidden(item->groupType());
1254     const bool hidingChildOnShownParent = hidden && !groupHidden;
1255     const bool showingChildOnShownParent = !hidden && !groupHidden;
1256 
1257     if (hidingChildOnShownParent || showingChildOnShownParent) {
1258         item->setHidden(hidden);
1259 
1260         d->reloadAndSignal();
1261         Q_EMIT dataChanged(index, index, { KFilePlacesModel::HiddenRole });
1262     }
1263 }
1264 
setGroupHidden(const GroupType type,bool hidden)1265 void KFilePlacesModel::setGroupHidden(const GroupType type, bool hidden)
1266 {
1267     if (isGroupHidden(type) == hidden) {
1268         return;
1269     }
1270 
1271     d->bookmarkManager->root().setMetaDataItem(stateNameForGroupType(type), (hidden ? QStringLiteral("true") : QStringLiteral("false")));
1272     d->reloadAndSignal();
1273     Q_EMIT groupHiddenChanged(type, hidden);
1274 }
1275 
movePlace(int itemRow,int row)1276 bool KFilePlacesModel::movePlace(int itemRow, int row)
1277 {
1278     KBookmark afterBookmark;
1279 
1280     if ((itemRow < 0) || (itemRow >= d->items.count())) {
1281         return false;
1282     }
1283 
1284     if (row >= d->items.count()) {
1285         row = -1;
1286     }
1287 
1288     if (row == -1) {
1289         // The dropped item is moved or added to the last position
1290 
1291         KFilePlacesItem *lastItem = d->items.last();
1292         afterBookmark = lastItem->bookmark();
1293 
1294     } else {
1295         // The dropped item is moved or added before position 'row', ie after position 'row-1'
1296 
1297         if (row > 0) {
1298             KFilePlacesItem *afterItem = d->items[row - 1];
1299             afterBookmark = afterItem->bookmark();
1300         }
1301     }
1302 
1303     KFilePlacesItem *item = d->items[itemRow];
1304     KBookmark bookmark = item->bookmark();
1305 
1306     int destRow = row == -1 ? d->items.count() : row;
1307 
1308     // avoid move item away from its group
1309     destRow = d->findNearestPosition(itemRow, destRow);
1310 
1311     // The item is not moved when the drop indicator is on either item edge
1312     if (itemRow == destRow || itemRow + 1 == destRow) {
1313         return false;
1314     }
1315 
1316     beginMoveRows(QModelIndex(), itemRow, itemRow, QModelIndex(), destRow);
1317     d->bookmarkManager->root().moveBookmark(bookmark, afterBookmark);
1318     // Move item ourselves so that reloadBookmarks() does not consider
1319     // the move as a remove + insert.
1320     //
1321     // 2nd argument of QList::move() expects the final destination index,
1322     // but 'row' is the value of the destination index before the moved
1323     // item has been removed from its original position. That is why we
1324     // adjust if necessary.
1325     d->items.move(itemRow, itemRow < destRow ? (destRow - 1) : destRow);
1326     endMoveRows();
1327 
1328     return true;
1329 }
1330 
hiddenCount() const1331 int KFilePlacesModel::hiddenCount() const
1332 {
1333     int rows = rowCount();
1334     int hidden = 0;
1335 
1336     for (int i = 0; i < rows; ++i) {
1337         if (isHidden(index(i, 0))) {
1338             hidden++;
1339         }
1340     }
1341 
1342     return hidden;
1343 }
1344 
teardownActionForIndex(const QModelIndex & index) const1345 QAction *KFilePlacesModel::teardownActionForIndex(const QModelIndex &index) const
1346 {
1347     Solid::Device device = deviceForIndex(index);
1348 
1349     if (device.is<Solid::StorageAccess>() && device.as<Solid::StorageAccess>()->isAccessible()) {
1350         Solid::StorageDrive *drive = device.as<Solid::StorageDrive>();
1351 
1352         if (drive == nullptr) {
1353             drive = device.parent().as<Solid::StorageDrive>();
1354         }
1355 
1356         bool hotpluggable = false;
1357         bool removable = false;
1358 
1359         if (drive != nullptr) {
1360             hotpluggable = drive->isHotpluggable();
1361             removable = drive->isRemovable();
1362         }
1363 
1364         QString iconName;
1365         QString text;
1366         QString label = data(index, Qt::DisplayRole).toString().replace(QLatin1Char('&'), QLatin1String("&&"));
1367 
1368         if (device.is<Solid::OpticalDisc>()) {
1369             text = i18n("&Release '%1'", label);
1370         } else if (removable || hotpluggable) {
1371             text = i18n("&Safely Remove '%1'", label);
1372             iconName = QStringLiteral("media-eject");
1373         } else {
1374             text = i18n("&Unmount '%1'", label);
1375             iconName = QStringLiteral("media-eject");
1376         }
1377 
1378         if (!iconName.isEmpty()) {
1379             return new QAction(QIcon::fromTheme(iconName), text, nullptr);
1380         } else {
1381             return new QAction(text, nullptr);
1382         }
1383     }
1384 
1385     return nullptr;
1386 }
1387 
ejectActionForIndex(const QModelIndex & index) const1388 QAction *KFilePlacesModel::ejectActionForIndex(const QModelIndex &index) const
1389 {
1390     Solid::Device device = deviceForIndex(index);
1391 
1392     if (device.is<Solid::OpticalDisc>()) {
1393         QString label = data(index, Qt::DisplayRole).toString().replace(QLatin1Char('&'), QLatin1String("&&"));
1394         QString text = i18n("&Eject '%1'", label);
1395 
1396         return new QAction(QIcon::fromTheme(QStringLiteral("media-eject")), text, nullptr);
1397     }
1398 
1399     return nullptr;
1400 }
1401 
requestTeardown(const QModelIndex & index)1402 void KFilePlacesModel::requestTeardown(const QModelIndex &index)
1403 {
1404     Solid::Device device = deviceForIndex(index);
1405     Solid::StorageAccess *access = device.as<Solid::StorageAccess>();
1406 
1407     if (access != nullptr) {
1408         connect(access, &Solid::StorageAccess::teardownDone, this, [this](Solid::ErrorType error, QVariant errorData) {
1409             d->storageTeardownDone(error, errorData);
1410         });
1411 
1412         access->teardown();
1413     }
1414 }
1415 
requestEject(const QModelIndex & index)1416 void KFilePlacesModel::requestEject(const QModelIndex &index)
1417 {
1418     Solid::Device device = deviceForIndex(index);
1419 
1420     Solid::OpticalDrive *drive = device.parent().as<Solid::OpticalDrive>();
1421 
1422     if (drive != nullptr) {
1423         connect(drive, &Solid::OpticalDrive::ejectDone, this, [this](Solid::ErrorType error, QVariant errorData) {
1424             d->storageTeardownDone(error, errorData);
1425         });
1426 
1427         drive->eject();
1428     } else {
1429         QString label = data(index, Qt::DisplayRole).toString().replace(QLatin1Char('&'), QLatin1String("&&"));
1430         QString message = i18n("The device '%1' is not a disk and cannot be ejected.", label);
1431         Q_EMIT errorMessage(message);
1432     }
1433 }
1434 
requestSetup(const QModelIndex & index)1435 void KFilePlacesModel::requestSetup(const QModelIndex &index)
1436 {
1437     Solid::Device device = deviceForIndex(index);
1438 
1439     if (device.is<Solid::StorageAccess>() && !d->setupInProgress.contains(device.as<Solid::StorageAccess>())
1440         && !device.as<Solid::StorageAccess>()->isAccessible()) {
1441         Solid::StorageAccess *access = device.as<Solid::StorageAccess>();
1442 
1443         d->setupInProgress[access] = index;
1444 
1445         connect(access, &Solid::StorageAccess::setupDone, this, [this, access](Solid::ErrorType error, QVariant errorData) {
1446             d->storageSetupDone(error, errorData, access);
1447         });
1448 
1449         access->setup();
1450     }
1451 }
1452 
storageSetupDone(Solid::ErrorType error,const QVariant & errorData,Solid::StorageAccess * sender)1453 void KFilePlacesModelPrivate::storageSetupDone(Solid::ErrorType error, const QVariant &errorData, Solid::StorageAccess *sender)
1454 {
1455     QPersistentModelIndex index = setupInProgress.take(sender);
1456 
1457     if (!index.isValid()) {
1458         return;
1459     }
1460 
1461     if (!error) {
1462         Q_EMIT q->setupDone(index, true);
1463     } else {
1464         if (errorData.isValid()) {
1465             Q_EMIT q->errorMessage(i18n("An error occurred while accessing '%1', the system responded: %2", q->text(index), errorData.toString()));
1466         } else {
1467             Q_EMIT q->errorMessage(i18n("An error occurred while accessing '%1'", q->text(index)));
1468         }
1469         Q_EMIT q->setupDone(index, false);
1470     }
1471 }
1472 
storageTeardownDone(Solid::ErrorType error,const QVariant & errorData)1473 void KFilePlacesModelPrivate::storageTeardownDone(Solid::ErrorType error, const QVariant &errorData)
1474 {
1475     if (error && errorData.isValid()) {
1476         Q_EMIT q->errorMessage(errorData.toString());
1477     }
1478 }
1479 
setSupportedSchemes(const QStringList & schemes)1480 void KFilePlacesModel::setSupportedSchemes(const QStringList &schemes)
1481 {
1482     d->supportedSchemes = schemes;
1483     d->reloadBookmarks();
1484 }
1485 
supportedSchemes() const1486 QStringList KFilePlacesModel::supportedSchemes() const
1487 {
1488     return d->supportedSchemes;
1489 }
1490 
1491 #include "moc_kfileplacesmodel.cpp"
1492