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