cf12defd28
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
1574 lines
55 KiB
C++
1574 lines
55 KiB
C++
/*
|
|
This file is part of the KDE project
|
|
SPDX-FileCopyrightText: 2007 Kevin Ottens <ervin@kde.org>
|
|
SPDX-FileCopyrightText: 2007 David Faure <faure@kde.org>
|
|
SPDX-FileCopyrightText: 2023 Harald Sitter <sitter@kde.org>
|
|
|
|
SPDX-License-Identifier: LGPL-2.0-only
|
|
*/
|
|
|
|
#include "kfileplacesmodel.h"
|
|
#include "kfileplacesitem_p.h"
|
|
#include "kfileplacesmodel_p.h"
|
|
|
|
#include <KCoreDirLister>
|
|
#include <KLazyLocalizedString>
|
|
#include <KListOpenFilesJob>
|
|
#include <KLocalizedString>
|
|
#include <commandlauncherjob.h>
|
|
#include <kfileitem.h>
|
|
#include <kio/statjob.h>
|
|
#include <kprotocolinfo.h>
|
|
|
|
#include <KBookmarkManager>
|
|
#include <KConfig>
|
|
#include <KConfigGroup>
|
|
#include <KUrlMimeData>
|
|
|
|
#include <solid/block.h>
|
|
#include <solid/devicenotifier.h>
|
|
#include <solid/opticaldisc.h>
|
|
#include <solid/opticaldrive.h>
|
|
#include <solid/portablemediaplayer.h>
|
|
#include <solid/predicate.h>
|
|
#include <solid/storageaccess.h>
|
|
#include <solid/storagedrive.h>
|
|
#include <solid/storagevolume.h>
|
|
|
|
#include <QAction>
|
|
#include <QCoreApplication>
|
|
#include <QDebug>
|
|
#include <QDir>
|
|
#include <QFile>
|
|
#include <QMimeData>
|
|
#include <QMimeDatabase>
|
|
#include <QStandardPaths>
|
|
#include <QTimer>
|
|
|
|
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<QUrl> 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<qptrdiff>(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<QUrl> 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<QIcon>();
|
|
}
|
|
|
|
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<KFilePlacesItem *>(index.internalPointer());
|
|
return isGroupHidden(item->groupType());
|
|
}
|
|
|
|
bool KFilePlacesModel::isDevice(const QModelIndex &index) const
|
|
{
|
|
if (!index.isValid()) {
|
|
return false;
|
|
}
|
|
|
|
KFilePlacesItem *item = static_cast<KFilePlacesItem *>(index.internalPointer());
|
|
|
|
return item->isDevice();
|
|
}
|
|
|
|
bool KFilePlacesModel::isTeardownAllowed(const QModelIndex &index) const
|
|
{
|
|
if (!index.isValid()) {
|
|
return false;
|
|
}
|
|
|
|
KFilePlacesItem *item = static_cast<KFilePlacesItem *>(index.internalPointer());
|
|
return item->isTeardownAllowed();
|
|
}
|
|
|
|
bool KFilePlacesModel::isEjectAllowed(const QModelIndex &index) const
|
|
{
|
|
if (!index.isValid()) {
|
|
return false;
|
|
}
|
|
|
|
KFilePlacesItem *item = static_cast<KFilePlacesItem *>(index.internalPointer());
|
|
return item->isEjectAllowed();
|
|
}
|
|
|
|
bool KFilePlacesModel::isTeardownOverlayRecommended(const QModelIndex &index) const
|
|
{
|
|
if (!index.isValid()) {
|
|
return false;
|
|
}
|
|
|
|
KFilePlacesItem *item = static_cast<KFilePlacesItem *>(index.internalPointer());
|
|
return item->isTeardownOverlayRecommended();
|
|
}
|
|
|
|
KFilePlacesModel::DeviceAccessibility KFilePlacesModel::deviceAccessibility(const QModelIndex &index) const
|
|
{
|
|
if (!index.isValid()) {
|
|
return KFilePlacesModel::Accessible;
|
|
}
|
|
|
|
KFilePlacesItem *item = static_cast<KFilePlacesItem *>(index.internalPointer());
|
|
return item->deviceAccessibility();
|
|
}
|
|
|
|
Solid::Device KFilePlacesModel::deviceForIndex(const QModelIndex &index) const
|
|
{
|
|
if (!index.isValid()) {
|
|
return Solid::Device();
|
|
}
|
|
|
|
KFilePlacesItem *item = static_cast<KFilePlacesItem *>(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<KFilePlacesItem *>(index.internalPointer());
|
|
return item->bookmark();
|
|
}
|
|
|
|
KFilePlacesModel::GroupType KFilePlacesModel::groupType(const QModelIndex &index) const
|
|
{
|
|
if (!index.isValid()) {
|
|
return UnknownType;
|
|
}
|
|
|
|
KFilePlacesItem *item = static_cast<KFilePlacesItem *>(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<KFilePlacesItem *>(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<int, QByteArray> 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<int> &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<KFilePlacesItem *> currentItems = loadBookmarkList();
|
|
|
|
QList<KFilePlacesItem *>::Iterator it_i = items.begin();
|
|
QList<KFilePlacesItem *>::Iterator it_c = currentItems.begin();
|
|
|
|
QList<KFilePlacesItem *>::Iterator end_i = items.end();
|
|
QList<KFilePlacesItem *>::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<KFilePlacesItem *> KFilePlacesModelPrivate::loadBookmarkList()
|
|
{
|
|
QList<KFilePlacesItem *> items;
|
|
|
|
KBookmarkGroup root = bookmarkManager->root();
|
|
KBookmark bookmark = root.first();
|
|
QList<Solid::Device> devices{availableDevices};
|
|
QList<QString> 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<Solid::StorageVolume>();
|
|
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<int> &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<int> &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<int> &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<QUrl> 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<QUrl> 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<KFilePlacesItem *>(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<KFilePlacesItem *>(index.internalPointer());
|
|
|
|
if (item->isDevice()) {
|
|
return;
|
|
}
|
|
|
|
KBookmark bookmark = item->bookmark();
|
|
|
|
if (bookmark.isNull()) {
|
|
return;
|
|
}
|
|
|
|
QList<int> 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<KFilePlacesItem *>(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<KFilePlacesItem *>(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<Solid::StorageAccess>() && device.as<Solid::StorageAccess>()->isAccessible()) {
|
|
Solid::StorageDrive *drive = device.as<Solid::StorageDrive>();
|
|
|
|
if (drive == nullptr) {
|
|
drive = device.parent().as<Solid::StorageDrive>();
|
|
}
|
|
|
|
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<Solid::OpticalDisc>()) {
|
|
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<Solid::OpticalDisc>()) {
|
|
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<Solid::StorageAccess>();
|
|
|
|
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<Solid::OpticalDrive>();
|
|
|
|
if (drive != nullptr) {
|
|
d->teardownInProgress[drive] = index;
|
|
|
|
QString filePath;
|
|
Solid::StorageAccess *access = device.as<Solid::StorageAccess>();
|
|
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<Solid::StorageAccess>() && !d->setupInProgress.contains(device.as<Solid::StorageAccess>())
|
|
&& !device.as<Solid::StorageAccess>()->isAccessible()) {
|
|
Solid::StorageAccess *access = device.as<Solid::StorageAccess>();
|
|
|
|
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 <application>\"%2\"</application>.",
|
|
"One or more files on this device are opened in following applications: <application>%2</application>.",
|
|
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<Solid::Block>()) {
|
|
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<Solid::Block>();
|
|
auto job = new KIO::CommandLauncherJob(partitionManagerPath(), {QStringLiteral("--device"), block->device()});
|
|
job->start();
|
|
});
|
|
return action;
|
|
}
|
|
|
|
#include "moc_kfileplacesmodel.cpp"
|