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 | |
48 | namespace |
49 | { |
50 | QString 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 | |
72 | static 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 | |
79 | static 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 | |
90 | static 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 | |
117 | static 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 | |
138 | KFilePlacesModelPrivate::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 | |
185 | QString KFilePlacesModelPrivate::ignoreMimeType() |
186 | { |
187 | return QStringLiteral("application/x-kfileplacesmodel-ignore" ); |
188 | } |
189 | |
190 | QString KFilePlacesModelPrivate::internalMimeType(const KFilePlacesModel *model) |
191 | { |
192 | return QStringLiteral("application/x-kfileplacesmodel-" ) + QString::number(reinterpret_cast<qptrdiff>(model)); |
193 | } |
194 | |
195 | KBookmark 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 | |
208 | static inline QString versionKey() |
209 | { |
210 | return QStringLiteral("kde_places_version" ); |
211 | } |
212 | |
213 | KFilePlacesModel::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 | |
464 | KFilePlacesModel::~KFilePlacesModel() = default; |
465 | |
466 | QUrl KFilePlacesModel::url(const QModelIndex &index) const |
467 | { |
468 | return data(index, role: UrlRole).toUrl(); |
469 | } |
470 | |
471 | bool KFilePlacesModel::setupNeeded(const QModelIndex &index) const |
472 | { |
473 | return data(index, role: SetupNeededRole).toBool(); |
474 | } |
475 | |
476 | QIcon KFilePlacesModel::icon(const QModelIndex &index) const |
477 | { |
478 | return data(index, role: Qt::DecorationRole).value<QIcon>(); |
479 | } |
480 | |
481 | QString KFilePlacesModel::text(const QModelIndex &index) const |
482 | { |
483 | return data(index, role: Qt::DisplayRole).toString(); |
484 | } |
485 | |
486 | bool 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 | |
492 | bool 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 | |
498 | bool 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 | |
508 | bool 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 | |
519 | bool 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 | |
529 | bool 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 | |
539 | bool 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 | |
549 | KFilePlacesModel::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 | |
559 | Solid::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 | |
574 | KBookmark 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 | |
584 | KFilePlacesModel::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 | |
594 | QModelIndexList 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 | |
612 | QVariant 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 | |
626 | QModelIndex 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 | |
639 | QModelIndex KFilePlacesModel::parent(const QModelIndex &child) const |
640 | { |
641 | Q_UNUSED(child); |
642 | return QModelIndex(); |
643 | } |
644 | |
645 | QHash<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 | |
665 | int KFilePlacesModel::rowCount(const QModelIndex &parent) const |
666 | { |
667 | if (parent.isValid()) { |
668 | return 0; |
669 | } else { |
670 | return d->items.size(); |
671 | } |
672 | } |
673 | |
674 | int 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 | |
681 | QModelIndex 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 | |
715 | void 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 | |
731 | void 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 | |
741 | void 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 | |
752 | void 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 | |
762 | void 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 | |
836 | bool KFilePlacesModelPrivate::isBalooUrl(const QUrl &url) const |
837 | { |
838 | const QString scheme = url.scheme(); |
839 | return ((scheme == QLatin1String("timeline" )) || (scheme == QLatin1String("search" ))); |
840 | } |
841 | |
842 | QList<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 | |
934 | int 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 = 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 | |
969 | void KFilePlacesModelPrivate::reloadAndSignal() |
970 | { |
971 | bookmarkManager->emitChanged(group: bookmarkManager->root()); // ... we'll get relisted anyway |
972 | } |
973 | |
974 | Qt::DropActions KFilePlacesModel::supportedDropActions() const |
975 | { |
976 | return Qt::ActionMask; |
977 | } |
978 | |
979 | Qt::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 | |
994 | QStringList KFilePlacesModel::mimeTypes() const |
995 | { |
996 | QStringList types; |
997 | |
998 | types << KFilePlacesModelPrivate::internalMimeType(model: this) << QStringLiteral("text/uri-list" ); |
999 | |
1000 | return types; |
1001 | } |
1002 | |
1003 | QMimeData *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 | |
1029 | bool 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 | |
1119 | void KFilePlacesModel::refresh() const |
1120 | { |
1121 | d->reloadAndSignal(); |
1122 | } |
1123 | |
1124 | QUrl 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 | |
1136 | void 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 | |
1141 | void 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 | |
1157 | void 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 | |
1208 | void 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 | |
1230 | void 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 | |
1254 | void 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 | |
1265 | bool 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 | |
1320 | int 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 | |
1334 | QAction *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 | |
1396 | QAction *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 | |
1409 | void 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 | |
1426 | void 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 | |
1453 | void 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 | |
1471 | void 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 | |
1491 | void 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 | |
1530 | void KFilePlacesModel::setSupportedSchemes(const QStringList &schemes) |
1531 | { |
1532 | d->supportedSchemes = schemes; |
1533 | d->reloadBookmarks(); |
1534 | Q_EMIT supportedSchemesChanged(); |
1535 | } |
1536 | |
1537 | QStringList KFilePlacesModel::supportedSchemes() const |
1538 | { |
1539 | return d->supportedSchemes; |
1540 | } |
1541 | |
1542 | namespace { |
1543 | QString partitionManagerPath() |
1544 | { |
1545 | static const QString path = QStandardPaths::findExecutable(QStringLiteral("partitionmanager" )); |
1546 | return path; |
1547 | } |
1548 | } // namespace |
1549 | |
1550 | QAction *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 | |