/* This file is part of the KDE project SPDX-FileCopyrightText: 2007 Kevin Ottens SPDX-FileCopyrightText: 2007 David Faure SPDX-FileCopyrightText: 2023 Harald Sitter SPDX-License-Identifier: LGPL-2.0-only */ #include "kfileplacesmodel.h" #include "kfileplacesitem_p.h" #include "kfileplacesmodel_p.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace { QString stateNameForGroupType(KFilePlacesModel::GroupType type) { switch (type) { case KFilePlacesModel::PlacesType: return QStringLiteral("GroupState-Places-IsHidden"); case KFilePlacesModel::RemoteType: return QStringLiteral("GroupState-Remote-IsHidden"); case KFilePlacesModel::RecentlySavedType: return QStringLiteral("GroupState-RecentlySaved-IsHidden"); case KFilePlacesModel::SearchForType: return QStringLiteral("GroupState-SearchFor-IsHidden"); case KFilePlacesModel::DevicesType: return QStringLiteral("GroupState-Devices-IsHidden"); case KFilePlacesModel::RemovableDevicesType: return QStringLiteral("GroupState-RemovableDevices-IsHidden"); case KFilePlacesModel::TagsType: return QStringLiteral("GroupState-Tags-IsHidden"); default: Q_UNREACHABLE(); } } static bool isFileIndexingEnabled() { KConfig config(QStringLiteral("baloofilerc")); KConfigGroup basicSettings = config.group(QStringLiteral("Basic Settings")); return basicSettings.readEntry("Indexing-Enabled", true); } static QString timelineDateString(int year, int month, int day = 0) { const QString dateFormat = QStringLiteral("%1-%2"); QString date = dateFormat.arg(year).arg(month, 2, 10, QLatin1Char('0')); if (day > 0) { date += QStringLiteral("-%1").arg(day, 2, 10, QLatin1Char('0')); } return date; } static QUrl createTimelineUrl(const QUrl &url) { // based on dolphin urls const QString timelinePrefix = QLatin1String("timeline:") + QLatin1Char('/'); QUrl timelineUrl; const QString path = url.toDisplayString(QUrl::PreferLocalFile); if (path.endsWith(QLatin1String("/yesterday"))) { const QDate date = QDate::currentDate().addDays(-1); const int year = date.year(); const int month = date.month(); const int day = date.day(); timelineUrl = QUrl(timelinePrefix + timelineDateString(year, month) + QLatin1Char('/') + timelineDateString(year, month, day)); } else if (path.endsWith(QLatin1String("/thismonth"))) { const QDate date = QDate::currentDate(); timelineUrl = QUrl(timelinePrefix + timelineDateString(date.year(), date.month())); } else if (path.endsWith(QLatin1String("/lastmonth"))) { const QDate date = QDate::currentDate().addMonths(-1); timelineUrl = QUrl(timelinePrefix + timelineDateString(date.year(), date.month())); } else { timelineUrl = url; } return timelineUrl; } static QUrl createSearchUrl(const QUrl &url) { QUrl searchUrl = url; const QString path = url.toDisplayString(QUrl::PreferLocalFile); const QStringList validSearchPaths = {QStringLiteral("/documents"), QStringLiteral("/images"), QStringLiteral("/audio"), QStringLiteral("/videos")}; for (const QString &validPath : validSearchPaths) { if (path.endsWith(validPath)) { searchUrl.setScheme(QStringLiteral("baloosearch")); return searchUrl; } } qWarning() << "Invalid search url:" << url; return searchUrl; } } KFilePlacesModelPrivate::KFilePlacesModelPrivate(KFilePlacesModel *qq) : q(qq) , fileIndexingEnabled(isFileIndexingEnabled()) , tagsLister(new KCoreDirLister(q)) { if (KProtocolInfo::isKnownProtocol(QStringLiteral("tags"))) { QObject::connect(tagsLister, &KCoreDirLister::itemsAdded, q, [this](const QUrl &, const KFileItemList &items) { if (tags.isEmpty()) { QList existingBookmarks; KBookmarkGroup root = bookmarkManager->root(); KBookmark bookmark = root.first(); while (!bookmark.isNull()) { existingBookmarks.append(bookmark.url()); bookmark = root.next(bookmark); } if (!existingBookmarks.contains(QUrl(tagsUrlBase))) { KBookmark alltags = KFilePlacesItem::createSystemBookmark(bookmarkManager, kli18nc("KFile System Bookmarks", "All tags").untranslatedText(), QUrl(tagsUrlBase), QStringLiteral("tag")); } } for (const KFileItem &item : items) { const QString name = item.name(); if (!tags.contains(name)) { tags.append(name); } } reloadBookmarks(); }); QObject::connect(tagsLister, &KCoreDirLister::itemsDeleted, q, [this](const KFileItemList &items) { for (const KFileItem &item : items) { tags.removeAll(item.name()); } reloadBookmarks(); }); tagsLister->openUrl(QUrl(tagsUrlBase), KCoreDirLister::OpenUrlFlag::Reload); } } QString KFilePlacesModelPrivate::ignoreMimeType() { return QStringLiteral("application/x-kfileplacesmodel-ignore"); } QString KFilePlacesModelPrivate::internalMimeType(const KFilePlacesModel *model) { return QStringLiteral("application/x-kfileplacesmodel-") + QString::number(reinterpret_cast(model)); } KBookmark KFilePlacesModel::bookmarkForUrl(const QUrl &searchUrl) const { KBookmarkGroup root = d->bookmarkManager->root(); KBookmark current = root.first(); while (!current.isNull()) { if (current.url() == searchUrl) { return current; } current = root.next(current); } return KBookmark(); } static inline QString versionKey() { return QStringLiteral("kde_places_version"); } KFilePlacesModel::KFilePlacesModel(QObject *parent) : QAbstractItemModel(parent) , d(new KFilePlacesModelPrivate(this)) { const QString file = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1String("/user-places.xbel"); d->bookmarkManager = new KBookmarkManager(file, this); // Let's put some places in there if it's empty. KBookmarkGroup root = d->bookmarkManager->root(); const auto setDefaultMetadataItemForGroup = [&root](KFilePlacesModel::GroupType type) { root.setMetaDataItem(stateNameForGroupType(type), QStringLiteral("false")); }; // Increase this version number and use the following logic to handle the update process for existing installations. static const int s_currentVersion = 4; const bool newFile = root.first().isNull() || !QFile::exists(file); const int fileVersion = root.metaDataItem(versionKey()).toInt(); if (newFile || fileVersion < s_currentVersion) { root.setMetaDataItem(versionKey(), QString::number(s_currentVersion)); const QList seenUrls = root.groupUrlList(); /* clang-format off */ auto createSystemBookmark = [this, &seenUrls](const char *untranslatedLabel, const QUrl &url, const QString &iconName, const KBookmark &after) { if (!seenUrls.contains(url)) { return KFilePlacesItem::createSystemBookmark(d->bookmarkManager, untranslatedLabel, url, iconName, after); } return KBookmark(); }; /* clang-format on */ if (fileVersion < 2) { // NOTE: The context for these kli18nc calls has to be "KFile System Bookmarks". // The real i18nc call is made later, with this context, so the two must match. createSystemBookmark(kli18nc("KFile System Bookmarks", "Home").untranslatedText(), QUrl::fromLocalFile(QDir::homePath()), QStringLiteral("user-home"), KBookmark()); // Some distros may not create various standard XDG folders by default // so check for their existence before adding bookmarks for them const QString desktopFolder = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation); if (QDir(desktopFolder).exists()) { createSystemBookmark(kli18nc("KFile System Bookmarks", "Desktop").untranslatedText(), QUrl::fromLocalFile(desktopFolder), QStringLiteral("user-desktop"), KBookmark()); } const QString documentsFolder = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation); if (QDir(documentsFolder).exists()) { createSystemBookmark(kli18nc("KFile System Bookmarks", "Documents").untranslatedText(), QUrl::fromLocalFile(documentsFolder), QStringLiteral("folder-documents"), KBookmark()); } const QString downloadFolder = QStandardPaths::writableLocation(QStandardPaths::DownloadLocation); if (QDir(downloadFolder).exists()) { createSystemBookmark(kli18nc("KFile System Bookmarks", "Downloads").untranslatedText(), QUrl::fromLocalFile(downloadFolder), QStringLiteral("folder-downloads"), KBookmark()); } createSystemBookmark(kli18nc("KFile System Bookmarks", "Network").untranslatedText(), QUrl(QStringLiteral("remote:/")), QStringLiteral("folder-network"), KBookmark()); createSystemBookmark(kli18nc("KFile System Bookmarks", "Trash").untranslatedText(), QUrl(QStringLiteral("trash:/")), QStringLiteral("user-trash"), KBookmark()); } if (!newFile && fileVersion < 3) { KBookmarkGroup rootGroup = d->bookmarkManager->root(); KBookmark bItem = rootGroup.first(); while (!bItem.isNull()) { KBookmark nextbItem = rootGroup.next(bItem); const bool isSystemItem = bItem.metaDataItem(QStringLiteral("isSystemItem")) == QLatin1String("true"); if (isSystemItem) { const QString text = bItem.fullText(); // Because of b8a4c2223453932202397d812a0c6b30c6186c70 we need to find the system bookmark named Audio Files // and rename it to Audio, otherwise users are getting untranslated strings if (text == QLatin1String("Audio Files")) { bItem.setFullText(QStringLiteral("Audio")); } else if (text == QLatin1String("Today")) { // Because of 19feef732085b444515da3f6c66f3352bbcb1824 we need to find the system bookmark named Today // and rename it to Modified Today, otherwise users are getting untranslated strings bItem.setFullText(QStringLiteral("Modified Today")); } else if (text == QLatin1String("Yesterday")) { // Because of 19feef732085b444515da3f6c66f3352bbcb1824 we need to find the system bookmark named Yesterday // and rename it to Modified Yesterday, otherwise users are getting untranslated strings bItem.setFullText(QStringLiteral("Modified Yesterday")); } else if (text == QLatin1String("This Month")) { // Because of 7e1d2fb84546506c91684dd222c2485f0783848f we need to find the system bookmark named This Month // and remove it, otherwise users are getting untranslated strings rootGroup.deleteBookmark(bItem); } else if (text == QLatin1String("Last Month")) { // Because of 7e1d2fb84546506c91684dd222c2485f0783848f we need to find the system bookmark named Last Month // and remove it, otherwise users are getting untranslated strings rootGroup.deleteBookmark(bItem); } } bItem = nextbItem; } } if (fileVersion < 4) { auto findSystemBookmark = [this](const QString &untranslatedText) { KBookmarkGroup root = d->bookmarkManager->root(); KBookmark bItem = root.first(); while (!bItem.isNull()) { const bool isSystemItem = bItem.metaDataItem(QStringLiteral("isSystemItem")) == QLatin1String("true"); if (isSystemItem && bItem.fullText() == untranslatedText) { return bItem; } bItem = root.next(bItem); } return KBookmark(); }; // This variable is used to insert the new bookmarks at the correct place starting after the "Downloads" // bookmark. When the user already has some of the bookmarks set up manually, the createSystemBookmark() // function returns an empty KBookmark so the following entries will be added at the end of the bookmark // section to not mess with the users setup. KBookmark after = findSystemBookmark(QLatin1String("Downloads")); const QString musicFolder = QStandardPaths::writableLocation(QStandardPaths::MusicLocation); if (QDir(musicFolder).exists()) { after = createSystemBookmark(kli18nc("KFile System Bookmarks", "Music").untranslatedText(), QUrl::fromLocalFile(musicFolder), QStringLiteral("folder-music"), after); } const QString pictureFolder = QStandardPaths::writableLocation(QStandardPaths::PicturesLocation); if (QDir(pictureFolder).exists()) { after = createSystemBookmark(kli18nc("KFile System Bookmarks", "Pictures").untranslatedText(), QUrl::fromLocalFile(pictureFolder), QStringLiteral("folder-pictures"), after); } // Choosing the name "Videos" instead of "Movies", since that is how the folder // is called normally on Linux: https://cgit.freedesktop.org/xdg/xdg-user-dirs/tree/user-dirs.defaults const QString videoFolder = QStandardPaths::writableLocation(QStandardPaths::MoviesLocation); if (QDir(videoFolder).exists()) { after = createSystemBookmark(kli18nc("KFile System Bookmarks", "Videos").untranslatedText(), QUrl::fromLocalFile(videoFolder), QStringLiteral("folder-videos"), after); } } if (newFile) { setDefaultMetadataItemForGroup(PlacesType); setDefaultMetadataItemForGroup(RemoteType); setDefaultMetadataItemForGroup(DevicesType); setDefaultMetadataItemForGroup(RemovableDevicesType); setDefaultMetadataItemForGroup(TagsType); } // Force bookmarks to be saved. If on open/save dialog and the bookmarks are not saved, QFile::exists // will always return false, which opening/closing all the time the open/save dialog would cause the // bookmarks to be added once each time, having lots of times each bookmark. (ereslibre) d->bookmarkManager->saveAs(file); } // Add a Recently Used entry if available (it comes from kio-extras) if (qEnvironmentVariableIsSet("KDE_FULL_SESSION") && KProtocolInfo::isKnownProtocol(QStringLiteral("recentlyused")) && root.metaDataItem(QStringLiteral("withRecentlyUsed")) != QLatin1String("true")) { root.setMetaDataItem(QStringLiteral("withRecentlyUsed"), QStringLiteral("true")); KBookmark recentFilesBookmark = KFilePlacesItem::createSystemBookmark(d->bookmarkManager, kli18nc("KFile System Bookmarks", "Recent Files").untranslatedText(), QUrl(QStringLiteral("recentlyused:/files")), QStringLiteral("document-open-recent")); KBookmark recentDirectoriesBookmark = KFilePlacesItem::createSystemBookmark(d->bookmarkManager, kli18nc("KFile System Bookmarks", "Recent Locations").untranslatedText(), QUrl(QStringLiteral("recentlyused:/locations")), QStringLiteral("folder-open-recent")); setDefaultMetadataItemForGroup(RecentlySavedType); // Move The recently used bookmarks below the trash, making it the first element in the Recent group KBookmark trashBookmark = bookmarkForUrl(QUrl(QStringLiteral("trash:/"))); if (!trashBookmark.isNull()) { root.moveBookmark(recentFilesBookmark, trashBookmark); root.moveBookmark(recentDirectoriesBookmark, recentFilesBookmark); } d->bookmarkManager->save(); } // if baloo is enabled, add new urls even if the bookmark file is not empty if (d->fileIndexingEnabled && root.metaDataItem(QStringLiteral("withBaloo")) != QLatin1String("true")) { root.setMetaDataItem(QStringLiteral("withBaloo"), QStringLiteral("true")); // don't add by default "Modified Today" and "Modified Yesterday" when recentlyused:/ is present if (root.metaDataItem(QStringLiteral("withRecentlyUsed")) != QLatin1String("true")) { KFilePlacesItem::createSystemBookmark(d->bookmarkManager, kli18nc("KFile System Bookmarks", "Modified Today").untranslatedText(), QUrl(QStringLiteral("timeline:/today")), QStringLiteral("go-jump-today")); KFilePlacesItem::createSystemBookmark(d->bookmarkManager, kli18nc("KFile System Bookmarks", "Modified Yesterday").untranslatedText(), QUrl(QStringLiteral("timeline:/yesterday")), QStringLiteral("view-calendar-day")); } setDefaultMetadataItemForGroup(SearchForType); setDefaultMetadataItemForGroup(RecentlySavedType); d->bookmarkManager->save(); } QString predicate( QString::fromLatin1("[[[[ StorageVolume.ignored == false AND [ StorageVolume.usage == 'FileSystem' OR StorageVolume.usage == 'Encrypted' ]]" " OR " "[ IS StorageAccess AND StorageDrive.driveType == 'Floppy' ]]" " OR " "OpticalDisc.availableContent & 'Audio' ]" " OR " "StorageAccess.ignored == false ]")); if (KProtocolInfo::isKnownProtocol(QStringLiteral("mtp"))) { predicate = QLatin1Char('[') + predicate + QLatin1String(" OR PortableMediaPlayer.supportedProtocols == 'mtp']"); } if (KProtocolInfo::isKnownProtocol(QStringLiteral("afc"))) { predicate = QLatin1Char('[') + predicate + QLatin1String(" OR PortableMediaPlayer.supportedProtocols == 'afc']"); } d->predicate = Solid::Predicate::fromString(predicate); Q_ASSERT(d->predicate.isValid()); connect(d->bookmarkManager, &KBookmarkManager::changed, this, [this]() { d->reloadBookmarks(); }); d->reloadBookmarks(); QTimer::singleShot(0, this, [this]() { d->initDeviceList(); }); } KFilePlacesModel::~KFilePlacesModel() = default; QUrl KFilePlacesModel::url(const QModelIndex &index) const { return data(index, UrlRole).toUrl(); } bool KFilePlacesModel::setupNeeded(const QModelIndex &index) const { return data(index, SetupNeededRole).toBool(); } QIcon KFilePlacesModel::icon(const QModelIndex &index) const { return data(index, Qt::DecorationRole).value(); } QString KFilePlacesModel::text(const QModelIndex &index) const { return data(index, Qt::DisplayRole).toString(); } bool KFilePlacesModel::isHidden(const QModelIndex &index) const { // Note: we do not want to show an index if its parent is hidden return data(index, HiddenRole).toBool() || isGroupHidden(index); } bool KFilePlacesModel::isGroupHidden(const GroupType type) const { const QString hidden = d->bookmarkManager->root().metaDataItem(stateNameForGroupType(type)); return hidden == QLatin1String("true"); } bool KFilePlacesModel::isGroupHidden(const QModelIndex &index) const { if (!index.isValid()) { return false; } KFilePlacesItem *item = static_cast(index.internalPointer()); return isGroupHidden(item->groupType()); } bool KFilePlacesModel::isDevice(const QModelIndex &index) const { if (!index.isValid()) { return false; } KFilePlacesItem *item = static_cast(index.internalPointer()); return item->isDevice(); } bool KFilePlacesModel::isTeardownAllowed(const QModelIndex &index) const { if (!index.isValid()) { return false; } KFilePlacesItem *item = static_cast(index.internalPointer()); return item->isTeardownAllowed(); } bool KFilePlacesModel::isEjectAllowed(const QModelIndex &index) const { if (!index.isValid()) { return false; } KFilePlacesItem *item = static_cast(index.internalPointer()); return item->isEjectAllowed(); } bool KFilePlacesModel::isTeardownOverlayRecommended(const QModelIndex &index) const { if (!index.isValid()) { return false; } KFilePlacesItem *item = static_cast(index.internalPointer()); return item->isTeardownOverlayRecommended(); } KFilePlacesModel::DeviceAccessibility KFilePlacesModel::deviceAccessibility(const QModelIndex &index) const { if (!index.isValid()) { return KFilePlacesModel::Accessible; } KFilePlacesItem *item = static_cast(index.internalPointer()); return item->deviceAccessibility(); } Solid::Device KFilePlacesModel::deviceForIndex(const QModelIndex &index) const { if (!index.isValid()) { return Solid::Device(); } KFilePlacesItem *item = static_cast(index.internalPointer()); if (item->isDevice()) { return item->device(); } else { return Solid::Device(); } } KBookmark KFilePlacesModel::bookmarkForIndex(const QModelIndex &index) const { if (!index.isValid()) { return KBookmark(); } KFilePlacesItem *item = static_cast(index.internalPointer()); return item->bookmark(); } KFilePlacesModel::GroupType KFilePlacesModel::groupType(const QModelIndex &index) const { if (!index.isValid()) { return UnknownType; } KFilePlacesItem *item = static_cast(index.internalPointer()); return item->groupType(); } QModelIndexList KFilePlacesModel::groupIndexes(const KFilePlacesModel::GroupType type) const { if (type == UnknownType) { return QModelIndexList(); } QModelIndexList indexes; const int rows = rowCount(); for (int row = 0; row < rows; ++row) { const QModelIndex current = index(row, 0); if (groupType(current) == type) { indexes << current; } } return indexes; } QVariant KFilePlacesModel::data(const QModelIndex &index, int role) const { if (!index.isValid()) { return QVariant(); } KFilePlacesItem *item = static_cast(index.internalPointer()); if (role == KFilePlacesModel::GroupHiddenRole) { return isGroupHidden(item->groupType()); } else { return item->data(role); } } QModelIndex KFilePlacesModel::index(int row, int column, const QModelIndex &parent) const { if (row < 0 || column != 0 || row >= d->items.size()) { return QModelIndex(); } if (parent.isValid()) { return QModelIndex(); } return createIndex(row, column, d->items.at(row)); } QModelIndex KFilePlacesModel::parent(const QModelIndex &child) const { Q_UNUSED(child); return QModelIndex(); } QHash KFilePlacesModel::roleNames() const { auto super = QAbstractItemModel::roleNames(); super[UrlRole] = "url"; super[HiddenRole] = "isHidden"; super[SetupNeededRole] = "isSetupNeeded"; super[FixedDeviceRole] = "isFixedDevice"; super[CapacityBarRecommendedRole] = "isCapacityBarRecommended"; super[GroupRole] = "group"; super[IconNameRole] = "iconName"; super[GroupHiddenRole] = "isGroupHidden"; super[TeardownAllowedRole] = "isTeardownAllowed"; super[EjectAllowedRole] = "isEjectAllowed"; super[TeardownOverlayRecommendedRole] = "isTeardownOverlayRecommended"; super[DeviceAccessibilityRole] = "deviceAccessibility"; return super; } int KFilePlacesModel::rowCount(const QModelIndex &parent) const { if (parent.isValid()) { return 0; } else { return d->items.size(); } } int KFilePlacesModel::columnCount(const QModelIndex &parent) const { Q_UNUSED(parent) // We only know 1 piece of information for a particular entry return 1; } QModelIndex KFilePlacesModel::closestItem(const QUrl &url) const { int foundRow = -1; int maxLength = 0; // Search the item which is equal to the URL or at least is a parent URL. // If there are more than one possible item URL candidates, choose the item // which covers the bigger range of the URL. for (int row = 0; row < d->items.size(); ++row) { KFilePlacesItem *item = d->items[row]; if (item->isHidden() || isGroupHidden(item->groupType())) { continue; } const QUrl itemUrl = convertedUrl(item->data(UrlRole).toUrl()); if (itemUrl.matches(url, QUrl::StripTrailingSlash) || (itemUrl.isParentOf(url) && itemUrl.query() == url.query() && itemUrl.fragment() == url.fragment())) { const int length = itemUrl.toString().length(); if (length > maxLength) { foundRow = row; maxLength = length; } } } if (foundRow == -1) { return QModelIndex(); } else { return createIndex(foundRow, 0, d->items[foundRow]); } } void KFilePlacesModelPrivate::initDeviceList() { Solid::DeviceNotifier *notifier = Solid::DeviceNotifier::instance(); QObject::connect(notifier, &Solid::DeviceNotifier::deviceAdded, q, [this](const QString &device) { deviceAdded(device); }); QObject::connect(notifier, &Solid::DeviceNotifier::deviceRemoved, q, [this](const QString &device) { deviceRemoved(device); }); availableDevices = Solid::Device::listFromQuery(predicate); reloadBookmarks(); } void KFilePlacesModelPrivate::deviceAdded(const QString &udi) { Solid::Device d(udi); if (predicate.matches(d)) { availableDevices << d; reloadBookmarks(); } } void KFilePlacesModelPrivate::deviceRemoved(const QString &udi) { auto it = std::find_if(availableDevices.begin(), availableDevices.end(), [udi](const Solid::Device &device) { return device.udi() == udi; }); if (it != availableDevices.end()) { availableDevices.erase(it); reloadBookmarks(); } } void KFilePlacesModelPrivate::itemChanged(const QString &id, const QList &roles) { for (int row = 0; row < items.size(); ++row) { if (items.at(row)->id() == id) { QModelIndex index = q->index(row, 0); Q_EMIT q->dataChanged(index, index, roles); } } } void KFilePlacesModelPrivate::reloadBookmarks() { QList currentItems = loadBookmarkList(); QList::Iterator it_i = items.begin(); QList::Iterator it_c = currentItems.begin(); QList::Iterator end_i = items.end(); QList::Iterator end_c = currentItems.end(); while (it_i != end_i || it_c != end_c) { if (it_i == end_i && it_c != end_c) { int row = items.count(); q->beginInsertRows(QModelIndex(), row, row); it_i = items.insert(it_i, *it_c); ++it_i; it_c = currentItems.erase(it_c); end_i = items.end(); end_c = currentItems.end(); q->endInsertRows(); } else if (it_i != end_i && it_c == end_c) { int row = items.indexOf(*it_i); q->beginRemoveRows(QModelIndex(), row, row); delete *it_i; it_i = items.erase(it_i); end_i = items.end(); end_c = currentItems.end(); q->endRemoveRows(); } else if ((*it_i)->id() == (*it_c)->id()) { bool shouldEmit = !((*it_i)->bookmark() == (*it_c)->bookmark()); (*it_i)->setBookmark((*it_c)->bookmark()); if (shouldEmit) { int row = items.indexOf(*it_i); QModelIndex idx = q->index(row, 0); Q_EMIT q->dataChanged(idx, idx); } ++it_i; ++it_c; } else { int row = items.indexOf(*it_i); if (it_i + 1 != end_i && (*(it_i + 1))->id() == (*it_c)->id()) { // if the next one matches, it's a remove q->beginRemoveRows(QModelIndex(), row, row); delete *it_i; it_i = items.erase(it_i); end_i = items.end(); end_c = currentItems.end(); q->endRemoveRows(); } else { q->beginInsertRows(QModelIndex(), row, row); it_i = items.insert(it_i, *it_c); ++it_i; it_c = currentItems.erase(it_c); end_i = items.end(); end_c = currentItems.end(); q->endInsertRows(); } } } qDeleteAll(currentItems); currentItems.clear(); Q_EMIT q->reloaded(); } bool KFilePlacesModelPrivate::isBalooUrl(const QUrl &url) const { const QString scheme = url.scheme(); return ((scheme == QLatin1String("timeline")) || (scheme == QLatin1String("search"))); } QList KFilePlacesModelPrivate::loadBookmarkList() { QList items; KBookmarkGroup root = bookmarkManager->root(); KBookmark bookmark = root.first(); QList devices{availableDevices}; QList tagsList = tags; while (!bookmark.isNull()) { KFilePlacesItem *item = nullptr; if (const QString udi = bookmark.metaDataItem(QStringLiteral("UDI")); !udi.isEmpty()) { const QString uuid = bookmark.metaDataItem(QStringLiteral("uuid")); auto it = std::find_if(devices.begin(), devices.end(), [udi, uuid](const Solid::Device &device) { if (!uuid.isEmpty()) { auto storageVolume = device.as(); if (storageVolume && !storageVolume->uuid().isEmpty()) { return storageVolume->uuid() == uuid; } } return device.udi() == udi; }); if (it != devices.end()) { item = new KFilePlacesItem(bookmarkManager, bookmark.address(), it->udi(), q); if (!item->hasSupportedScheme(supportedSchemes)) { delete item; item = nullptr; } devices.erase(it); } } else if (const QString tag = bookmark.metaDataItem(QStringLiteral("tag")); !tag.isEmpty()) { auto it = std::find(tagsList.begin(), tagsList.end(), tag); if (it != tagsList.end()) { tagsList.erase(it); item = new KFilePlacesItem(bookmarkManager, bookmark.address(), QString(), q); } } else if (const QUrl url = bookmark.url(); url.isValid()) { QString appName = bookmark.metaDataItem(QStringLiteral("OnlyInApp")); bool allowedHere = appName.isEmpty() || appName == QCoreApplication::instance()->applicationName(); bool isSupportedUrl = isBalooUrl(url) ? fileIndexingEnabled : true; bool isSupportedScheme = supportedSchemes.isEmpty() || supportedSchemes.contains(url.scheme()); if (isSupportedScheme && isSupportedUrl && allowedHere) { // TODO: Update bookmark internal element item = new KFilePlacesItem(bookmarkManager, bookmark.address(), QString(), q); } } if (item) { QObject::connect(item, &KFilePlacesItem::itemChanged, q, [this](const QString &id, const QList &roles) { itemChanged(id, roles); }); items << item; } bookmark = root.next(bookmark); } // Add bookmarks for the remaining devices, they were previously unknown for (const Solid::Device &device : std::as_const(devices)) { bookmark = KFilePlacesItem::createDeviceBookmark(bookmarkManager, device); if (!bookmark.isNull()) { KFilePlacesItem *item = new KFilePlacesItem(bookmarkManager, bookmark.address(), device.udi(), q); QObject::connect(item, &KFilePlacesItem::itemChanged, q, [this](const QString &id, const QList &roles) { itemChanged(id, roles); }); // TODO: Update bookmark internal element items << item; } } for (const QString &tag : tagsList) { bookmark = KFilePlacesItem::createTagBookmark(bookmarkManager, tag); if (!bookmark.isNull()) { KFilePlacesItem *item = new KFilePlacesItem(bookmarkManager, bookmark.address(), tag, q); QObject::connect(item, &KFilePlacesItem::itemChanged, q, [this](const QString &id, const QList &roles) { itemChanged(id, roles); }); items << item; } } // return a sorted list based on groups std::stable_sort(items.begin(), items.end(), [](KFilePlacesItem *itemA, KFilePlacesItem *itemB) { return (itemA->groupType() < itemB->groupType()); }); return items; } int KFilePlacesModelPrivate::findNearestPosition(int source, int target) { const KFilePlacesItem *item = items.at(source); const KFilePlacesModel::GroupType groupType = item->groupType(); int newTarget = qMin(target, items.count() - 1); // moving inside the same group is ok if ((items.at(newTarget)->groupType() == groupType)) { return target; } if (target > source) { // moving down, move it to the end of the group int groupFooter = source; while (items.at(groupFooter)->groupType() == groupType) { groupFooter++; // end of the list move it there if (groupFooter == items.count()) { break; } } target = groupFooter; } else { // moving up, move it to beginning of the group int groupHead = source; while (items.at(groupHead)->groupType() == groupType) { groupHead--; // beginning of the list move it there if (groupHead == 0) { break; } } target = groupHead; } return target; } void KFilePlacesModelPrivate::reloadAndSignal() { bookmarkManager->emitChanged(bookmarkManager->root()); // ... we'll get relisted anyway } Qt::DropActions KFilePlacesModel::supportedDropActions() const { return Qt::ActionMask; } Qt::ItemFlags KFilePlacesModel::flags(const QModelIndex &index) const { Qt::ItemFlags res; if (index.isValid()) { res |= Qt::ItemIsDragEnabled | Qt::ItemIsSelectable | Qt::ItemIsEnabled; } if (!index.isValid()) { res |= Qt::ItemIsDropEnabled; } return res; } QStringList KFilePlacesModel::mimeTypes() const { QStringList types; types << KFilePlacesModelPrivate::internalMimeType(this) << QStringLiteral("text/uri-list"); return types; } QMimeData *KFilePlacesModel::mimeData(const QModelIndexList &indexes) const { QList urls; QByteArray itemData; QDataStream stream(&itemData, QIODevice::WriteOnly); for (const QModelIndex &index : std::as_const(indexes)) { QUrl itemUrl = url(index); if (itemUrl.isValid()) { urls << itemUrl; } stream << index.row(); } QMimeData *mimeData = new QMimeData(); if (!urls.isEmpty()) { mimeData->setUrls(urls); } mimeData->setData(KFilePlacesModelPrivate::internalMimeType(this), itemData); return mimeData; } bool KFilePlacesModel::dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) { if (action == Qt::IgnoreAction) { return true; } if (column > 0) { return false; } if (row == -1 && parent.isValid()) { return false; // Don't allow to move an item onto another one, // too easy for the user to mess something up // If we really really want to allow copying files this way, // let's do it in the views to get the good old drop menu } if (data->hasFormat(KFilePlacesModelPrivate::ignoreMimeType())) { return false; } if (data->hasFormat(KFilePlacesModelPrivate::internalMimeType(this))) { // The operation is an internal move QByteArray itemData = data->data(KFilePlacesModelPrivate::internalMimeType(this)); QDataStream stream(&itemData, QIODevice::ReadOnly); int itemRow; stream >> itemRow; if (!movePlace(itemRow, row)) { return false; } } else if (data->hasFormat(QStringLiteral("text/uri-list"))) { // The operation is an add QMimeDatabase db; KBookmark afterBookmark; if (row == -1) { // The dropped item is moved or added to the last position KFilePlacesItem *lastItem = d->items.last(); afterBookmark = lastItem->bookmark(); } else { // The dropped item is moved or added before position 'row', ie after position 'row-1' if (row > 0) { KFilePlacesItem *afterItem = d->items[row - 1]; afterBookmark = afterItem->bookmark(); } } const QList urls = KUrlMimeData::urlsFromMimeData(data); KBookmarkGroup group = d->bookmarkManager->root(); for (const QUrl &url : urls) { KIO::StatJob *job = KIO::stat(url, KIO::StatJob::SourceSide, KIO::StatBasic); if (!job->exec()) { Q_EMIT errorMessage(i18nc("Placeholder is error message", "Could not add to the Places panel: %1", job->errorString())); continue; } KFileItem item(job->statResult(), url, true /*delayed mime types*/); if (!item.isDir()) { Q_EMIT errorMessage(i18n("Only folders can be added to the Places panel.")); continue; } KBookmark bookmark = KFilePlacesItem::createBookmark(d->bookmarkManager, item.text(), url, KIO::iconNameForUrl(url)); group.moveBookmark(bookmark, afterBookmark); afterBookmark = bookmark; } } else { // Oops, shouldn't happen thanks to mimeTypes() qWarning() << ": received wrong mimedata, " << data->formats(); return false; } refresh(); return true; } void KFilePlacesModel::refresh() const { d->reloadAndSignal(); } QUrl KFilePlacesModel::convertedUrl(const QUrl &url) { QUrl newUrl = url; if (url.scheme() == QLatin1String("timeline")) { newUrl = createTimelineUrl(url); } else if (url.scheme() == QLatin1String("search")) { newUrl = createSearchUrl(url); } return newUrl; } void KFilePlacesModel::addPlace(const QString &text, const QUrl &url, const QString &iconName, const QString &appName) { addPlace(text, url, iconName, appName, QModelIndex()); } void KFilePlacesModel::addPlace(const QString &text, const QUrl &url, const QString &iconName, const QString &appName, const QModelIndex &after) { KBookmark bookmark = KFilePlacesItem::createBookmark(d->bookmarkManager, text, url, iconName); if (!appName.isEmpty()) { bookmark.setMetaDataItem(QStringLiteral("OnlyInApp"), appName); } if (after.isValid()) { KFilePlacesItem *item = static_cast(after.internalPointer()); d->bookmarkManager->root().moveBookmark(bookmark, item->bookmark()); } refresh(); } void KFilePlacesModel::editPlace(const QModelIndex &index, const QString &text, const QUrl &url, const QString &iconName, const QString &appName) { if (!index.isValid()) { return; } KFilePlacesItem *item = static_cast(index.internalPointer()); if (item->isDevice()) { return; } KBookmark bookmark = item->bookmark(); if (bookmark.isNull()) { return; } QList changedRoles; bool changed = false; if (text != bookmark.fullText()) { bookmark.setFullText(text); changed = true; changedRoles << Qt::DisplayRole; } if (url != bookmark.url()) { bookmark.setUrl(url); changed = true; changedRoles << KFilePlacesModel::UrlRole; } if (iconName != bookmark.icon()) { bookmark.setIcon(iconName); changed = true; changedRoles << Qt::DecorationRole; } const QString onlyInApp = bookmark.metaDataItem(QStringLiteral("OnlyInApp")); if (appName != onlyInApp) { bookmark.setMetaDataItem(QStringLiteral("OnlyInApp"), appName); changed = true; } if (changed) { refresh(); Q_EMIT dataChanged(index, index, changedRoles); } } void KFilePlacesModel::removePlace(const QModelIndex &index) const { if (!index.isValid()) { return; } KFilePlacesItem *item = static_cast(index.internalPointer()); if (item->isDevice()) { return; } KBookmark bookmark = item->bookmark(); if (bookmark.isNull()) { return; } d->bookmarkManager->root().deleteBookmark(bookmark); refresh(); } void KFilePlacesModel::setPlaceHidden(const QModelIndex &index, bool hidden) { if (!index.isValid()) { return; } KFilePlacesItem *item = static_cast(index.internalPointer()); if (item->bookmark().isNull() || item->isHidden() == hidden) { return; } const bool groupHidden = isGroupHidden(item->groupType()); const bool hidingChildOnShownParent = hidden && !groupHidden; const bool showingChildOnShownParent = !hidden && !groupHidden; if (hidingChildOnShownParent || showingChildOnShownParent) { item->setHidden(hidden); d->reloadAndSignal(); Q_EMIT dataChanged(index, index, {KFilePlacesModel::HiddenRole}); } } void KFilePlacesModel::setGroupHidden(const GroupType type, bool hidden) { if (isGroupHidden(type) == hidden) { return; } d->bookmarkManager->root().setMetaDataItem(stateNameForGroupType(type), (hidden ? QStringLiteral("true") : QStringLiteral("false"))); d->reloadAndSignal(); Q_EMIT groupHiddenChanged(type, hidden); } bool KFilePlacesModel::movePlace(int itemRow, int row) { KBookmark afterBookmark; if ((itemRow < 0) || (itemRow >= d->items.count())) { return false; } if (row >= d->items.count()) { row = -1; } if (row == -1) { // The dropped item is moved or added to the last position KFilePlacesItem *lastItem = d->items.last(); afterBookmark = lastItem->bookmark(); } else { // The dropped item is moved or added before position 'row', ie after position 'row-1' if (row > 0) { KFilePlacesItem *afterItem = d->items[row - 1]; afterBookmark = afterItem->bookmark(); } } KFilePlacesItem *item = d->items[itemRow]; KBookmark bookmark = item->bookmark(); int destRow = row == -1 ? d->items.count() : row; // avoid move item away from its group destRow = d->findNearestPosition(itemRow, destRow); // The item is not moved when the drop indicator is on either item edge if (itemRow == destRow || itemRow + 1 == destRow) { return false; } beginMoveRows(QModelIndex(), itemRow, itemRow, QModelIndex(), destRow); d->bookmarkManager->root().moveBookmark(bookmark, afterBookmark); // Move item ourselves so that reloadBookmarks() does not consider // the move as a remove + insert. // // 2nd argument of QList::move() expects the final destination index, // but 'row' is the value of the destination index before the moved // item has been removed from its original position. That is why we // adjust if necessary. d->items.move(itemRow, itemRow < destRow ? (destRow - 1) : destRow); endMoveRows(); return true; } int KFilePlacesModel::hiddenCount() const { int rows = rowCount(); int hidden = 0; for (int i = 0; i < rows; ++i) { if (isHidden(index(i, 0))) { hidden++; } } return hidden; } QAction *KFilePlacesModel::teardownActionForIndex(const QModelIndex &index) const { Solid::Device device = deviceForIndex(index); QAction *action = nullptr; if (device.is() && device.as()->isAccessible()) { Solid::StorageDrive *drive = device.as(); if (drive == nullptr) { drive = device.parent().as(); } const bool teardownInProgress = deviceAccessibility(index) == KFilePlacesModel::TeardownInProgress; bool hotpluggable = false; bool removable = false; if (drive != nullptr) { hotpluggable = drive->isHotpluggable(); removable = drive->isRemovable(); } QString iconName; QString text; if (device.is()) { if (teardownInProgress) { text = i18nc("@action:inmenu", "Releasing…"); } else { text = i18nc("@action:inmenu", "&Release"); } } else if (removable || hotpluggable) { if (teardownInProgress) { text = i18nc("@action:inmenu", "Safely Removing…"); } else { text = i18nc("@action:inmenu", "&Safely Remove"); } iconName = QStringLiteral("media-eject"); } else { if (teardownInProgress) { text = i18nc("@action:inmenu", "Unmounting…"); } else { text = i18nc("@action:inmenu", "&Unmount"); } iconName = QStringLiteral("media-eject"); } if (!iconName.isEmpty()) { action = new QAction(QIcon::fromTheme(iconName), text, nullptr); } else { action = new QAction(text, nullptr); } if (teardownInProgress) { action->setEnabled(false); } } return action; } QAction *KFilePlacesModel::ejectActionForIndex(const QModelIndex &index) const { Solid::Device device = deviceForIndex(index); if (device.is()) { QString text = i18nc("@action:inmenu", "&Eject"); return new QAction(QIcon::fromTheme(QStringLiteral("media-eject")), text, nullptr); } return nullptr; } void KFilePlacesModel::requestTeardown(const QModelIndex &index) { Solid::Device device = deviceForIndex(index); Solid::StorageAccess *access = device.as(); if (access != nullptr) { d->teardownInProgress[access] = index; const QString filePath = access->filePath(); connect(access, &Solid::StorageAccess::teardownDone, this, [this, access, filePath](Solid::ErrorType error, QVariant errorData) { d->storageTeardownDone(filePath, error, errorData, access); }); access->teardown(); } } void KFilePlacesModel::requestEject(const QModelIndex &index) { Solid::Device device = deviceForIndex(index); Solid::OpticalDrive *drive = device.parent().as(); if (drive != nullptr) { d->teardownInProgress[drive] = index; QString filePath; Solid::StorageAccess *access = device.as(); if (access) { filePath = access->filePath(); } connect(drive, &Solid::OpticalDrive::ejectDone, this, [this, filePath, drive](Solid::ErrorType error, QVariant errorData) { d->storageTeardownDone(filePath, error, errorData, drive); }); drive->eject(); } else { QString label = data(index, Qt::DisplayRole).toString().replace(QLatin1Char('&'), QLatin1String("&&")); QString message = i18n("The device '%1' is not a disk and cannot be ejected.", label); Q_EMIT errorMessage(message); } } void KFilePlacesModel::requestSetup(const QModelIndex &index) { Solid::Device device = deviceForIndex(index); if (device.is() && !d->setupInProgress.contains(device.as()) && !device.as()->isAccessible()) { Solid::StorageAccess *access = device.as(); d->setupInProgress[access] = index; connect(access, &Solid::StorageAccess::setupDone, this, [this, access](Solid::ErrorType error, QVariant errorData) { d->storageSetupDone(error, errorData, access); }); access->setup(); } } void KFilePlacesModelPrivate::storageSetupDone(Solid::ErrorType error, const QVariant &errorData, Solid::StorageAccess *sender) { QPersistentModelIndex index = setupInProgress.take(sender); if (!index.isValid()) { return; } if (!error) { Q_EMIT q->setupDone(index, true); } else { if (errorData.isValid()) { Q_EMIT q->errorMessage(i18n("An error occurred while accessing '%1', the system responded: %2", q->text(index), errorData.toString())); } else { Q_EMIT q->errorMessage(i18n("An error occurred while accessing '%1'", q->text(index))); } Q_EMIT q->setupDone(index, false); } } void KFilePlacesModelPrivate::storageTeardownDone(const QString &filePath, Solid::ErrorType error, const QVariant &errorData, QObject *sender) { QPersistentModelIndex index = teardownInProgress.take(sender); if (!index.isValid()) { return; } if (error == Solid::ErrorType::DeviceBusy && !filePath.isEmpty()) { auto *listOpenFilesJob = new KListOpenFilesJob(filePath); QObject::connect(listOpenFilesJob, &KIO::Job::result, q, [this, index, error, errorData, listOpenFilesJob]() { const auto blockingProcesses = listOpenFilesJob->processInfoList(); QStringList blockingApps; blockingApps.reserve(blockingProcesses.count()); for (const auto &process : blockingProcesses) { blockingApps << process.name(); } Q_EMIT q->teardownDone(index, error, errorData); if (blockingProcesses.isEmpty()) { Q_EMIT q->errorMessage(i18n("One or more files on this device are open within an application.")); } else { blockingApps.removeDuplicates(); Q_EMIT q->errorMessage(xi18np("One or more files on this device are opened in application \"%2\".", "One or more files on this device are opened in following applications: %2.", blockingApps.count(), blockingApps.join(i18nc("separator in list of apps blocking device unmount", ", ")))); } }); listOpenFilesJob->start(); return; } Q_EMIT q->teardownDone(index, error, errorData); if (error != Solid::ErrorType::NoError && error != Solid::ErrorType::UserCanceled) { Q_EMIT q->errorMessage(errorData.toString()); } } void KFilePlacesModel::setSupportedSchemes(const QStringList &schemes) { d->supportedSchemes = schemes; d->reloadBookmarks(); Q_EMIT supportedSchemesChanged(); } QStringList KFilePlacesModel::supportedSchemes() const { return d->supportedSchemes; } namespace { QString partitionManagerPath() { static const QString path = QStandardPaths::findExecutable(QStringLiteral("partitionmanager")); return path; } } // namespace QAction *KFilePlacesModel::partitionActionForIndex(const QModelIndex &index) const { const auto device = deviceForIndex(index); if (!device.is()) { return nullptr; } // Not using kservice to find partitionmanager because we need to manually invoke it so we can pass the --device argument. if (partitionManagerPath().isEmpty()) { return nullptr; } auto action = new QAction(QIcon::fromTheme(QStringLiteral("partitionmanager")), i18nc("@action:inmenu", "Reformat or Edit with Partition Manager"), nullptr); connect(action, &QAction::triggered, this, [device] { const auto block = device.as(); auto job = new KIO::CommandLauncherJob(partitionManagerPath(), {QStringLiteral("--device"), block->device()}); job->start(); }); return action; } #include "moc_kfileplacesmodel.cpp"