diff --git a/src/service/plugins/CMakeLists.txt b/src/service/plugins/CMakeLists.txt --- a/src/service/plugins/CMakeLists.txt +++ b/src/service/plugins/CMakeLists.txt @@ -6,4 +6,5 @@ add_subdirectory (virtualdesktopswitch) add_subdirectory (globalshortcuts) add_subdirectory (eventspy) +add_subdirectory (gtk-eventspy) add_subdirectory (runapplication) diff --git a/src/service/plugins/gtk-eventspy/CMakeLists.txt b/src/service/plugins/gtk-eventspy/CMakeLists.txt new file mode 100644 --- /dev/null +++ b/src/service/plugins/gtk-eventspy/CMakeLists.txt @@ -0,0 +1,40 @@ +# vim:set softtabstop=3 shiftwidth=3 tabstop=3 expandtab: + +project (activitymanager-gtk-eventspy) + +find_package (KF5KIO ${KF5_MIN_VERSION} CONFIG REQUIRED) + +set ( + gtkevenyspy_SRCS + GtkEventSpy.cpp + ) + +ecm_qt_declare_logging_category(gtkevenyspy_SRCS + HEADER DebugPluginGtkEventSpy.h + IDENTIFIER KAMD_PLUGIN_GTK_EVENTSPY + CATEGORY_NAME org.kde.kactivities.plugin.gtk-eventspy + DEFAULT_SEVERITY Warning) + +kcoreaddons_add_plugin( + kactivitymanagerd_plugin_gtk_eventspy + JSON kactivitymanagerd-plugin-gtk-eventspy.json + SOURCES ${gtkevenyspy_SRCS} + INSTALL_NAMESPACE ${KAMD_PLUGIN_DIR} + ) + +target_link_libraries ( + kactivitymanagerd_plugin_gtk_eventspy + Qt5::Core + Qt5::DBus + Qt5::Xml + KF5::ConfigCore + KF5::CoreAddons + KF5::KIOCore + KF5::Service + kactivitymanagerd_plugin + ) + +set_target_properties ( + kactivitymanagerd_plugin_gtk_eventspy + PROPERTIES PREFIX "" + ) diff --git a/src/service/plugins/gtk-eventspy/GtkEventSpy.h b/src/service/plugins/gtk-eventspy/GtkEventSpy.h new file mode 100644 --- /dev/null +++ b/src/service/plugins/gtk-eventspy/GtkEventSpy.h @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2019 Méven Car (meven.car@kdemail.net) + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef PLUGINS_EVENT_SPY_PLUGIN_H +#define PLUGINS_EVENT_SPY_PLUGIN_H + +#include +#include + +#include + +class KDirWatch; + +class GtkEventSpyPlugin : public Plugin { + Q_OBJECT + +public: + explicit GtkEventSpyPlugin(QObject *parent = nullptr, const QVariantList &args = QVariantList()); + ~GtkEventSpyPlugin() override; + + bool init(QHash &modules) override; + +private Q_SLOTS: + void fileUpdated(const QString &file); + void addDocument(const QUrl &url, const QString &application); + +private: + QObject *m_resources; + std::unique_ptr m_dirWatcher; + QDateTime m_lastUpdate; +}; + +#endif // PLUGINS_EVENT_SPY_PLUGIN_H diff --git a/src/service/plugins/gtk-eventspy/GtkEventSpy.cpp b/src/service/plugins/gtk-eventspy/GtkEventSpy.cpp new file mode 100644 --- /dev/null +++ b/src/service/plugins/gtk-eventspy/GtkEventSpy.cpp @@ -0,0 +1,230 @@ +/* + * Copyright (C) 2019 Méven Car (meven.car@kdemail.net) + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "GtkEventSpy.h" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "DebugPluginGtkEventSpy.h" + +KAMD_EXPORT_PLUGIN(GtkEventSpyPlugin, GtkEventSpyPlugin, "kactivitymanagerd-plugin-gtk-eventspy.json") + +GtkEventSpyPlugin::GtkEventSpyPlugin(QObject *parent, const QVariantList &args) + : Plugin(parent) + , m_resources(nullptr) + , m_dirWatcher(new KDirWatch(this)) + , m_lastUpdate(QDateTime::currentDateTime()) +{ + Q_UNUSED(args); + + // gtk xml history file + // usually $HOME/.local/share/recently-used.xbel + QString filename = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1String("/recently-used.xbel"); + + m_dirWatcher->addFile(filename); + connect(m_dirWatcher.get(), &KDirWatch::dirty, + this, &GtkEventSpyPlugin::fileUpdated); + connect(m_dirWatcher.get(), &KDirWatch::created, + this, &GtkEventSpyPlugin::fileUpdated); +} + +struct Application { + QString name; + QDateTime modified; +}; + +class Bookmark { + +public: + QUrl href; + QDateTime added; + QDateTime modified; + QDateTime visited; + QList *applications; + + Bookmark(){ + applications = new QList(); + } + QString latestApplication() const; +}; + +QString Bookmark::latestApplication() const { + Application current = applications->first(); + for (const Application &app : qAsConst(*applications)) { + if (app.modified > current.modified) { + current = app; + } + } + return current.name; +} + +class BookmarkHandler : public QXmlDefaultHandler +{ +public: + BookmarkHandler(){ + current = nullptr; + marks = QList(); + } + + bool startElement(const QString &namespaceURI, const QString &localName, + const QString &qName, const QXmlAttributes &attributes) override; + bool endElement(const QString &namespaceURI, const QString &localName, const QString &qName) override; + + QList bookmarks() const; +private: + QList marks; + Bookmark* current; +}; + +QList BookmarkHandler::bookmarks() const { + return marks; +} + +bool BookmarkHandler::startElement(const QString &/*namespaceURI*/, const QString &/*localName*/, + const QString &qName, const QXmlAttributes &attributes) +{ + // new bookmark + if (qName == QStringLiteral("bookmark")) { + current = new Bookmark(); + current->href = QUrl(attributes.value("href")); + QString added = attributes.value("added"); + QString modified = attributes.value("modified"); + QString visited = attributes.value("visited"); + current->added = QDateTime::fromString(added, Qt::ISODate); + current->modified = QDateTime::fromString(modified, Qt::ISODate); + current->visited = QDateTime::fromString(visited, Qt::ISODate); + + // application for the current bookmark + } else if (qName == QStringLiteral("bookmark:application")) { + Application app; + + QString exec = attributes.value("exec"); + + if (exec.at(0) == QStringLiteral("'") && exec.at(exec.size() - 1) == QStringLiteral("'")) { + // remove "'" caracters wrapping the command + exec = exec.mid(1, exec.size() -2 ); + } + + // Search for applications which are executable and case-insensitively match the search term + // See https://techbase.kde.org/Development/Tutorials/Services/Traders#The_KTrader_Query_Language + const auto query = QString("exist Exec and Exec ~~ '%1'").arg(exec); + const KService::List services = KServiceTypeTrader::self()->query(QStringLiteral("Application"), query); + + if (!services.isEmpty()) { + // use the first item matching + const auto service = services.first(); + app.name = service->desktopEntryName(); + } else { + // when no services are found, sanitize a little the exec + // remove space and any caracter after + const int spaceIdex = exec.indexOf(" "); + if (spaceIdex != -1) { + exec = exec.mid(0, spaceIdex); + } + app.name = exec; + } + + app.modified = QDateTime::fromString(attributes.value("modified"), Qt::ISODate); + + current->applications->append(app); + } + return true; +} + +bool BookmarkHandler::endElement(const QString &namespaceURI, const QString &localName, const QString &qName) { + Q_UNUSED(namespaceURI); + Q_UNUSED(localName); + + if (qName == QStringLiteral("bookmark")) { + // keep track of the finished parsed bookmark + marks << *current; + } + + return true; +} + +void GtkEventSpyPlugin::fileUpdated(const QString &filename) +{ + QFile file(filename); + if ( ! file.open(QFile::ReadOnly | QFile::Text) ) { + qCWarning(KAMD_PLUGIN_GTK_EVENTSPY) << "Could not read" << filename; + return; + } + + // must parse the xbel xml file + BookmarkHandler bookmarkHandler; + + QXmlSimpleReader reader; + reader.setContentHandler(&bookmarkHandler); + reader.setErrorHandler(&bookmarkHandler); + QXmlInputSource *source = new QXmlInputSource(&file); + + if ( ! reader.parse(source) ) { + qCWarning(KAMD_PLUGIN_GTK_EVENTSPY) << "could not parse" << file << "error was " << bookmarkHandler.errorString(); + return ; + } + + // then find the files that were accessed since last run + const QList bookmarks = bookmarkHandler.bookmarks(); + for (const Bookmark &mark : bookmarks) { + if (mark.added > m_lastUpdate || mark.modified > m_lastUpdate || mark.visited > m_lastUpdate) { + addDocument(mark.href, mark.latestApplication()); + } + } + + m_lastUpdate = QDateTime::currentDateTime(); +} + +void GtkEventSpyPlugin::addDocument(const QUrl &url, const QString &application) +{ + const QString name = url.fileName(); + + Plugin::invoke( + m_resources, "RegisterResourceEvent", + Q_ARG(QString, application), // Application + Q_ARG(uint, 0), // Window ID + Q_ARG(QString, url.toString()), // URI + Q_ARG(uint, 0) // Event Activities::Accessed + ); +} + +GtkEventSpyPlugin::~GtkEventSpyPlugin() +{ +} + +bool GtkEventSpyPlugin::init(QHash &modules) +{ + Plugin::init(modules); + + m_resources = modules["resources"]; + + return true; +} + +#include "GtkEventSpy.moc" + diff --git a/src/service/plugins/gtk-eventspy/kactivitymanagerd-plugin-gtk-eventspy.json b/src/service/plugins/gtk-eventspy/kactivitymanagerd-plugin-gtk-eventspy.json new file mode 100644 --- /dev/null +++ b/src/service/plugins/gtk-eventspy/kactivitymanagerd-plugin-gtk-eventspy.json @@ -0,0 +1,58 @@ +{ + "KPlugin": { + "Authors": [ + { + "Email": "meven.car(at)kdemail.net", + "Name": "Méven Car", + "Name[ca@valencia]": "Méven Car", + "Name[ca]": "Méven Car", + "Name[cs]": "Méven Car", + "Name[da]": "Méven Car", + "Name[de]": "Méven Car", + "Name[el]": "Méven Car", + "Name[en_GB]": "Méven Car", + "Name[es]": "Méven Car", + "Name[et]": "Méven Car", + "Name[eu]": "Méven Car", + "Name[fi]": "Méven Car", + "Name[fr]": "Méven Car", + "Name[gl]": "Méven Car", + "Name[hu]": "Méven Car", + "Name[ia]": "Méven Car", + "Name[id]": "Méven Car", + "Name[it]": "Méven Car", + "Name[ko]": "Méven Car", + "Name[lt]": "Méven Car", + "Name[nl]": "Méven Car", + "Name[nn]": "Méven Car", + "Name[pl]": "Méven Car", + "Name[pt]": "Méven Car", + "Name[pt_BR]": "Méven Car", + "Name[ru]": "Méven Car", + "Name[sk]": "Méven Car", + "Name[sl]": "Méven Car", + "Name[sr@ijekavianlatin]": "Méven Car", + "Name[sr@latin]": "Méven Car", + "Name[sv]": "Méven Car", + "Name[tr]": "Méven Car", + "Name[uk]": "Méven Car", + "Name[x-test]": "xxMéven Carxx", + "Name[zh_CN]": "Méven Car", + "Name[zh_TW]": "Méven Car" + } + ], + "Category": "", + "Dependencies": [], + "Description": "Collects events from applications that use GtkFileChooser and GtkRecentManager as specified by https://www.freedesktop.org/wiki/Specifications/desktop-bookmark-spec/", + "EnabledByDefault": true, + "Icon": "preferences-system", + "Id": "org.kde.ActivityManager.GtkEventSpy", + "License": "GPL", + "Name": "Gtk Event Spy", + "ServiceTypes": [ + "ActivityManager/Plugin" + ], + "Version": "1.0", + "Website": "http://plasma.kde.org/" + } +}