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

source code of kio/src/filewidgets/kfileplacesmodel.cpp