File indexing completed on 2024-10-13 03:38:18
0001 /* 0002 This file is part of the KDE project 0003 SPDX-FileCopyrightText: 2007 Kevin Ottens <ervin@kde.org> 0004 SPDX-FileCopyrightText: 2007 David Faure <faure@kde.org> 0005 SPDX-FileCopyrightText: 2023 Harald Sitter <sitter@kde.org> 0006 0007 SPDX-License-Identifier: LGPL-2.0-only 0008 */ 0009 0010 #include "kfileplacesmodel.h" 0011 #include "kfileplacesitem_p.h" 0012 #include "kfileplacesmodel_p.h" 0013 0014 #include <KCoreDirLister> 0015 #include <KLazyLocalizedString> 0016 #include <KListOpenFilesJob> 0017 #include <KLocalizedString> 0018 #include <commandlauncherjob.h> 0019 #include <kfileitem.h> 0020 #include <kio/statjob.h> 0021 #include <kprotocolinfo.h> 0022 0023 #include <KBookmarkManager> 0024 #include <KConfig> 0025 #include <KConfigGroup> 0026 #include <KUrlMimeData> 0027 0028 #include <solid/block.h> 0029 #include <solid/devicenotifier.h> 0030 #include <solid/opticaldisc.h> 0031 #include <solid/opticaldrive.h> 0032 #include <solid/portablemediaplayer.h> 0033 #include <solid/predicate.h> 0034 #include <solid/storageaccess.h> 0035 #include <solid/storagedrive.h> 0036 #include <solid/storagevolume.h> 0037 0038 #include <QAction> 0039 #include <QCoreApplication> 0040 #include <QDebug> 0041 #include <QDir> 0042 #include <QFile> 0043 #include <QMimeData> 0044 #include <QMimeDatabase> 0045 #include <QStandardPaths> 0046 #include <QTimer> 0047 0048 namespace 0049 { 0050 QString stateNameForGroupType(KFilePlacesModel::GroupType type) 0051 { 0052 switch (type) { 0053 case KFilePlacesModel::PlacesType: 0054 return QStringLiteral("GroupState-Places-IsHidden"); 0055 case KFilePlacesModel::RemoteType: 0056 return QStringLiteral("GroupState-Remote-IsHidden"); 0057 case KFilePlacesModel::RecentlySavedType: 0058 return QStringLiteral("GroupState-RecentlySaved-IsHidden"); 0059 case KFilePlacesModel::SearchForType: 0060 return QStringLiteral("GroupState-SearchFor-IsHidden"); 0061 case KFilePlacesModel::DevicesType: 0062 return QStringLiteral("GroupState-Devices-IsHidden"); 0063 case KFilePlacesModel::RemovableDevicesType: 0064 return QStringLiteral("GroupState-RemovableDevices-IsHidden"); 0065 case KFilePlacesModel::TagsType: 0066 return QStringLiteral("GroupState-Tags-IsHidden"); 0067 default: 0068 Q_UNREACHABLE(); 0069 } 0070 } 0071 0072 static bool isFileIndexingEnabled() 0073 { 0074 KConfig config(QStringLiteral("baloofilerc")); 0075 KConfigGroup basicSettings = config.group(QStringLiteral("Basic Settings")); 0076 return basicSettings.readEntry("Indexing-Enabled", true); 0077 } 0078 0079 static QString timelineDateString(int year, int month, int day = 0) 0080 { 0081 const QString dateFormat = QStringLiteral("%1-%2"); 0082 0083 QString date = dateFormat.arg(year).arg(month, 2, 10, QLatin1Char('0')); 0084 if (day > 0) { 0085 date += QStringLiteral("-%1").arg(day, 2, 10, QLatin1Char('0')); 0086 } 0087 return date; 0088 } 0089 0090 static QUrl createTimelineUrl(const QUrl &url) 0091 { 0092 // based on dolphin urls 0093 const QString timelinePrefix = QLatin1String("timeline:") + QLatin1Char('/'); 0094 QUrl timelineUrl; 0095 0096 const QString path = url.toDisplayString(QUrl::PreferLocalFile); 0097 if (path.endsWith(QLatin1String("/yesterday"))) { 0098 const QDate date = QDate::currentDate().addDays(-1); 0099 const int year = date.year(); 0100 const int month = date.month(); 0101 const int day = date.day(); 0102 0103 timelineUrl = QUrl(timelinePrefix + timelineDateString(year, month) + QLatin1Char('/') + timelineDateString(year, month, day)); 0104 } else if (path.endsWith(QLatin1String("/thismonth"))) { 0105 const QDate date = QDate::currentDate(); 0106 timelineUrl = QUrl(timelinePrefix + timelineDateString(date.year(), date.month())); 0107 } else if (path.endsWith(QLatin1String("/lastmonth"))) { 0108 const QDate date = QDate::currentDate().addMonths(-1); 0109 timelineUrl = QUrl(timelinePrefix + timelineDateString(date.year(), date.month())); 0110 } else { 0111 Q_ASSERT(path.endsWith(QLatin1String("/today"))); 0112 timelineUrl = url; 0113 } 0114 0115 return timelineUrl; 0116 } 0117 0118 static QUrl createSearchUrl(const QUrl &url) 0119 { 0120 QUrl searchUrl = url; 0121 0122 const QString path = url.toDisplayString(QUrl::PreferLocalFile); 0123 0124 const QStringList validSearchPaths = {QStringLiteral("/documents"), QStringLiteral("/images"), QStringLiteral("/audio"), QStringLiteral("/videos")}; 0125 0126 for (const QString &validPath : validSearchPaths) { 0127 if (path.endsWith(validPath)) { 0128 searchUrl.setScheme(QStringLiteral("baloosearch")); 0129 return searchUrl; 0130 } 0131 } 0132 0133 qWarning() << "Invalid search url:" << url; 0134 0135 return searchUrl; 0136 } 0137 } 0138 0139 KFilePlacesModelPrivate::KFilePlacesModelPrivate(KFilePlacesModel *qq) 0140 : q(qq) 0141 , fileIndexingEnabled(isFileIndexingEnabled()) 0142 , tagsLister(new KCoreDirLister(q)) 0143 { 0144 if (KProtocolInfo::isKnownProtocol(QStringLiteral("tags"))) { 0145 QObject::connect(tagsLister, &KCoreDirLister::itemsAdded, q, [this](const QUrl &, const KFileItemList &items) { 0146 if (tags.isEmpty()) { 0147 QList<QUrl> existingBookmarks; 0148 0149 KBookmarkGroup root = bookmarkManager->root(); 0150 KBookmark bookmark = root.first(); 0151 0152 while (!bookmark.isNull()) { 0153 existingBookmarks.append(bookmark.url()); 0154 bookmark = root.next(bookmark); 0155 } 0156 0157 if (!existingBookmarks.contains(QUrl(tagsUrlBase))) { 0158 KBookmark alltags = KFilePlacesItem::createSystemBookmark(bookmarkManager, 0159 kli18nc("KFile System Bookmarks", "All tags").untranslatedText(), 0160 QUrl(tagsUrlBase), 0161 QStringLiteral("tag")); 0162 } 0163 } 0164 0165 for (const KFileItem &item : items) { 0166 const QString name = item.name(); 0167 0168 if (!tags.contains(name)) { 0169 tags.append(name); 0170 } 0171 } 0172 reloadBookmarks(); 0173 }); 0174 0175 QObject::connect(tagsLister, &KCoreDirLister::itemsDeleted, q, [this](const KFileItemList &items) { 0176 for (const KFileItem &item : items) { 0177 tags.removeAll(item.name()); 0178 } 0179 reloadBookmarks(); 0180 }); 0181 0182 tagsLister->openUrl(QUrl(tagsUrlBase), KCoreDirLister::OpenUrlFlag::Reload); 0183 } 0184 } 0185 0186 QString KFilePlacesModelPrivate::ignoreMimeType() 0187 { 0188 return QStringLiteral("application/x-kfileplacesmodel-ignore"); 0189 } 0190 0191 QString KFilePlacesModelPrivate::internalMimeType(const KFilePlacesModel *model) 0192 { 0193 return QStringLiteral("application/x-kfileplacesmodel-") + QString::number(reinterpret_cast<qptrdiff>(model)); 0194 } 0195 0196 KBookmark KFilePlacesModel::bookmarkForUrl(const QUrl &searchUrl) const 0197 { 0198 KBookmarkGroup root = d->bookmarkManager->root(); 0199 KBookmark current = root.first(); 0200 while (!current.isNull()) { 0201 if (current.url() == searchUrl) { 0202 return current; 0203 } 0204 current = root.next(current); 0205 } 0206 return KBookmark(); 0207 } 0208 0209 static inline QString versionKey() 0210 { 0211 return QStringLiteral("kde_places_version"); 0212 } 0213 0214 KFilePlacesModel::KFilePlacesModel(QObject *parent) 0215 : QAbstractItemModel(parent) 0216 , d(new KFilePlacesModelPrivate(this)) 0217 { 0218 const QString file = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1String("/user-places.xbel"); 0219 d->bookmarkManager = new KBookmarkManager(file, this); 0220 0221 // Let's put some places in there if it's empty. 0222 KBookmarkGroup root = d->bookmarkManager->root(); 0223 0224 const auto setDefaultMetadataItemForGroup = [&root](KFilePlacesModel::GroupType type) { 0225 root.setMetaDataItem(stateNameForGroupType(type), QStringLiteral("false")); 0226 }; 0227 0228 // Increase this version number and use the following logic to handle the update process for existing installations. 0229 static const int s_currentVersion = 4; 0230 0231 const bool newFile = root.first().isNull() || !QFile::exists(file); 0232 const int fileVersion = root.metaDataItem(versionKey()).toInt(); 0233 0234 if (newFile || fileVersion < s_currentVersion) { 0235 root.setMetaDataItem(versionKey(), QString::number(s_currentVersion)); 0236 0237 const QList<QUrl> seenUrls = root.groupUrlList(); 0238 0239 /* clang-format off */ 0240 auto createSystemBookmark = 0241 [this, &seenUrls](const char *untranslatedLabel, 0242 const QUrl &url, 0243 const QString &iconName, 0244 const KBookmark &after) { 0245 if (!seenUrls.contains(url)) { 0246 return KFilePlacesItem::createSystemBookmark(d->bookmarkManager, untranslatedLabel, url, iconName, after); 0247 } 0248 return KBookmark(); 0249 }; 0250 /* clang-format on */ 0251 0252 if (fileVersion < 2) { 0253 // NOTE: The context for these kli18nc calls has to be "KFile System Bookmarks". 0254 // The real i18nc call is made later, with this context, so the two must match. 0255 createSystemBookmark(kli18nc("KFile System Bookmarks", "Home").untranslatedText(), 0256 QUrl::fromLocalFile(QDir::homePath()), 0257 QStringLiteral("user-home"), 0258 KBookmark()); 0259 0260 // Some distros may not create various standard XDG folders by default 0261 // so check for their existence before adding bookmarks for them 0262 const QString desktopFolder = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation); 0263 if (QDir(desktopFolder).exists()) { 0264 createSystemBookmark(kli18nc("KFile System Bookmarks", "Desktop").untranslatedText(), 0265 QUrl::fromLocalFile(desktopFolder), 0266 QStringLiteral("user-desktop"), 0267 KBookmark()); 0268 } 0269 const QString documentsFolder = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation); 0270 if (QDir(documentsFolder).exists()) { 0271 createSystemBookmark(kli18nc("KFile System Bookmarks", "Documents").untranslatedText(), 0272 QUrl::fromLocalFile(documentsFolder), 0273 QStringLiteral("folder-documents"), 0274 KBookmark()); 0275 } 0276 const QString downloadFolder = QStandardPaths::writableLocation(QStandardPaths::DownloadLocation); 0277 if (QDir(downloadFolder).exists()) { 0278 createSystemBookmark(kli18nc("KFile System Bookmarks", "Downloads").untranslatedText(), 0279 QUrl::fromLocalFile(downloadFolder), 0280 QStringLiteral("folder-downloads"), 0281 KBookmark()); 0282 } 0283 createSystemBookmark(kli18nc("KFile System Bookmarks", "Network").untranslatedText(), 0284 QUrl(QStringLiteral("remote:/")), 0285 QStringLiteral("folder-network"), 0286 KBookmark()); 0287 0288 createSystemBookmark(kli18nc("KFile System Bookmarks", "Trash").untranslatedText(), 0289 QUrl(QStringLiteral("trash:/")), 0290 QStringLiteral("user-trash"), 0291 KBookmark()); 0292 } 0293 0294 if (!newFile && fileVersion < 3) { 0295 KBookmarkGroup rootGroup = d->bookmarkManager->root(); 0296 KBookmark bItem = rootGroup.first(); 0297 while (!bItem.isNull()) { 0298 KBookmark nextbItem = rootGroup.next(bItem); 0299 const bool isSystemItem = bItem.metaDataItem(QStringLiteral("isSystemItem")) == QLatin1String("true"); 0300 if (isSystemItem) { 0301 const QString text = bItem.fullText(); 0302 // Because of b8a4c2223453932202397d812a0c6b30c6186c70 we need to find the system bookmark named Audio Files 0303 // and rename it to Audio, otherwise users are getting untranslated strings 0304 if (text == QLatin1String("Audio Files")) { 0305 bItem.setFullText(QStringLiteral("Audio")); 0306 } else if (text == QLatin1String("Today")) { 0307 // Because of 19feef732085b444515da3f6c66f3352bbcb1824 we need to find the system bookmark named Today 0308 // and rename it to Modified Today, otherwise users are getting untranslated strings 0309 bItem.setFullText(QStringLiteral("Modified Today")); 0310 } else if (text == QLatin1String("Yesterday")) { 0311 // Because of 19feef732085b444515da3f6c66f3352bbcb1824 we need to find the system bookmark named Yesterday 0312 // and rename it to Modified Yesterday, otherwise users are getting untranslated strings 0313 bItem.setFullText(QStringLiteral("Modified Yesterday")); 0314 } else if (text == QLatin1String("This Month")) { 0315 // Because of 7e1d2fb84546506c91684dd222c2485f0783848f we need to find the system bookmark named This Month 0316 // and remove it, otherwise users are getting untranslated strings 0317 rootGroup.deleteBookmark(bItem); 0318 } else if (text == QLatin1String("Last Month")) { 0319 // Because of 7e1d2fb84546506c91684dd222c2485f0783848f we need to find the system bookmark named Last Month 0320 // and remove it, otherwise users are getting untranslated strings 0321 rootGroup.deleteBookmark(bItem); 0322 } 0323 } 0324 0325 bItem = nextbItem; 0326 } 0327 } 0328 if (fileVersion < 4) { 0329 auto findSystemBookmark = [this](const QString &untranslatedText) { 0330 KBookmarkGroup root = d->bookmarkManager->root(); 0331 KBookmark bItem = root.first(); 0332 while (!bItem.isNull()) { 0333 const bool isSystemItem = bItem.metaDataItem(QStringLiteral("isSystemItem")) == QLatin1String("true"); 0334 if (isSystemItem && bItem.fullText() == untranslatedText) { 0335 return bItem; 0336 } 0337 bItem = root.next(bItem); 0338 } 0339 return KBookmark(); 0340 }; 0341 // This variable is used to insert the new bookmarks at the correct place starting after the "Downloads" 0342 // bookmark. When the user already has some of the bookmarks set up manually, the createSystemBookmark() 0343 // function returns an empty KBookmark so the following entries will be added at the end of the bookmark 0344 // section to not mess with the users setup. 0345 KBookmark after = findSystemBookmark(QLatin1String("Downloads")); 0346 0347 const QString musicFolder = QStandardPaths::writableLocation(QStandardPaths::MusicLocation); 0348 if (QDir(musicFolder).exists()) { 0349 after = createSystemBookmark(kli18nc("KFile System Bookmarks", "Music").untranslatedText(), 0350 QUrl::fromLocalFile(musicFolder), 0351 QStringLiteral("folder-music"), 0352 after); 0353 } 0354 const QString pictureFolder = QStandardPaths::writableLocation(QStandardPaths::PicturesLocation); 0355 if (QDir(pictureFolder).exists()) { 0356 after = createSystemBookmark(kli18nc("KFile System Bookmarks", "Pictures").untranslatedText(), 0357 QUrl::fromLocalFile(pictureFolder), 0358 QStringLiteral("folder-pictures"), 0359 after); 0360 } 0361 // Choosing the name "Videos" instead of "Movies", since that is how the folder 0362 // is called normally on Linux: https://cgit.freedesktop.org/xdg/xdg-user-dirs/tree/user-dirs.defaults 0363 const QString videoFolder = QStandardPaths::writableLocation(QStandardPaths::MoviesLocation); 0364 if (QDir(videoFolder).exists()) { 0365 after = createSystemBookmark(kli18nc("KFile System Bookmarks", "Videos").untranslatedText(), 0366 QUrl::fromLocalFile(videoFolder), 0367 QStringLiteral("folder-videos"), 0368 after); 0369 } 0370 } 0371 0372 if (newFile) { 0373 setDefaultMetadataItemForGroup(PlacesType); 0374 setDefaultMetadataItemForGroup(RemoteType); 0375 setDefaultMetadataItemForGroup(DevicesType); 0376 setDefaultMetadataItemForGroup(RemovableDevicesType); 0377 setDefaultMetadataItemForGroup(TagsType); 0378 } 0379 0380 // Force bookmarks to be saved. If on open/save dialog and the bookmarks are not saved, QFile::exists 0381 // will always return false, which opening/closing all the time the open/save dialog would cause the 0382 // bookmarks to be added once each time, having lots of times each bookmark. (ereslibre) 0383 d->bookmarkManager->saveAs(file); 0384 } 0385 0386 // Add a Recently Used entry if available (it comes from kio-extras) 0387 if (qEnvironmentVariableIsSet("KDE_FULL_SESSION") && KProtocolInfo::isKnownProtocol(QStringLiteral("recentlyused")) 0388 && root.metaDataItem(QStringLiteral("withRecentlyUsed")) != QLatin1String("true")) { 0389 root.setMetaDataItem(QStringLiteral("withRecentlyUsed"), QStringLiteral("true")); 0390 0391 KBookmark recentFilesBookmark = KFilePlacesItem::createSystemBookmark(d->bookmarkManager, 0392 kli18nc("KFile System Bookmarks", "Recent Files").untranslatedText(), 0393 QUrl(QStringLiteral("recentlyused:/files")), 0394 QStringLiteral("document-open-recent")); 0395 0396 KBookmark recentDirectoriesBookmark = KFilePlacesItem::createSystemBookmark(d->bookmarkManager, 0397 kli18nc("KFile System Bookmarks", "Recent Locations").untranslatedText(), 0398 QUrl(QStringLiteral("recentlyused:/locations")), 0399 QStringLiteral("folder-open-recent")); 0400 0401 setDefaultMetadataItemForGroup(RecentlySavedType); 0402 0403 // Move The recently used bookmarks below the trash, making it the first element in the Recent group 0404 KBookmark trashBookmark = bookmarkForUrl(QUrl(QStringLiteral("trash:/"))); 0405 if (!trashBookmark.isNull()) { 0406 root.moveBookmark(recentFilesBookmark, trashBookmark); 0407 root.moveBookmark(recentDirectoriesBookmark, recentFilesBookmark); 0408 } 0409 0410 d->bookmarkManager->save(); 0411 } 0412 0413 // if baloo is enabled, add new urls even if the bookmark file is not empty 0414 if (d->fileIndexingEnabled && root.metaDataItem(QStringLiteral("withBaloo")) != QLatin1String("true")) { 0415 root.setMetaDataItem(QStringLiteral("withBaloo"), QStringLiteral("true")); 0416 0417 // don't add by default "Modified Today" and "Modified Yesterday" when recentlyused:/ is present 0418 if (root.metaDataItem(QStringLiteral("withRecentlyUsed")) != QLatin1String("true")) { 0419 KFilePlacesItem::createSystemBookmark(d->bookmarkManager, 0420 kli18nc("KFile System Bookmarks", "Modified Today").untranslatedText(), 0421 QUrl(QStringLiteral("timeline:/today")), 0422 QStringLiteral("go-jump-today")); 0423 KFilePlacesItem::createSystemBookmark(d->bookmarkManager, 0424 kli18nc("KFile System Bookmarks", "Modified Yesterday").untranslatedText(), 0425 QUrl(QStringLiteral("timeline:/yesterday")), 0426 QStringLiteral("view-calendar-day")); 0427 } 0428 0429 setDefaultMetadataItemForGroup(SearchForType); 0430 setDefaultMetadataItemForGroup(RecentlySavedType); 0431 0432 d->bookmarkManager->save(); 0433 } 0434 0435 QString predicate( 0436 QString::fromLatin1("[[[[ StorageVolume.ignored == false AND [ StorageVolume.usage == 'FileSystem' OR StorageVolume.usage == 'Encrypted' ]]" 0437 " OR " 0438 "[ IS StorageAccess AND StorageDrive.driveType == 'Floppy' ]]" 0439 " OR " 0440 "OpticalDisc.availableContent & 'Audio' ]" 0441 " OR " 0442 "StorageAccess.ignored == false ]")); 0443 0444 if (KProtocolInfo::isKnownProtocol(QStringLiteral("mtp"))) { 0445 predicate = QLatin1Char('[') + predicate + QLatin1String(" OR PortableMediaPlayer.supportedProtocols == 'mtp']"); 0446 } 0447 if (KProtocolInfo::isKnownProtocol(QStringLiteral("afc"))) { 0448 predicate = QLatin1Char('[') + predicate + QLatin1String(" OR PortableMediaPlayer.supportedProtocols == 'afc']"); 0449 } 0450 0451 d->predicate = Solid::Predicate::fromString(predicate); 0452 0453 Q_ASSERT(d->predicate.isValid()); 0454 0455 connect(d->bookmarkManager, &KBookmarkManager::changed, this, [this]() { 0456 d->reloadBookmarks(); 0457 }); 0458 0459 d->reloadBookmarks(); 0460 QTimer::singleShot(0, this, [this]() { 0461 d->initDeviceList(); 0462 }); 0463 } 0464 0465 KFilePlacesModel::~KFilePlacesModel() = default; 0466 0467 QUrl KFilePlacesModel::url(const QModelIndex &index) const 0468 { 0469 return data(index, UrlRole).toUrl(); 0470 } 0471 0472 bool KFilePlacesModel::setupNeeded(const QModelIndex &index) const 0473 { 0474 return data(index, SetupNeededRole).toBool(); 0475 } 0476 0477 QIcon KFilePlacesModel::icon(const QModelIndex &index) const 0478 { 0479 return data(index, Qt::DecorationRole).value<QIcon>(); 0480 } 0481 0482 QString KFilePlacesModel::text(const QModelIndex &index) const 0483 { 0484 return data(index, Qt::DisplayRole).toString(); 0485 } 0486 0487 bool KFilePlacesModel::isHidden(const QModelIndex &index) const 0488 { 0489 // Note: we do not want to show an index if its parent is hidden 0490 return data(index, HiddenRole).toBool() || isGroupHidden(index); 0491 } 0492 0493 bool KFilePlacesModel::isGroupHidden(const GroupType type) const 0494 { 0495 const QString hidden = d->bookmarkManager->root().metaDataItem(stateNameForGroupType(type)); 0496 return hidden == QLatin1String("true"); 0497 } 0498 0499 bool KFilePlacesModel::isGroupHidden(const QModelIndex &index) const 0500 { 0501 if (!index.isValid()) { 0502 return false; 0503 } 0504 0505 KFilePlacesItem *item = static_cast<KFilePlacesItem *>(index.internalPointer()); 0506 return isGroupHidden(item->groupType()); 0507 } 0508 0509 bool KFilePlacesModel::isDevice(const QModelIndex &index) const 0510 { 0511 if (!index.isValid()) { 0512 return false; 0513 } 0514 0515 KFilePlacesItem *item = static_cast<KFilePlacesItem *>(index.internalPointer()); 0516 0517 return item->isDevice(); 0518 } 0519 0520 bool KFilePlacesModel::isTeardownAllowed(const QModelIndex &index) const 0521 { 0522 if (!index.isValid()) { 0523 return false; 0524 } 0525 0526 KFilePlacesItem *item = static_cast<KFilePlacesItem *>(index.internalPointer()); 0527 return item->isTeardownAllowed(); 0528 } 0529 0530 bool KFilePlacesModel::isEjectAllowed(const QModelIndex &index) const 0531 { 0532 if (!index.isValid()) { 0533 return false; 0534 } 0535 0536 KFilePlacesItem *item = static_cast<KFilePlacesItem *>(index.internalPointer()); 0537 return item->isEjectAllowed(); 0538 } 0539 0540 bool KFilePlacesModel::isTeardownOverlayRecommended(const QModelIndex &index) const 0541 { 0542 if (!index.isValid()) { 0543 return false; 0544 } 0545 0546 KFilePlacesItem *item = static_cast<KFilePlacesItem *>(index.internalPointer()); 0547 return item->isTeardownOverlayRecommended(); 0548 } 0549 0550 KFilePlacesModel::DeviceAccessibility KFilePlacesModel::deviceAccessibility(const QModelIndex &index) const 0551 { 0552 if (!index.isValid()) { 0553 return KFilePlacesModel::Accessible; 0554 } 0555 0556 KFilePlacesItem *item = static_cast<KFilePlacesItem *>(index.internalPointer()); 0557 return item->deviceAccessibility(); 0558 } 0559 0560 Solid::Device KFilePlacesModel::deviceForIndex(const QModelIndex &index) const 0561 { 0562 if (!index.isValid()) { 0563 return Solid::Device(); 0564 } 0565 0566 KFilePlacesItem *item = static_cast<KFilePlacesItem *>(index.internalPointer()); 0567 0568 if (item->isDevice()) { 0569 return item->device(); 0570 } else { 0571 return Solid::Device(); 0572 } 0573 } 0574 0575 KBookmark KFilePlacesModel::bookmarkForIndex(const QModelIndex &index) const 0576 { 0577 if (!index.isValid()) { 0578 return KBookmark(); 0579 } 0580 0581 KFilePlacesItem *item = static_cast<KFilePlacesItem *>(index.internalPointer()); 0582 return item->bookmark(); 0583 } 0584 0585 KFilePlacesModel::GroupType KFilePlacesModel::groupType(const QModelIndex &index) const 0586 { 0587 if (!index.isValid()) { 0588 return UnknownType; 0589 } 0590 0591 KFilePlacesItem *item = static_cast<KFilePlacesItem *>(index.internalPointer()); 0592 return item->groupType(); 0593 } 0594 0595 QModelIndexList KFilePlacesModel::groupIndexes(const KFilePlacesModel::GroupType type) const 0596 { 0597 if (type == UnknownType) { 0598 return QModelIndexList(); 0599 } 0600 0601 QModelIndexList indexes; 0602 const int rows = rowCount(); 0603 for (int row = 0; row < rows; ++row) { 0604 const QModelIndex current = index(row, 0); 0605 if (groupType(current) == type) { 0606 indexes << current; 0607 } 0608 } 0609 0610 return indexes; 0611 } 0612 0613 QVariant KFilePlacesModel::data(const QModelIndex &index, int role) const 0614 { 0615 if (!index.isValid()) { 0616 return QVariant(); 0617 } 0618 0619 KFilePlacesItem *item = static_cast<KFilePlacesItem *>(index.internalPointer()); 0620 if (role == KFilePlacesModel::GroupHiddenRole) { 0621 return isGroupHidden(item->groupType()); 0622 } else { 0623 return item->data(role); 0624 } 0625 } 0626 0627 QModelIndex KFilePlacesModel::index(int row, int column, const QModelIndex &parent) const 0628 { 0629 if (row < 0 || column != 0 || row >= d->items.size()) { 0630 return QModelIndex(); 0631 } 0632 0633 if (parent.isValid()) { 0634 return QModelIndex(); 0635 } 0636 0637 return createIndex(row, column, d->items.at(row)); 0638 } 0639 0640 QModelIndex KFilePlacesModel::parent(const QModelIndex &child) const 0641 { 0642 Q_UNUSED(child); 0643 return QModelIndex(); 0644 } 0645 0646 QHash<int, QByteArray> KFilePlacesModel::roleNames() const 0647 { 0648 auto super = QAbstractItemModel::roleNames(); 0649 0650 super[UrlRole] = "url"; 0651 super[HiddenRole] = "isHidden"; 0652 super[SetupNeededRole] = "isSetupNeeded"; 0653 super[FixedDeviceRole] = "isFixedDevice"; 0654 super[CapacityBarRecommendedRole] = "isCapacityBarRecommended"; 0655 super[GroupRole] = "group"; 0656 super[IconNameRole] = "iconName"; 0657 super[GroupHiddenRole] = "isGroupHidden"; 0658 super[TeardownAllowedRole] = "isTeardownAllowed"; 0659 super[EjectAllowedRole] = "isEjectAllowed"; 0660 super[TeardownOverlayRecommendedRole] = "isTeardownOverlayRecommended"; 0661 super[DeviceAccessibilityRole] = "deviceAccessibility"; 0662 0663 return super; 0664 } 0665 0666 int KFilePlacesModel::rowCount(const QModelIndex &parent) const 0667 { 0668 if (parent.isValid()) { 0669 return 0; 0670 } else { 0671 return d->items.size(); 0672 } 0673 } 0674 0675 int KFilePlacesModel::columnCount(const QModelIndex &parent) const 0676 { 0677 Q_UNUSED(parent) 0678 // We only know 1 piece of information for a particular entry 0679 return 1; 0680 } 0681 0682 QModelIndex KFilePlacesModel::closestItem(const QUrl &url) const 0683 { 0684 int foundRow = -1; 0685 int maxLength = 0; 0686 0687 // Search the item which is equal to the URL or at least is a parent URL. 0688 // If there are more than one possible item URL candidates, choose the item 0689 // which covers the bigger range of the URL. 0690 for (int row = 0; row < d->items.size(); ++row) { 0691 KFilePlacesItem *item = d->items[row]; 0692 0693 if (item->isHidden() || isGroupHidden(item->groupType())) { 0694 continue; 0695 } 0696 0697 const QUrl itemUrl = convertedUrl(item->data(UrlRole).toUrl()); 0698 0699 if (itemUrl.matches(url, QUrl::StripTrailingSlash) 0700 || (itemUrl.isParentOf(url) && itemUrl.query() == url.query() && itemUrl.fragment() == url.fragment())) { 0701 const int length = itemUrl.toString().length(); 0702 if (length > maxLength) { 0703 foundRow = row; 0704 maxLength = length; 0705 } 0706 } 0707 } 0708 0709 if (foundRow == -1) { 0710 return QModelIndex(); 0711 } else { 0712 return createIndex(foundRow, 0, d->items[foundRow]); 0713 } 0714 } 0715 0716 void KFilePlacesModelPrivate::initDeviceList() 0717 { 0718 Solid::DeviceNotifier *notifier = Solid::DeviceNotifier::instance(); 0719 0720 QObject::connect(notifier, &Solid::DeviceNotifier::deviceAdded, q, [this](const QString &device) { 0721 deviceAdded(device); 0722 }); 0723 QObject::connect(notifier, &Solid::DeviceNotifier::deviceRemoved, q, [this](const QString &device) { 0724 deviceRemoved(device); 0725 }); 0726 0727 availableDevices = Solid::Device::listFromQuery(predicate); 0728 0729 reloadBookmarks(); 0730 } 0731 0732 void KFilePlacesModelPrivate::deviceAdded(const QString &udi) 0733 { 0734 Solid::Device d(udi); 0735 0736 if (predicate.matches(d)) { 0737 availableDevices << d; 0738 reloadBookmarks(); 0739 } 0740 } 0741 0742 void KFilePlacesModelPrivate::deviceRemoved(const QString &udi) 0743 { 0744 auto it = std::find_if(availableDevices.begin(), availableDevices.end(), [udi](const Solid::Device &device) { 0745 return device.udi() == udi; 0746 }); 0747 if (it != availableDevices.end()) { 0748 availableDevices.erase(it); 0749 reloadBookmarks(); 0750 } 0751 } 0752 0753 void KFilePlacesModelPrivate::itemChanged(const QString &id, const QList<int> &roles) 0754 { 0755 for (int row = 0; row < items.size(); ++row) { 0756 if (items.at(row)->id() == id) { 0757 QModelIndex index = q->index(row, 0); 0758 Q_EMIT q->dataChanged(index, index, roles); 0759 } 0760 } 0761 } 0762 0763 void KFilePlacesModelPrivate::reloadBookmarks() 0764 { 0765 QList<KFilePlacesItem *> currentItems = loadBookmarkList(); 0766 0767 QList<KFilePlacesItem *>::Iterator it_i = items.begin(); 0768 QList<KFilePlacesItem *>::Iterator it_c = currentItems.begin(); 0769 0770 QList<KFilePlacesItem *>::Iterator end_i = items.end(); 0771 QList<KFilePlacesItem *>::Iterator end_c = currentItems.end(); 0772 0773 while (it_i != end_i || it_c != end_c) { 0774 if (it_i == end_i && it_c != end_c) { 0775 int row = items.count(); 0776 0777 q->beginInsertRows(QModelIndex(), row, row); 0778 it_i = items.insert(it_i, *it_c); 0779 ++it_i; 0780 it_c = currentItems.erase(it_c); 0781 0782 end_i = items.end(); 0783 end_c = currentItems.end(); 0784 q->endInsertRows(); 0785 0786 } else if (it_i != end_i && it_c == end_c) { 0787 int row = items.indexOf(*it_i); 0788 0789 q->beginRemoveRows(QModelIndex(), row, row); 0790 delete *it_i; 0791 it_i = items.erase(it_i); 0792 0793 end_i = items.end(); 0794 end_c = currentItems.end(); 0795 q->endRemoveRows(); 0796 0797 } else if ((*it_i)->id() == (*it_c)->id()) { 0798 bool shouldEmit = !((*it_i)->bookmark() == (*it_c)->bookmark()); 0799 (*it_i)->setBookmark((*it_c)->bookmark()); 0800 if (shouldEmit) { 0801 int row = items.indexOf(*it_i); 0802 QModelIndex idx = q->index(row, 0); 0803 Q_EMIT q->dataChanged(idx, idx); 0804 } 0805 ++it_i; 0806 ++it_c; 0807 } else { 0808 int row = items.indexOf(*it_i); 0809 0810 if (it_i + 1 != end_i && (*(it_i + 1))->id() == (*it_c)->id()) { // if the next one matches, it's a remove 0811 q->beginRemoveRows(QModelIndex(), row, row); 0812 delete *it_i; 0813 it_i = items.erase(it_i); 0814 0815 end_i = items.end(); 0816 end_c = currentItems.end(); 0817 q->endRemoveRows(); 0818 } else { 0819 q->beginInsertRows(QModelIndex(), row, row); 0820 it_i = items.insert(it_i, *it_c); 0821 ++it_i; 0822 it_c = currentItems.erase(it_c); 0823 0824 end_i = items.end(); 0825 end_c = currentItems.end(); 0826 q->endInsertRows(); 0827 } 0828 } 0829 } 0830 0831 qDeleteAll(currentItems); 0832 currentItems.clear(); 0833 0834 Q_EMIT q->reloaded(); 0835 } 0836 0837 bool KFilePlacesModelPrivate::isBalooUrl(const QUrl &url) const 0838 { 0839 const QString scheme = url.scheme(); 0840 return ((scheme == QLatin1String("timeline")) || (scheme == QLatin1String("search"))); 0841 } 0842 0843 QList<KFilePlacesItem *> KFilePlacesModelPrivate::loadBookmarkList() 0844 { 0845 QList<KFilePlacesItem *> items; 0846 0847 KBookmarkGroup root = bookmarkManager->root(); 0848 KBookmark bookmark = root.first(); 0849 QList<Solid::Device> devices{availableDevices}; 0850 QList<QString> tagsList = tags; 0851 0852 while (!bookmark.isNull()) { 0853 KFilePlacesItem *item = nullptr; 0854 0855 if (const QString udi = bookmark.metaDataItem(QStringLiteral("UDI")); !udi.isEmpty()) { 0856 const QString uuid = bookmark.metaDataItem(QStringLiteral("uuid")); 0857 auto it = std::find_if(devices.begin(), devices.end(), [udi, uuid](const Solid::Device &device) { 0858 if (!uuid.isEmpty()) { 0859 auto storageVolume = device.as<Solid::StorageVolume>(); 0860 if (storageVolume && !storageVolume->uuid().isEmpty()) { 0861 return storageVolume->uuid() == uuid; 0862 } 0863 } 0864 0865 return device.udi() == udi; 0866 }); 0867 if (it != devices.end()) { 0868 item = new KFilePlacesItem(bookmarkManager, bookmark.address(), it->udi(), q); 0869 if (!item->hasSupportedScheme(supportedSchemes)) { 0870 delete item; 0871 item = nullptr; 0872 } 0873 devices.erase(it); 0874 } 0875 } else if (const QString tag = bookmark.metaDataItem(QStringLiteral("tag")); !tag.isEmpty()) { 0876 auto it = std::find(tagsList.begin(), tagsList.end(), tag); 0877 if (it != tagsList.end()) { 0878 tagsList.erase(it); 0879 item = new KFilePlacesItem(bookmarkManager, bookmark.address(), QString(), q); 0880 } 0881 } else if (const QUrl url = bookmark.url(); url.isValid()) { 0882 QString appName = bookmark.metaDataItem(QStringLiteral("OnlyInApp")); 0883 bool allowedHere = appName.isEmpty() || appName == QCoreApplication::instance()->applicationName(); 0884 bool isSupportedUrl = isBalooUrl(url) ? fileIndexingEnabled : true; 0885 bool isSupportedScheme = supportedSchemes.isEmpty() || supportedSchemes.contains(url.scheme()); 0886 0887 if (isSupportedScheme && isSupportedUrl && allowedHere) { 0888 // TODO: Update bookmark internal element 0889 item = new KFilePlacesItem(bookmarkManager, bookmark.address(), QString(), q); 0890 } 0891 } 0892 0893 if (item) { 0894 QObject::connect(item, &KFilePlacesItem::itemChanged, q, [this](const QString &id, const QList<int> &roles) { 0895 itemChanged(id, roles); 0896 }); 0897 items << item; 0898 } 0899 0900 bookmark = root.next(bookmark); 0901 } 0902 0903 // Add bookmarks for the remaining devices, they were previously unknown 0904 for (const Solid::Device &device : std::as_const(devices)) { 0905 bookmark = KFilePlacesItem::createDeviceBookmark(bookmarkManager, device); 0906 if (!bookmark.isNull()) { 0907 KFilePlacesItem *item = new KFilePlacesItem(bookmarkManager, bookmark.address(), device.udi(), q); 0908 QObject::connect(item, &KFilePlacesItem::itemChanged, q, [this](const QString &id, const QList<int> &roles) { 0909 itemChanged(id, roles); 0910 }); 0911 // TODO: Update bookmark internal element 0912 items << item; 0913 } 0914 } 0915 0916 for (const QString &tag : tagsList) { 0917 bookmark = KFilePlacesItem::createTagBookmark(bookmarkManager, tag); 0918 if (!bookmark.isNull()) { 0919 KFilePlacesItem *item = new KFilePlacesItem(bookmarkManager, bookmark.address(), tag, q); 0920 QObject::connect(item, &KFilePlacesItem::itemChanged, q, [this](const QString &id, const QList<int> &roles) { 0921 itemChanged(id, roles); 0922 }); 0923 items << item; 0924 } 0925 } 0926 0927 // return a sorted list based on groups 0928 std::stable_sort(items.begin(), items.end(), [](KFilePlacesItem *itemA, KFilePlacesItem *itemB) { 0929 return (itemA->groupType() < itemB->groupType()); 0930 }); 0931 0932 return items; 0933 } 0934 0935 int KFilePlacesModelPrivate::findNearestPosition(int source, int target) 0936 { 0937 const KFilePlacesItem *item = items.at(source); 0938 const KFilePlacesModel::GroupType groupType = item->groupType(); 0939 int newTarget = qMin(target, items.count() - 1); 0940 0941 // moving inside the same group is ok 0942 if ((items.at(newTarget)->groupType() == groupType)) { 0943 return target; 0944 } 0945 0946 if (target > source) { // moving down, move it to the end of the group 0947 int groupFooter = source; 0948 while (items.at(groupFooter)->groupType() == groupType) { 0949 groupFooter++; 0950 // end of the list move it there 0951 if (groupFooter == items.count()) { 0952 break; 0953 } 0954 } 0955 target = groupFooter; 0956 } else { // moving up, move it to beginning of the group 0957 int groupHead = source; 0958 while (items.at(groupHead)->groupType() == groupType) { 0959 groupHead--; 0960 // beginning of the list move it there 0961 if (groupHead == 0) { 0962 break; 0963 } 0964 } 0965 target = groupHead; 0966 } 0967 return target; 0968 } 0969 0970 void KFilePlacesModelPrivate::reloadAndSignal() 0971 { 0972 bookmarkManager->emitChanged(bookmarkManager->root()); // ... we'll get relisted anyway 0973 } 0974 0975 Qt::DropActions KFilePlacesModel::supportedDropActions() const 0976 { 0977 return Qt::ActionMask; 0978 } 0979 0980 Qt::ItemFlags KFilePlacesModel::flags(const QModelIndex &index) const 0981 { 0982 Qt::ItemFlags res; 0983 0984 if (index.isValid()) { 0985 res |= Qt::ItemIsDragEnabled | Qt::ItemIsSelectable | Qt::ItemIsEnabled; 0986 } 0987 0988 if (!index.isValid()) { 0989 res |= Qt::ItemIsDropEnabled; 0990 } 0991 0992 return res; 0993 } 0994 0995 QStringList KFilePlacesModel::mimeTypes() const 0996 { 0997 QStringList types; 0998 0999 types << KFilePlacesModelPrivate::internalMimeType(this) << QStringLiteral("text/uri-list"); 1000 1001 return types; 1002 } 1003 1004 QMimeData *KFilePlacesModel::mimeData(const QModelIndexList &indexes) const 1005 { 1006 QList<QUrl> urls; 1007 QByteArray itemData; 1008 1009 QDataStream stream(&itemData, QIODevice::WriteOnly); 1010 1011 for (const QModelIndex &index : std::as_const(indexes)) { 1012 QUrl itemUrl = url(index); 1013 if (itemUrl.isValid()) { 1014 urls << itemUrl; 1015 } 1016 stream << index.row(); 1017 } 1018 1019 QMimeData *mimeData = new QMimeData(); 1020 1021 if (!urls.isEmpty()) { 1022 mimeData->setUrls(urls); 1023 } 1024 1025 mimeData->setData(KFilePlacesModelPrivate::internalMimeType(this), itemData); 1026 1027 return mimeData; 1028 } 1029 1030 bool KFilePlacesModel::dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) 1031 { 1032 if (action == Qt::IgnoreAction) { 1033 return true; 1034 } 1035 1036 if (column > 0) { 1037 return false; 1038 } 1039 1040 if (row == -1 && parent.isValid()) { 1041 return false; // Don't allow to move an item onto another one, 1042 // too easy for the user to mess something up 1043 // If we really really want to allow copying files this way, 1044 // let's do it in the views to get the good old drop menu 1045 } 1046 1047 if (data->hasFormat(KFilePlacesModelPrivate::ignoreMimeType())) { 1048 return false; 1049 } 1050 1051 if (data->hasFormat(KFilePlacesModelPrivate::internalMimeType(this))) { 1052 // The operation is an internal move 1053 QByteArray itemData = data->data(KFilePlacesModelPrivate::internalMimeType(this)); 1054 QDataStream stream(&itemData, QIODevice::ReadOnly); 1055 int itemRow; 1056 1057 stream >> itemRow; 1058 1059 if (!movePlace(itemRow, row)) { 1060 return false; 1061 } 1062 1063 } else if (data->hasFormat(QStringLiteral("text/uri-list"))) { 1064 // The operation is an add 1065 1066 QMimeDatabase db; 1067 KBookmark afterBookmark; 1068 1069 if (row == -1) { 1070 // The dropped item is moved or added to the last position 1071 1072 KFilePlacesItem *lastItem = d->items.last(); 1073 afterBookmark = lastItem->bookmark(); 1074 1075 } else { 1076 // The dropped item is moved or added before position 'row', ie after position 'row-1' 1077 1078 if (row > 0) { 1079 KFilePlacesItem *afterItem = d->items[row - 1]; 1080 afterBookmark = afterItem->bookmark(); 1081 } 1082 } 1083 1084 const QList<QUrl> urls = KUrlMimeData::urlsFromMimeData(data); 1085 1086 KBookmarkGroup group = d->bookmarkManager->root(); 1087 1088 for (const QUrl &url : urls) { 1089 KIO::StatJob *job = KIO::stat(url, KIO::StatJob::SourceSide, KIO::StatBasic); 1090 1091 if (!job->exec()) { 1092 Q_EMIT errorMessage(i18nc("Placeholder is error message", "Could not add to the Places panel: %1", job->errorString())); 1093 continue; 1094 } 1095 1096 KFileItem item(job->statResult(), url, true /*delayed mime types*/); 1097 1098 if (!item.isDir()) { 1099 Q_EMIT errorMessage(i18n("Only folders can be added to the Places panel.")); 1100 continue; 1101 } 1102 1103 KBookmark bookmark = KFilePlacesItem::createBookmark(d->bookmarkManager, item.text(), url, KIO::iconNameForUrl(url)); 1104 1105 group.moveBookmark(bookmark, afterBookmark); 1106 afterBookmark = bookmark; 1107 } 1108 1109 } else { 1110 // Oops, shouldn't happen thanks to mimeTypes() 1111 qWarning() << ": received wrong mimedata, " << data->formats(); 1112 return false; 1113 } 1114 1115 refresh(); 1116 1117 return true; 1118 } 1119 1120 void KFilePlacesModel::refresh() const 1121 { 1122 d->reloadAndSignal(); 1123 } 1124 1125 QUrl KFilePlacesModel::convertedUrl(const QUrl &url) 1126 { 1127 QUrl newUrl = url; 1128 if (url.scheme() == QLatin1String("timeline")) { 1129 newUrl = createTimelineUrl(url); 1130 } else if (url.scheme() == QLatin1String("search")) { 1131 newUrl = createSearchUrl(url); 1132 } 1133 1134 return newUrl; 1135 } 1136 1137 void KFilePlacesModel::addPlace(const QString &text, const QUrl &url, const QString &iconName, const QString &appName) 1138 { 1139 addPlace(text, url, iconName, appName, QModelIndex()); 1140 } 1141 1142 void KFilePlacesModel::addPlace(const QString &text, const QUrl &url, const QString &iconName, const QString &appName, const QModelIndex &after) 1143 { 1144 KBookmark bookmark = KFilePlacesItem::createBookmark(d->bookmarkManager, text, url, iconName); 1145 1146 if (!appName.isEmpty()) { 1147 bookmark.setMetaDataItem(QStringLiteral("OnlyInApp"), appName); 1148 } 1149 1150 if (after.isValid()) { 1151 KFilePlacesItem *item = static_cast<KFilePlacesItem *>(after.internalPointer()); 1152 d->bookmarkManager->root().moveBookmark(bookmark, item->bookmark()); 1153 } 1154 1155 refresh(); 1156 } 1157 1158 void KFilePlacesModel::editPlace(const QModelIndex &index, const QString &text, const QUrl &url, const QString &iconName, const QString &appName) 1159 { 1160 if (!index.isValid()) { 1161 return; 1162 } 1163 1164 KFilePlacesItem *item = static_cast<KFilePlacesItem *>(index.internalPointer()); 1165 1166 if (item->isDevice()) { 1167 return; 1168 } 1169 1170 KBookmark bookmark = item->bookmark(); 1171 1172 if (bookmark.isNull()) { 1173 return; 1174 } 1175 1176 QList<int> changedRoles; 1177 bool changed = false; 1178 1179 if (text != bookmark.fullText()) { 1180 bookmark.setFullText(text); 1181 changed = true; 1182 changedRoles << Qt::DisplayRole; 1183 } 1184 1185 if (url != bookmark.url()) { 1186 bookmark.setUrl(url); 1187 changed = true; 1188 changedRoles << KFilePlacesModel::UrlRole; 1189 } 1190 1191 if (iconName != bookmark.icon()) { 1192 bookmark.setIcon(iconName); 1193 changed = true; 1194 changedRoles << Qt::DecorationRole; 1195 } 1196 1197 const QString onlyInApp = bookmark.metaDataItem(QStringLiteral("OnlyInApp")); 1198 if (appName != onlyInApp) { 1199 bookmark.setMetaDataItem(QStringLiteral("OnlyInApp"), appName); 1200 changed = true; 1201 } 1202 1203 if (changed) { 1204 refresh(); 1205 Q_EMIT dataChanged(index, index, changedRoles); 1206 } 1207 } 1208 1209 void KFilePlacesModel::removePlace(const QModelIndex &index) const 1210 { 1211 if (!index.isValid()) { 1212 return; 1213 } 1214 1215 KFilePlacesItem *item = static_cast<KFilePlacesItem *>(index.internalPointer()); 1216 1217 if (item->isDevice()) { 1218 return; 1219 } 1220 1221 KBookmark bookmark = item->bookmark(); 1222 1223 if (bookmark.isNull()) { 1224 return; 1225 } 1226 1227 d->bookmarkManager->root().deleteBookmark(bookmark); 1228 refresh(); 1229 } 1230 1231 void KFilePlacesModel::setPlaceHidden(const QModelIndex &index, bool hidden) 1232 { 1233 if (!index.isValid()) { 1234 return; 1235 } 1236 1237 KFilePlacesItem *item = static_cast<KFilePlacesItem *>(index.internalPointer()); 1238 1239 if (item->bookmark().isNull() || item->isHidden() == hidden) { 1240 return; 1241 } 1242 1243 const bool groupHidden = isGroupHidden(item->groupType()); 1244 const bool hidingChildOnShownParent = hidden && !groupHidden; 1245 const bool showingChildOnShownParent = !hidden && !groupHidden; 1246 1247 if (hidingChildOnShownParent || showingChildOnShownParent) { 1248 item->setHidden(hidden); 1249 1250 d->reloadAndSignal(); 1251 Q_EMIT dataChanged(index, index, {KFilePlacesModel::HiddenRole}); 1252 } 1253 } 1254 1255 void KFilePlacesModel::setGroupHidden(const GroupType type, bool hidden) 1256 { 1257 if (isGroupHidden(type) == hidden) { 1258 return; 1259 } 1260 1261 d->bookmarkManager->root().setMetaDataItem(stateNameForGroupType(type), (hidden ? QStringLiteral("true") : QStringLiteral("false"))); 1262 d->reloadAndSignal(); 1263 Q_EMIT groupHiddenChanged(type, hidden); 1264 } 1265 1266 bool KFilePlacesModel::movePlace(int itemRow, int row) 1267 { 1268 KBookmark afterBookmark; 1269 1270 if ((itemRow < 0) || (itemRow >= d->items.count())) { 1271 return false; 1272 } 1273 1274 if (row >= d->items.count()) { 1275 row = -1; 1276 } 1277 1278 if (row == -1) { 1279 // The dropped item is moved or added to the last position 1280 1281 KFilePlacesItem *lastItem = d->items.last(); 1282 afterBookmark = lastItem->bookmark(); 1283 1284 } else { 1285 // The dropped item is moved or added before position 'row', ie after position 'row-1' 1286 1287 if (row > 0) { 1288 KFilePlacesItem *afterItem = d->items[row - 1]; 1289 afterBookmark = afterItem->bookmark(); 1290 } 1291 } 1292 1293 KFilePlacesItem *item = d->items[itemRow]; 1294 KBookmark bookmark = item->bookmark(); 1295 1296 int destRow = row == -1 ? d->items.count() : row; 1297 1298 // avoid move item away from its group 1299 destRow = d->findNearestPosition(itemRow, destRow); 1300 1301 // The item is not moved when the drop indicator is on either item edge 1302 if (itemRow == destRow || itemRow + 1 == destRow) { 1303 return false; 1304 } 1305 1306 beginMoveRows(QModelIndex(), itemRow, itemRow, QModelIndex(), destRow); 1307 d->bookmarkManager->root().moveBookmark(bookmark, afterBookmark); 1308 // Move item ourselves so that reloadBookmarks() does not consider 1309 // the move as a remove + insert. 1310 // 1311 // 2nd argument of QList::move() expects the final destination index, 1312 // but 'row' is the value of the destination index before the moved 1313 // item has been removed from its original position. That is why we 1314 // adjust if necessary. 1315 d->items.move(itemRow, itemRow < destRow ? (destRow - 1) : destRow); 1316 endMoveRows(); 1317 1318 return true; 1319 } 1320 1321 int KFilePlacesModel::hiddenCount() const 1322 { 1323 int rows = rowCount(); 1324 int hidden = 0; 1325 1326 for (int i = 0; i < rows; ++i) { 1327 if (isHidden(index(i, 0))) { 1328 hidden++; 1329 } 1330 } 1331 1332 return hidden; 1333 } 1334 1335 QAction *KFilePlacesModel::teardownActionForIndex(const QModelIndex &index) const 1336 { 1337 Solid::Device device = deviceForIndex(index); 1338 1339 QAction *action = nullptr; 1340 1341 if (device.is<Solid::StorageAccess>() && device.as<Solid::StorageAccess>()->isAccessible()) { 1342 Solid::StorageDrive *drive = device.as<Solid::StorageDrive>(); 1343 1344 if (drive == nullptr) { 1345 drive = device.parent().as<Solid::StorageDrive>(); 1346 } 1347 1348 const bool teardownInProgress = deviceAccessibility(index) == KFilePlacesModel::TeardownInProgress; 1349 1350 bool hotpluggable = false; 1351 bool removable = false; 1352 1353 if (drive != nullptr) { 1354 hotpluggable = drive->isHotpluggable(); 1355 removable = drive->isRemovable(); 1356 } 1357 1358 QString iconName; 1359 QString text; 1360 1361 if (device.is<Solid::OpticalDisc>()) { 1362 if (teardownInProgress) { 1363 text = i18nc("@action:inmenu", "Releasing…"); 1364 } else { 1365 text = i18nc("@action:inmenu", "&Release"); 1366 } 1367 } else if (removable || hotpluggable) { 1368 if (teardownInProgress) { 1369 text = i18nc("@action:inmenu", "Safely Removing…"); 1370 } else { 1371 text = i18nc("@action:inmenu", "&Safely Remove"); 1372 } 1373 iconName = QStringLiteral("media-eject"); 1374 } else { 1375 if (teardownInProgress) { 1376 text = i18nc("@action:inmenu", "Unmounting…"); 1377 } else { 1378 text = i18nc("@action:inmenu", "&Unmount"); 1379 } 1380 iconName = QStringLiteral("media-eject"); 1381 } 1382 1383 if (!iconName.isEmpty()) { 1384 action = new QAction(QIcon::fromTheme(iconName), text, nullptr); 1385 } else { 1386 action = new QAction(text, nullptr); 1387 } 1388 1389 if (teardownInProgress) { 1390 action->setEnabled(false); 1391 } 1392 } 1393 1394 return action; 1395 } 1396 1397 QAction *KFilePlacesModel::ejectActionForIndex(const QModelIndex &index) const 1398 { 1399 Solid::Device device = deviceForIndex(index); 1400 1401 if (device.is<Solid::OpticalDisc>()) { 1402 QString text = i18nc("@action:inmenu", "&Eject"); 1403 1404 return new QAction(QIcon::fromTheme(QStringLiteral("media-eject")), text, nullptr); 1405 } 1406 1407 return nullptr; 1408 } 1409 1410 void KFilePlacesModel::requestTeardown(const QModelIndex &index) 1411 { 1412 Solid::Device device = deviceForIndex(index); 1413 Solid::StorageAccess *access = device.as<Solid::StorageAccess>(); 1414 1415 if (access != nullptr) { 1416 d->teardownInProgress[access] = index; 1417 1418 const QString filePath = access->filePath(); 1419 connect(access, &Solid::StorageAccess::teardownDone, this, [this, access, filePath](Solid::ErrorType error, QVariant errorData) { 1420 d->storageTeardownDone(filePath, error, errorData, access); 1421 }); 1422 1423 access->teardown(); 1424 } 1425 } 1426 1427 void KFilePlacesModel::requestEject(const QModelIndex &index) 1428 { 1429 Solid::Device device = deviceForIndex(index); 1430 1431 Solid::OpticalDrive *drive = device.parent().as<Solid::OpticalDrive>(); 1432 1433 if (drive != nullptr) { 1434 d->teardownInProgress[drive] = index; 1435 1436 QString filePath; 1437 Solid::StorageAccess *access = device.as<Solid::StorageAccess>(); 1438 if (access) { 1439 filePath = access->filePath(); 1440 } 1441 1442 connect(drive, &Solid::OpticalDrive::ejectDone, this, [this, filePath, drive](Solid::ErrorType error, QVariant errorData) { 1443 d->storageTeardownDone(filePath, error, errorData, drive); 1444 }); 1445 1446 drive->eject(); 1447 } else { 1448 QString label = data(index, Qt::DisplayRole).toString().replace(QLatin1Char('&'), QLatin1String("&&")); 1449 QString message = i18n("The device '%1' is not a disk and cannot be ejected.", label); 1450 Q_EMIT errorMessage(message); 1451 } 1452 } 1453 1454 void KFilePlacesModel::requestSetup(const QModelIndex &index) 1455 { 1456 Solid::Device device = deviceForIndex(index); 1457 1458 if (device.is<Solid::StorageAccess>() && !d->setupInProgress.contains(device.as<Solid::StorageAccess>()) 1459 && !device.as<Solid::StorageAccess>()->isAccessible()) { 1460 Solid::StorageAccess *access = device.as<Solid::StorageAccess>(); 1461 1462 d->setupInProgress[access] = index; 1463 1464 connect(access, &Solid::StorageAccess::setupDone, this, [this, access](Solid::ErrorType error, QVariant errorData) { 1465 d->storageSetupDone(error, errorData, access); 1466 }); 1467 1468 access->setup(); 1469 } 1470 } 1471 1472 void KFilePlacesModelPrivate::storageSetupDone(Solid::ErrorType error, const QVariant &errorData, Solid::StorageAccess *sender) 1473 { 1474 QPersistentModelIndex index = setupInProgress.take(sender); 1475 1476 if (!index.isValid()) { 1477 return; 1478 } 1479 1480 if (!error) { 1481 Q_EMIT q->setupDone(index, true); 1482 } else { 1483 if (errorData.isValid()) { 1484 Q_EMIT q->errorMessage(i18n("An error occurred while accessing '%1', the system responded: %2", q->text(index), errorData.toString())); 1485 } else { 1486 Q_EMIT q->errorMessage(i18n("An error occurred while accessing '%1'", q->text(index))); 1487 } 1488 Q_EMIT q->setupDone(index, false); 1489 } 1490 } 1491 1492 void KFilePlacesModelPrivate::storageTeardownDone(const QString &filePath, Solid::ErrorType error, const QVariant &errorData, QObject *sender) 1493 { 1494 QPersistentModelIndex index = teardownInProgress.take(sender); 1495 if (!index.isValid()) { 1496 return; 1497 } 1498 1499 if (error == Solid::ErrorType::DeviceBusy && !filePath.isEmpty()) { 1500 auto *listOpenFilesJob = new KListOpenFilesJob(filePath); 1501 QObject::connect(listOpenFilesJob, &KIO::Job::result, q, [this, index, error, errorData, listOpenFilesJob]() { 1502 const auto blockingProcesses = listOpenFilesJob->processInfoList(); 1503 1504 QStringList blockingApps; 1505 blockingApps.reserve(blockingProcesses.count()); 1506 for (const auto &process : blockingProcesses) { 1507 blockingApps << process.name(); 1508 } 1509 1510 Q_EMIT q->teardownDone(index, error, errorData); 1511 if (blockingProcesses.isEmpty()) { 1512 Q_EMIT q->errorMessage(i18n("One or more files on this device are open within an application.")); 1513 } else { 1514 blockingApps.removeDuplicates(); 1515 Q_EMIT q->errorMessage(xi18np("One or more files on this device are opened in application <application>\"%2\"</application>.", 1516 "One or more files on this device are opened in following applications: <application>%2</application>.", 1517 blockingApps.count(), 1518 blockingApps.join(i18nc("separator in list of apps blocking device unmount", ", ")))); 1519 } 1520 }); 1521 listOpenFilesJob->start(); 1522 return; 1523 } 1524 1525 Q_EMIT q->teardownDone(index, error, errorData); 1526 if (error != Solid::ErrorType::NoError && error != Solid::ErrorType::UserCanceled) { 1527 Q_EMIT q->errorMessage(errorData.toString()); 1528 } 1529 } 1530 1531 void KFilePlacesModel::setSupportedSchemes(const QStringList &schemes) 1532 { 1533 d->supportedSchemes = schemes; 1534 d->reloadBookmarks(); 1535 Q_EMIT supportedSchemesChanged(); 1536 } 1537 1538 QStringList KFilePlacesModel::supportedSchemes() const 1539 { 1540 return d->supportedSchemes; 1541 } 1542 1543 namespace { 1544 QString partitionManagerPath() 1545 { 1546 static const QString path = QStandardPaths::findExecutable(QStringLiteral("partitionmanager")); 1547 return path; 1548 } 1549 } // namespace 1550 1551 QAction *KFilePlacesModel::partitionActionForIndex(const QModelIndex &index) const 1552 { 1553 const auto device = deviceForIndex(index); 1554 if (!device.is<Solid::Block>()) { 1555 return nullptr; 1556 } 1557 1558 // Not using kservice to find partitionmanager because we need to manually invoke it so we can pass the --device argument. 1559 if (partitionManagerPath().isEmpty()) { 1560 return nullptr; 1561 } 1562 1563 auto action = new QAction(QIcon::fromTheme(QStringLiteral("partitionmanager")), 1564 i18nc("@action:inmenu", "Reformat or Edit with Partition Manager"), 1565 nullptr); 1566 connect(action, &QAction::triggered, this, [device] { 1567 const auto block = device.as<Solid::Block>(); 1568 auto job = new KIO::CommandLauncherJob(partitionManagerPath(), {QStringLiteral("--device"), block->device()}); 1569 job->start(); 1570 }); 1571 return action; 1572 } 1573 1574 #include "moc_kfileplacesmodel.cpp"