diff --git a/kactivitymanagerd.categories b/kactivitymanagerd.categories
--- a/kactivitymanagerd.categories
+++ b/kactivitymanagerd.categories
@@ -1,3 +1,4 @@
org.kde.kactivities.activities KActivities Activities DEFAULT_SEVERITY [WARNING] IDENTIFIER [KAMD_LOG_ACTIVITIES]
org.kde.kactivities.application KActivities Application DEFAULT_SEVERITY [WARNING] IDENTIFIER [KAMD_LOG_APPLICATION]
org.kde.kactivities.resources KActivities Resources DEFAULT_SEVERITY [WARNING] IDENTIFIER [KAMD_LOG_RESOURCES]
+org.kde.kactivities.plugin.gtk-eventspy KActivities Gtk Event Spy Resources DEFAULT_SEVERITY [WARNING] IDENTIFIER [KAMD_LOG_PLUGIN_GTK_EVENTSPY]
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,37 @@
+# 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_LOG_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::Xml
+ KF5::CoreAddons
+ 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,49 @@
+/*
+ * 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 "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;
+
+ QString latestApplication() const;
+};
+
+QString Bookmark::latestApplication() const
+{
+ Application current = applications.first();
+ for (const Application &app : applications) {
+ if (app.modified > current.modified) {
+ current = app;
+ }
+ }
+ return current.name;
+}
+
+class BookmarkHandler : public QXmlDefaultHandler
+{
+public:
+
+ 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 = 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.startsWith(QLatin1Char('\'')) && exec.endsWith(QLatin1Char('\''))) {
+ // 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 spaceIndex = exec.indexOf(" ");
+ if (spaceIndex != -1) {
+ exec = exec.mid(0, spaceIndex);
+ }
+ 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_LOG_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(&file);
+
+ if (!reader.parse(source)) {
+ qCWarning(KAMD_LOG_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/"
+ }
+}