diff --git a/CMakeLists.txt b/CMakeLists.txt --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -65,6 +65,12 @@ TYPE OPTIONAL ) +find_package(KF5ActivitiesStats QUIET) +set_package_properties(KF5ActivitiesStats PROPERTIES + PURPOSE "Provides the recentlyused:/ kioslave." + TYPE OPTIONAL +) + find_package(Phonon4Qt5 4.6.60 NO_MODULE) set_package_properties(Phonon4Qt5 PROPERTIES DESCRIPTION "Qt-based audio library" @@ -143,6 +149,9 @@ if(KF5Activities_FOUND) add_subdirectory( activities ) endif() +if(KF5ActivitiesStats_FOUND) + add_subdirectory( recentlyused ) +endif() add_subdirectory( bookmarks ) add_subdirectory( filter ) if(Phonon4Qt5_FOUND) diff --git a/kio-extras.categories b/kio-extras.categories --- a/kio-extras.categories +++ b/kio-extras.categories @@ -10,3 +10,4 @@ log_kio_sftp.trace kioslave (sftp trace) IDENTIFIER [KIO_SFTP_TRACE_LOG] log_kio_archive kioslave (archive) IDENTIFIER [KIO_ARCHIVE_LOG] log_kio_man kioslave (man) IDENTIFIER [KIO_MAN_LOG] +log_kio_recentlyused kioslave (recentlyused) IDENTIFIER [KIO_RECENTLYUSED_LOG] diff --git a/recentlyused/CMakeLists.txt b/recentlyused/CMakeLists.txt new file mode 100644 --- /dev/null +++ b/recentlyused/CMakeLists.txt @@ -0,0 +1,29 @@ +add_definitions(-DTRANSLATION_DOMAIN=\"kio5_recentlyused\") + +find_package(KF5 ${KF5_MIN_VERSION} REQUIRED COMPONENTS + KIO + I18n + Activities + ActivitiesStats +) + +set( + kio_recentlyused_SRCS + + recentlyused.cpp +) + +ecm_qt_declare_logging_category(kio_recentlyused_SRCS + HEADER recentlyused-logsettings.h + IDENTIFIER KIO_RECENTLYUSED_LOG +CATEGORY_NAME log_kio_recentlyused) + +add_library(recentlyused MODULE ${kio_recentlyused_SRCS}) + +target_link_libraries(recentlyused + KF5::KIOCore + KF5::I18n + KF5::Activities + KF5::ActivitiesStats) +set_target_properties(recentlyused PROPERTIES OUTPUT_NAME "recentlyused") +install(TARGETS recentlyused DESTINATION ${KDE_INSTALL_PLUGINDIR}/kf5/kio) diff --git a/recentlyused/recentlyused.h b/recentlyused/recentlyused.h new file mode 100644 --- /dev/null +++ b/recentlyused/recentlyused.h @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2019 Méven Car + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) version 3, or any + * later version accepted by the membership of KDE e.V. (or its + * successor approved by the membership of KDE e.V.), which shall + * act as a proxy defined in Section 6 of version 3 of the license. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. + * If not, see . + */ + +#ifndef RECENTLYUSED_H +#define RECENTLYUSED_H + +#include +#include + +/** + * Implements recentlyused:/ ioslave + * It uses KAcitivitiesStats as a backend (as kicoff/kicker do). + * + * It supports options to filter what is returned through url parameters: + * + * ?activity=[activity UUID|any] + * + * Allows to filter the resources based on the activity there were used in. + * Defaults to the current user activity. + * Any option means include resources from any activity. + * Example: recentlyused:/?activity=428fa590-1920-4b3c-a7e1-1842e6164707 + * + * ?agent=[agentName1[,agentName*]*|current] + * Filter on the name or names of the application that used the resource. + * Current option filters on the current application + * Default to any, meaning no agent filtering + * Example: recentlyused:/?agent=kate,dolphin + * + * ?url=path + * + * Filters resourcess based on the url of the resource, can cantain schemes. + * Path can contain '*', defaults to no filtering. + * Example: recentlyused:/?url=/home/meven/projects/* + * + * ?type=mimetype[,mimetype]* + * + * Filters resources based on the mimetype of files. + * Defaults to no filtering + * Example: recentlyused:/?type=video/*,audio/* + * + * ?limit=number + * + * Specify the number of resources to return. + * Defaults to 30 + * + * ?order=(HighScoredFirst|RecentlyCreatedFirst|OrderByUrl|OrderByTitle) + * Allow to modify the order of the returned resources. + * Defaults to RecentlyUsedFirst + * See KActivities::Stats::Terms::Order + * + * Examples: + * + * - recentlyused:/?type=video/*,audio/*&order=HighScoredFirst : recently used video or audio files ordered by their scoring descending + * - recentlyused:/?url=/home/meven/kde/src/*&type=text/plain : recently used text files located in a subdir of /home/meven/kde/src/ + * + * @brief The RecentlyUsed implements an ioslave to access recently used resources + */ +class RecentlyUsed: public KIO::ForwardingSlaveBase +{ +public: + RecentlyUsed(const QByteArray &pool, const QByteArray &app); + ~RecentlyUsed() override; + +protected: + bool rewriteUrl(const QUrl &url, QUrl &newUrl) override; + void listDir(const QUrl &url) override; + void stat(const QUrl& url) override; + void mimetype(const QUrl& url) override; + // void del(const QUrl& url, bool isfile) override; + +private: + + KIO::UDSEntry udsEntryFromResource(const QString& resource); +}; + +#endif diff --git a/recentlyused/recentlyused.cpp b/recentlyused/recentlyused.cpp new file mode 100644 --- /dev/null +++ b/recentlyused/recentlyused.cpp @@ -0,0 +1,272 @@ +/* + * Copyright (C) 2019 Méven Car + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) version 3, or any + * later version accepted by the membership of KDE e.V. (or its + * successor approved by the membership of KDE e.V.), which shall + * act as a proxy defined in Section 6 of version 3 of the license. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. + * If not, see . + */ + +#include "recentlyused-logsettings.h" +#include "recentlyused.h" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + + +#include + +#include +#include +#include + +namespace KAStats = KActivities::Stats; + +using namespace KAStats; +using namespace KAStats::Terms; + +// Pseudo plugin class to embed meta data +class KIOPluginForMetaData : public QObject +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID "org.kde.kio.slave.recentlyused" FILE "recentlyused.json") +}; + +extern "C" int Q_DECL_EXPORT kdemain(int argc, char **argv) +{ + // necessary to use other kio slaves + QCoreApplication app(argc, argv); + app.setApplicationName(QStringLiteral("kio_recentlyused")); + if (argc != 4) { + fprintf(stderr, "Usage: kio_recentlyused protocol domain-socket1 domain-socket2\n"); + exit(-1); + } + // start the slave + RecentlyUsed slave(argv[2], argv[3]); + slave.dispatchLoop(); + return 0; +} + +bool isRootUrl(const QUrl &url) +{ + const QString path = url.adjusted(QUrl::StripTrailingSlash).path(); + return (path.isEmpty() || path == QLatin1String("/")); +} + + +RecentlyUsed::RecentlyUsed(const QByteArray& pool, const QByteArray& app): + ForwardingSlaveBase("recentlyused", pool, app) +{} + +RecentlyUsed::~RecentlyUsed() +{} + + +bool RecentlyUsed::rewriteUrl(const QUrl& url, QUrl& newUrl) +{ + Q_UNUSED(url); + Q_UNUSED(newUrl); + return false; +} + +ResultModel* runQuery(const QUrl& url) { + qCDebug(KIO_RECENTLYUSED_LOG) << "runQuery for url" << url.toString(); + + auto query = UsedResources + | Limit(30); + + // Parse url query parameter + auto urlQuery = QUrlQuery(url); + + // handles type aka mimetype + if (urlQuery.hasQueryItem(QStringLiteral("type"))) { + auto typeValue = urlQuery.queryItemValue(QStringLiteral("type")); + if (typeValue.contains(QLatin1Char(','))) { + // multiple mimetypes were passed + query = query | Type(typeValue.split(QLatin1Char(','))); + } else { + query = query | Type(typeValue); + } + } + + // limit parameter + if (urlQuery.hasQueryItem(QStringLiteral("limit"))) { + auto limitValue = urlQuery.queryItemValue(QStringLiteral("limit")); + bool parseOk; + auto limitInt = limitValue.toInt(&parseOk); + if (parseOk) { + query = query | Limit(limitInt); + } + } + + // acitivity parameter, filter using the uuid of the activity + if (urlQuery.hasQueryItem(QStringLiteral("activity"))) { + auto activityValue = urlQuery.queryItemValue(QStringLiteral("activity")); + if (activityValue == QStringLiteral("any")) { + query = query | Activity::any(); + } else { + query = query | Activity(activityValue); + } + } else { + query = query | Activity::current(); + } + + // agent parameter, filter using the application name that used the resource + if (urlQuery.hasQueryItem(QStringLiteral("agent"))) { + auto agentValue = urlQuery.queryItemValue(QStringLiteral("agent")); + if (agentValue == QStringLiteral("current")) { + query = query | Agent::current(); + }else if (agentValue.contains(QLatin1Char(','))) { + query = query | Agent(agentValue.split(QLatin1Char(','))); + } else { + query = query | Agent(agentValue); + } + } else { + query = query | Agent::any(); + } + + // url parameter for exact path match or folders, supports wildcard pattern matching + if (urlQuery.hasQueryItem(QStringLiteral("url"))) { + query = query | Url(urlQuery.queryItemValue(QStringLiteral("url"))); + } else { + query = query | Url::file(); + } + + // see KActivities::Stats::Terms::Order + if (urlQuery.hasQueryItem(QStringLiteral("order"))) { + auto orderValue = urlQuery.queryItemValue(QStringLiteral("order")); + + if (orderValue == QStringLiteral("HighScoredFirst")) { + query = query | Order::HighScoredFirst; + } else if (orderValue == QStringLiteral("RecentlyCreatedFirst")) { + query = query | Order::RecentlyCreatedFirst; + } else if (orderValue == QStringLiteral("OrderByUrl")) { + query = query | Order::OrderByUrl; + } else if (orderValue == QStringLiteral("OrderByTitle")) { + query = query | Order::OrderByTitle; + } else { + query = query | Order::RecentlyUsedFirst; + } + } else { + query = query | Order::RecentlyUsedFirst; + } + + return new ResultModel(query); +} + +KIO::UDSEntry RecentlyUsed::udsEntryFromResource(const QString& resource){ + + // the query only returns files and folders + QUrl resourceUrl = QUrl::fromLocalFile(resource); + + KIO::UDSEntry uds; + KIO::StatJob* job = KIO::stat(resourceUrl, KIO::HideProgressInfo); + + // we do not want to wait for the event loop to delete the job + QScopedPointer sp(job); + job->setAutoDelete(false); + if (job->exec()) { + uds = job->statResult(); + } + uds.fastInsert(KIO::UDSEntry::UDS_URL, resourceUrl.toString()); + return uds; +} + +void RecentlyUsed::listDir(const QUrl& url) +{ + if (!isRootUrl(url)) { + error(KIO::ERR_DOES_NOT_EXIST, url.toDisplayString()); + return; + } + + auto model = runQuery(url); + + KIO::UDSEntryList udslist; + udslist.reserve(model->rowCount()); + + for(int r = 0; r < model->rowCount(); ++r) { + QModelIndex index = model->index(r, 0); + QString resource = model->data(index, ResultModel::ResourceRole).toString(); + + udslist << udsEntryFromResource(resource); + } + + listEntries(udslist); + finished(); +} + +void RecentlyUsed::stat(const QUrl& url) +{ + qCDebug(KIO_RECENTLYUSED_LOG) << "stating" << " " << url; + + if (isRootUrl(url)) { + // + // stat the root path + // + + QString dirName = i18n("Recent Documents"); + KIO::UDSEntry uds; + uds.reserve(5); + uds.fastInsert(KIO::UDSEntry::UDS_NAME, dirName); + uds.fastInsert(KIO::UDSEntry::UDS_DISPLAY_NAME, dirName); + uds.fastInsert(KIO::UDSEntry::UDS_DISPLAY_TYPE, dirName); + uds.fastInsert(KIO::UDSEntry::UDS_ICON_NAME, QStringLiteral("document-open-recent")); + uds.fastInsert(KIO::UDSEntry::UDS_FILE_TYPE, S_IFDIR); + uds.fastInsert(KIO::UDSEntry::UDS_MIME_TYPE, QStringLiteral("inode/directory")); + + statEntry(uds); + finished(); + } else { + auto model = runQuery(url); + if (model->rowCount() != 1) { + error(KIO::ERR_DOES_NOT_EXIST, url.toDisplayString()); + return ; + } + + QModelIndex index = model->index(0, 0); + QString resource = model->data(index, ResultModel::ResourceRole).toString(); + statEntry(udsEntryFromResource(resource)); + + finished(); + } +} + +void RecentlyUsed::mimetype(const QUrl& url) +{ + // the root url is always a folder + if (isRootUrl(url)) { + mimeType(QString::fromLatin1("inode/directory")); + finished(); + } + // others urls are forwarded + else { + ForwardingSlaveBase::mimetype(url); + } +} + +// needed for JSON file embedding +#include "recentlyused.moc" diff --git a/recentlyused/recentlyused.json b/recentlyused/recentlyused.json new file mode 100644 --- /dev/null +++ b/recentlyused/recentlyused.json @@ -0,0 +1,33 @@ +{ + "KDE-KIO-Protocols": { + "ftp": { + "Class": ":local", + "Icon": "document-open-recent", + "X-DocPath": "kioslave5/recentlyused/index.html", + "copyFromFile": false, + "copyToFile": false, + "deleting": false, + "exec": "kf5/kio/recentlyused", + "input": "none", + "listing": [ + "Name", + "Type", + "Size", + "Date", + "Access", + "Owner", + "Group", + "Link" + ], + "linking": false, + "makedir": false, + "maxInstances": 1, + "maxInstancesPerHost": 1, + "moving": false, + "output": "filesystem", + "protocol": "recentlyused", + "reading": false, + "writing": false + } + } +}