| 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 | KIO::StatResolveSymlink); |
| 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 removable = false; |
| 1350 | if (drive != nullptr) { |
| 1351 | removable = drive->isRemovable(); |
| 1352 | } |
| 1353 | |
| 1354 | QString iconName; |
| 1355 | QString text; |
| 1356 | |
| 1357 | if (device.is<Solid::OpticalDisc>()) { |
| 1358 | if (teardownInProgress) { |
| 1359 | text = i18nc("@action:inmenu" , "Releasing…" ); |
| 1360 | } else { |
| 1361 | text = i18nc("@action:inmenu" , "&Release" ); |
| 1362 | } |
| 1363 | } else if (removable) { |
| 1364 | if (teardownInProgress) { |
| 1365 | text = i18nc("@action:inmenu" , "Safely Removing…" ); |
| 1366 | } else { |
| 1367 | text = i18nc("@action:inmenu" , "&Safely Remove" ); |
| 1368 | } |
| 1369 | iconName = QStringLiteral("media-eject" ); |
| 1370 | } else { |
| 1371 | if (teardownInProgress) { |
| 1372 | text = i18nc("@action:inmenu" , "Unmounting…" ); |
| 1373 | } else { |
| 1374 | text = i18nc("@action:inmenu" , "&Unmount" ); |
| 1375 | } |
| 1376 | iconName = QStringLiteral("media-eject" ); |
| 1377 | } |
| 1378 | |
| 1379 | if (!iconName.isEmpty()) { |
| 1380 | action = new QAction(QIcon::fromTheme(name: iconName), text, nullptr); |
| 1381 | } else { |
| 1382 | action = new QAction(text, nullptr); |
| 1383 | } |
| 1384 | |
| 1385 | if (teardownInProgress) { |
| 1386 | action->setEnabled(false); |
| 1387 | } |
| 1388 | } |
| 1389 | |
| 1390 | return action; |
| 1391 | } |
| 1392 | |
| 1393 | QAction *KFilePlacesModel::ejectActionForIndex(const QModelIndex &index) const |
| 1394 | { |
| 1395 | Solid::Device device = deviceForIndex(index); |
| 1396 | |
| 1397 | if (device.is<Solid::OpticalDisc>()) { |
| 1398 | QString text = i18nc("@action:inmenu" , "&Eject" ); |
| 1399 | |
| 1400 | return new QAction(QIcon::fromTheme(QStringLiteral("media-eject" )), text, nullptr); |
| 1401 | } |
| 1402 | |
| 1403 | return nullptr; |
| 1404 | } |
| 1405 | |
| 1406 | void KFilePlacesModel::requestTeardown(const QModelIndex &index) |
| 1407 | { |
| 1408 | Solid::Device device = deviceForIndex(index); |
| 1409 | Solid::StorageAccess *access = device.as<Solid::StorageAccess>(); |
| 1410 | |
| 1411 | if (access != nullptr) { |
| 1412 | d->teardownInProgress[access] = index; |
| 1413 | |
| 1414 | const QString filePath = access->filePath(); |
| 1415 | connect(sender: access, signal: &Solid::StorageAccess::teardownDone, context: this, slot: [this, access, filePath](Solid::ErrorType error, QVariant errorData) { |
| 1416 | d->storageTeardownDone(filePath, error, errorData, sender: access); |
| 1417 | }); |
| 1418 | |
| 1419 | access->teardown(); |
| 1420 | } |
| 1421 | } |
| 1422 | |
| 1423 | void KFilePlacesModel::requestEject(const QModelIndex &index) |
| 1424 | { |
| 1425 | Solid::Device device = deviceForIndex(index); |
| 1426 | |
| 1427 | Solid::OpticalDrive *drive = device.parent().as<Solid::OpticalDrive>(); |
| 1428 | |
| 1429 | if (drive != nullptr) { |
| 1430 | d->teardownInProgress[drive] = index; |
| 1431 | |
| 1432 | QString filePath; |
| 1433 | Solid::StorageAccess *access = device.as<Solid::StorageAccess>(); |
| 1434 | if (access) { |
| 1435 | filePath = access->filePath(); |
| 1436 | } |
| 1437 | |
| 1438 | connect(sender: drive, signal: &Solid::OpticalDrive::ejectDone, context: this, slot: [this, filePath, drive](Solid::ErrorType error, QVariant errorData) { |
| 1439 | d->storageTeardownDone(filePath, error, errorData, sender: drive); |
| 1440 | }); |
| 1441 | |
| 1442 | drive->eject(); |
| 1443 | } else { |
| 1444 | QString label = data(index, role: Qt::DisplayRole).toString().replace(c: QLatin1Char('&'), after: QLatin1String("&&" )); |
| 1445 | QString message = i18n("The device '%1' is not a disk and cannot be ejected." , label); |
| 1446 | Q_EMIT errorMessage(message); |
| 1447 | } |
| 1448 | } |
| 1449 | |
| 1450 | void KFilePlacesModel::requestSetup(const QModelIndex &index) |
| 1451 | { |
| 1452 | Solid::Device device = deviceForIndex(index); |
| 1453 | |
| 1454 | if (device.is<Solid::StorageAccess>() && !d->setupInProgress.contains(key: device.as<Solid::StorageAccess>()) |
| 1455 | && !device.as<Solid::StorageAccess>()->isAccessible()) { |
| 1456 | Solid::StorageAccess *access = device.as<Solid::StorageAccess>(); |
| 1457 | |
| 1458 | d->setupInProgress[access] = index; |
| 1459 | |
| 1460 | connect(sender: access, signal: &Solid::StorageAccess::setupDone, context: this, slot: [this, access](Solid::ErrorType error, QVariant errorData) { |
| 1461 | d->storageSetupDone(error, errorData, sender: access); |
| 1462 | }); |
| 1463 | |
| 1464 | access->setup(); |
| 1465 | } |
| 1466 | } |
| 1467 | |
| 1468 | void KFilePlacesModelPrivate::storageSetupDone(Solid::ErrorType error, const QVariant &errorData, Solid::StorageAccess *sender) |
| 1469 | { |
| 1470 | QPersistentModelIndex index = setupInProgress.take(key: sender); |
| 1471 | |
| 1472 | if (!index.isValid()) { |
| 1473 | return; |
| 1474 | } |
| 1475 | |
| 1476 | if (!error) { |
| 1477 | Q_EMIT q->setupDone(index, success: true); |
| 1478 | } else { |
| 1479 | if (errorData.isValid()) { |
| 1480 | Q_EMIT q->errorMessage(i18n("An error occurred while accessing '%1', the system responded: %2" , q->text(index), errorData.toString())); |
| 1481 | } else { |
| 1482 | Q_EMIT q->errorMessage(i18n("An error occurred while accessing '%1'" , q->text(index))); |
| 1483 | } |
| 1484 | Q_EMIT q->setupDone(index, success: false); |
| 1485 | } |
| 1486 | } |
| 1487 | |
| 1488 | void KFilePlacesModelPrivate::storageTeardownDone(const QString &filePath, Solid::ErrorType error, const QVariant &errorData, QObject *sender) |
| 1489 | { |
| 1490 | QPersistentModelIndex index = teardownInProgress.take(key: sender); |
| 1491 | if (!index.isValid()) { |
| 1492 | return; |
| 1493 | } |
| 1494 | |
| 1495 | if (error == Solid::ErrorType::DeviceBusy && !filePath.isEmpty()) { |
| 1496 | auto *listOpenFilesJob = new KListOpenFilesJob(filePath); |
| 1497 | QObject::connect(sender: listOpenFilesJob, signal: &KIO::Job::result, context: q, slot: [this, index, error, errorData, listOpenFilesJob]() { |
| 1498 | const auto blockingProcesses = listOpenFilesJob->processInfoList(); |
| 1499 | |
| 1500 | QStringList blockingApps; |
| 1501 | blockingApps.reserve(asize: blockingProcesses.count()); |
| 1502 | for (const auto &process : blockingProcesses) { |
| 1503 | blockingApps << process.name(); |
| 1504 | } |
| 1505 | |
| 1506 | Q_EMIT q->teardownDone(index, error, errorData); |
| 1507 | if (blockingProcesses.isEmpty()) { |
| 1508 | Q_EMIT q->errorMessage(i18n("One or more files on this device are open within an application." )); |
| 1509 | } else { |
| 1510 | blockingApps.removeDuplicates(); |
| 1511 | Q_EMIT q->errorMessage(xi18np("One or more files on this device are opened in application <application>\"%2\"</application>." , |
| 1512 | "One or more files on this device are opened in following applications: <application>%2</application>." , |
| 1513 | blockingApps.count(), |
| 1514 | blockingApps.join(i18nc("separator in list of apps blocking device unmount" , ", " )))); |
| 1515 | } |
| 1516 | }); |
| 1517 | listOpenFilesJob->start(); |
| 1518 | return; |
| 1519 | } |
| 1520 | |
| 1521 | Q_EMIT q->teardownDone(index, error, errorData); |
| 1522 | if (error != Solid::ErrorType::NoError && error != Solid::ErrorType::UserCanceled) { |
| 1523 | Q_EMIT q->errorMessage(message: errorData.toString()); |
| 1524 | } |
| 1525 | } |
| 1526 | |
| 1527 | void KFilePlacesModel::setSupportedSchemes(const QStringList &schemes) |
| 1528 | { |
| 1529 | d->supportedSchemes = schemes; |
| 1530 | d->reloadBookmarks(); |
| 1531 | Q_EMIT supportedSchemesChanged(); |
| 1532 | } |
| 1533 | |
| 1534 | QStringList KFilePlacesModel::supportedSchemes() const |
| 1535 | { |
| 1536 | return d->supportedSchemes; |
| 1537 | } |
| 1538 | |
| 1539 | namespace |
| 1540 | { |
| 1541 | QString partitionManagerPath() |
| 1542 | { |
| 1543 | static const QString path = QStandardPaths::findExecutable(QStringLiteral("partitionmanager" )); |
| 1544 | return path; |
| 1545 | } |
| 1546 | } // namespace |
| 1547 | |
| 1548 | QAction *KFilePlacesModel::partitionActionForIndex(const QModelIndex &index) const |
| 1549 | { |
| 1550 | const auto device = deviceForIndex(index); |
| 1551 | if (!device.is<Solid::Block>()) { |
| 1552 | return nullptr; |
| 1553 | } |
| 1554 | |
| 1555 | // Not using kservice to find partitionmanager because we need to manually invoke it so we can pass the --device argument. |
| 1556 | if (partitionManagerPath().isEmpty()) { |
| 1557 | return nullptr; |
| 1558 | } |
| 1559 | |
| 1560 | auto action = |
| 1561 | new QAction(QIcon::fromTheme(QStringLiteral("partitionmanager" )), i18nc("@action:inmenu" , "Reformat or Edit with Partition Manager" ), nullptr); |
| 1562 | connect(sender: action, signal: &QAction::triggered, context: this, slot: [device] { |
| 1563 | const auto block = device.as<Solid::Block>(); |
| 1564 | auto job = new KIO::CommandLauncherJob(partitionManagerPath(), {QStringLiteral("--device" ), block->device()}); |
| 1565 | job->start(); |
| 1566 | }); |
| 1567 | return action; |
| 1568 | } |
| 1569 | |
| 1570 | #include "moc_kfileplacesmodel.cpp" |
| 1571 | |