diff options
author | Fabien Proriol <fabien.proriol@kazoe.org> | 2025-05-25 12:13:31 +0200 |
---|---|---|
committer | Fabien Proriol <fabien.proriol@kazoe.org> | 2025-05-25 12:13:31 +0200 |
commit | 1dbc0e3c88ba271ba35bc3f82e7864c4f35e1236 (patch) | |
tree | 8c491cd196e2eff4c59f8c23f566f7ff26981586 /src/lib |
Initial Commit
Diffstat (limited to 'src/lib')
30 files changed, 2698 insertions, 0 deletions
diff --git a/src/lib/debug.cpp b/src/lib/debug.cpp new file mode 100644 index 0000000..565bdbc --- /dev/null +++ b/src/lib/debug.cpp @@ -0,0 +1,4 @@ +#include "debug.h" + +Q_LOGGING_CATEGORY(AUTO_START, "xdg.auto_start") +Q_LOGGING_CATEGORY(SYSTEM_TRAY, "xdg.system_tray") diff --git a/src/lib/debug.h b/src/lib/debug.h new file mode 100644 index 0000000..978b470 --- /dev/null +++ b/src/lib/debug.h @@ -0,0 +1,9 @@ +#ifndef DEBUG_H +#define DEBUG_H + +#include <QLoggingCategory> + +Q_DECLARE_LOGGING_CATEGORY(AUTO_START) +Q_DECLARE_LOGGING_CATEGORY(SYSTEM_TRAY) + +#endif // DEBUG_H diff --git a/src/lib/xdgautostart.cpp b/src/lib/xdgautostart.cpp new file mode 100644 index 0000000..bd71261 --- /dev/null +++ b/src/lib/xdgautostart.cpp @@ -0,0 +1,38 @@ +#include "xdgautostart.h" +#include "xdgbasedir.h" +#include "xdgentry.h" +#include "debug.h" + +xdg::AutoStart::AutoStart(QObject *parent) + : QObject{parent} +{ + qCDebug(AUTO_START) << "Create Auto start"; + QStringList configdirs = xdg::configDirs(); + for (auto dir = configdirs.rbegin(); dir != configdirs.rend(); ++dir) + { + m_entries.addDirectory(*dir + "/autostart"); + } + m_entries.addDirectory(xdg::configHome() + "/autostart"); +} + +void xdg::AutoStart::start(QString key, QString value) +{ + qCDebug(AUTO_START) << "Start " << key << value; + for(xdg::Entry *entry: m_entries.getEntries()) + { + if((!value.isNull()) && (!key.isNull())) + { + QString c = entry->data(key); + if(c.trimmed().toLower() == value.trimmed().toLower()) + { + qCDebug(AUTO_START) << "Start app " << entry->appId() << "with" << key << value; + entry->start(QStringList()); + } + } + else + { + qCDebug(AUTO_START) << "Start app " << entry->appId(); + entry->start(QStringList()); + } + } +} diff --git a/src/lib/xdgautostart.h b/src/lib/xdgautostart.h new file mode 100644 index 0000000..fc6c02d --- /dev/null +++ b/src/lib/xdgautostart.h @@ -0,0 +1,24 @@ +#ifndef XDGAUTOSTART_H +#define XDGAUTOSTART_H + +#include <QObject> +#include "xdgentries.h" + + +namespace xdg { + class AutoStart : public QObject + { + Q_OBJECT + public: + explicit AutoStart(QObject *parent = nullptr); + xdg::Entries m_entries; + + public slots: + void start(QString key = QString(), QString value = QString()); + + signals: + + }; +} + +#endif // XDGAUTOSTART_H diff --git a/src/lib/xdgbasedir.cpp b/src/lib/xdgbasedir.cpp new file mode 100644 index 0000000..1f84109 --- /dev/null +++ b/src/lib/xdgbasedir.cpp @@ -0,0 +1,79 @@ +#include "xdgbasedir.h" +#include <QStringList> + +static inline QString user() +{ + QString user = QString::fromUtf8(qgetenv("USER")); + if(user.isEmpty()) return "root"; + return user; +} + +static inline QString home() +{ + QString home = QString::fromUtf8(qgetenv("HOME")); + if(home.isEmpty()) + { + QString u = user(); + if(u == "root") + return "/root"; + else + return "/home/" + u; + } + return home; +} + +static inline QString envOrDefault(const char *name, const QString &def) +{ + QString env = QString::fromUtf8(qgetenv(name)); + if(env.isEmpty()) + { + return def; + } + return env; +} + +QString xdg::dataHome() +{ + return envOrDefault("XDG_DATA_HOME", home() + "/.local/share"); +} + +QString xdg::configHome() +{ + return envOrDefault("XDG_CONFIG_HOME", home() + "/.config"); +} + +QString xdg::cacheHome() +{ + return envOrDefault("XDG_CACHE_HOME", home() + "/.cache"); +} + +QString xdg::stateHome() +{ + return envOrDefault("XDG_STATE_HOME", home() + "/.local/state"); +} + + +QStringList xdg::dataDirs() +{ + QString dataDirs = QString::fromUtf8(qgetenv("XDG_DATA_DIRS")); + if(dataDirs.isEmpty()) + { + return QStringList() << "/usr/local/share" << "/usr/share"; + } + return dataDirs.split(":"); +} + +QStringList xdg::configDirs() +{ + QString configDirs = QString::fromUtf8(qgetenv("XDG_CONFIG_DIRS")); + if(configDirs.isEmpty()) + { + return QStringList() << "/etc/xdg"; + } + return configDirs.split(":"); +} + +QString xdg::runtimeDir() +{ + return envOrDefault("XDG_RUNTIME_DIR", QString("/run/user/%i").arg(user())); +} diff --git a/src/lib/xdgbasedir.h b/src/lib/xdgbasedir.h new file mode 100644 index 0000000..bd7d688 --- /dev/null +++ b/src/lib/xdgbasedir.h @@ -0,0 +1,97 @@ +/*! \file xdgbasedir.h + \brief Implementation of basedir-spec + \author Fabien Proriol + + Implementation of https://specifications.freedesktop.org/basedir-spec/basedir-spec-0.6.html +*/ + +#ifndef XDGBASEDIR_H +#define XDGBASEDIR_H + +#include <QString> + +//! XDG basedir implementation +namespace xdg { +/*! + * \addtogroup XDG + * @{ + */ + +/*! \fn QString dataHome() + \brief Return XDG_DATA_HOME + XDG_DATA_HOME defines the base directory relative to + which user-specific data files should be stored. + \return $XDG_DATA_HOME content or $HOME/.local/share if not set +*/ + QString dataHome(); + + +/*! \fn QString configHome() + \brief Return XDG_CONFIG_HOME + XDG_CONFIG_HOME defines the base directory relative to + which user-specific configuration files should be stored. + \return $XDG_CONFIG_HOME content or $HOME/.config if not set +*/ + QString configHome(); + + +/*! \fn QString stateHome() + \brief Return XDG_STATE_HOME + XDG_STATE_HOME defines the base directory relative to + which user-specific state files should be stored. + \return $XDG_STATE_HOME content or $HOME/.local/state if not set +*/ + QString stateHome(); + + +/*! \fn QStringList dataDirs() + \brief Return XDG_DATA_DIRS + XDG_DATA_DIRS defines the preference-ordered set of + base directories to search for data files in addition + to the $XDG_DATA_HOME base directory. + The directories in $XDG_DATA_DIRS should be seperated + with a colon ':'. + \return $XDG_DATA_DIRS content or /usr/local/share:/usr/share if not set +*/ + QStringList dataDirs(); + +/*! \fn QStringList dataDirs() + \brief Return XDG_CONFIG_DIRS + XDG_CONFIG_DIRS defines the preference-ordered set of + base directories to search for configuration files in addition + to the $XDG_CONFIG_HOME base directory. + The directories in $XDG_CONFIG_DIRS should be seperated + with a colon ':'. + \return $XDG_CONFIG_DIRS content or /etc/xdg if not set +*/ + QStringList configDirs(); + + +/*! \fn QString cacheHome() + \brief Return XDG_CONFIG_DIRS + XDG_CACHE_HOME defines the base directory relative to + which user-specific non-essential data files should be + stored. + If $XDG_CACHE_HOME is either not set or empty, a default + equal to $HOME/.cache should be used. + \return $XDG_CONFIG_DIRS content or $HOME/.cache if not set +*/ + QString cacheHome(); + + +/*! \fn QString runtimeDir() + \brief Return XDG_CONFIG_DIRS + XDG_RUNTIME_DIR defines the base directory relative to + which user-specific non-essential runtime files and other + file objects (such as sockets, named pipes, ...) should + be stored. The directory MUST be owned by the user, + and he MUST be the only one having read and write access + to it. Its Unix access mode MUST be 0700. + \return $XDG_RUNTIME_DIR content or emit a warning and return /run/user/$USER +*/ + QString runtimeDir(); + +/*! @} End of XDG*/ +} + +#endif // XDGBASEDIR_H diff --git a/src/lib/xdgdesktopentries.cpp b/src/lib/xdgdesktopentries.cpp new file mode 100644 index 0000000..d1a3f95 --- /dev/null +++ b/src/lib/xdgdesktopentries.cpp @@ -0,0 +1,202 @@ +#include "xdgdesktopentries.h" +#include "xdgentry.h" +#include "xdgbasedir.h" +#include <QDir> +#include <QMutexLocker> +#include <qdebug.h> +#include <signal.h> + + + +xdg::DesktopEntries::DesktopEntries(QObject *parent) + : QAbstractListModel(parent) +{ + QStringList datadirs = xdg::dataDirs(); + for (auto dir = datadirs.rbegin(); dir != datadirs.rend(); ++dir) + { + m_entries.addDirectory(*dir + "/applications"); + } + m_entries.addDirectory(xdg::dataHome() + "/applications"); + QObject::connect(&m_entries, &xdg::Entries::entryDataChanged, this, &xdg::DesktopEntries::_dataChanged); + QObject::connect(&m_entries, &xdg::Entries::raiseProcess, this, &xdg::DesktopEntries::raiseProcess); + QObject::connect(&m_entries, &xdg::Entries::startCreateEntry, [this](int i){ + beginInsertRows(QModelIndex(), i, i); + }); + QObject::connect(&m_entries, &xdg::Entries::startCreateEntry, [this](int i){ + endInsertRows(); + emit categoriesChanged(categories()); + }); + + QObject::connect(&m_entries, &xdg::Entries::startRemoveEntry, [this](int i){ + beginRemoveRows(QModelIndex(), i, i); + }); + QObject::connect(&m_entries, &xdg::Entries::startRemoveEntry, [this](int i){ + endRemoveRows(); + emit categoriesChanged(categories()); + }); +} + +int xdg::DesktopEntries::rowCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent); + return m_entries.count(); +} + +QVariant xdg::DesktopEntries::data(const QModelIndex &index, int role) const +{ + if(index.row() > m_entries.count()) + return QVariant(); + + const xdg::Entry *entry = m_entries.getEntry(index.row()); + + switch(role) { + case Qt::DisplayRole: + case AppNameRole: + case NameRole: + return entry->data(XDG_NAME); + case ExecRole: + return entry->data(XDG_EXEC); + case IconRole: + return entry->data(XDG_ICON); + case CategoryRole: + return entry->data(XDG_CATEGORIES); + case NoDisplayRole: + return entry->data(XDG_NODISPLAY); + case KeywordsRole: + return entry->data(XDG_KEYWORDS); + case PathRole: + case DesktopFileRole: + return entry->path(); + case AppIdRole: + return entry->appId(); + case EntryRole: + { + QVariant ventry; + ventry.setValue(entry); + return ventry; + } + } + qWarning() << "Not managed role" << role; + return QVariant(); +} + +QHash<int, QByteArray> xdg::DesktopEntries::roleNames() const +{ + QHash<int, QByteArray> roles = QAbstractItemModel::roleNames(); + roles[IconRole] = "appIcon"; + roles[NameRole] = "name"; + roles[AppNameRole] = "appName"; + roles[ExecRole] = "exec"; + roles[PathRole] = "path"; + roles[NoDisplayRole] = "noDisplay"; + roles[CategoryRole] = "category"; + roles[KeywordsRole] = "keywords"; + roles[DesktopFileRole] = "desktopFile"; + roles[AppIdRole] = "appId"; + roles[EntryRole] = "entry"; + return roles; +} + +QStringList xdg::DesktopEntries::categories() +{ + QStringList ret; + for(xdg::Entry *entry: m_entries.getEntries()) + { + QString c = entry->data(XDG_CATEGORIES); + if(!ret.contains(c)) + ret.append(c); + } + return ret; +} + +void xdg::DesktopEntries::start(QString path, const QStringList& args) +{ + for(xdg::Entry *entry: m_entries.getEntries()) + { + if(entry->path() == path) + entry->start(args); + } +} + +void xdg::DesktopEntries::stop(QString path, const QStringList& args) +{ + bool isPid; + int pid = path.toInt(&isPid, 10); + for(xdg::Entry *entry: m_entries.getEntries()) + { + if((isPid && entry->pid() == pid) || (entry->path() == path)) + { + entry->stop(args); + return; + } + } + if(isPid) kill(pid, SIGTERM); +} + +QString xdg::DesktopEntries::appIdData(QString appid, QString key) +{ + for(xdg::Entry *entry: m_entries.getEntries()) + { + if(entry->appId() == appid) + return entry->data(key); + } + return QString(); +} + +bool xdg::DesktopEntries::_contains(QString path) +{ + for(const xdg::Entry *entry: m_entries.getEntries()) + { + if(entry->path() == path) + return true; + } + return false; +} + +QStringList xdg::DesktopEntries::_getEntriesFrom(QString path) +{ + QStringList entries; + for(xdg::Entry *entry: m_entries.getEntries()) + { + if(path == "" || entry->path().startsWith(path)) + entries.append(entry->path()); + } + return entries; +} + +void xdg::DesktopEntries::_dataChanged(const QString appid, const QString item, const QString value) +{ + if(item == XDG_CATEGORIES) + { + emit categoriesChanged(categories()); + } + + int role = -1; + if (item == XDG_ICON) + role = IconRole; + else if (item == XDG_NAME) + role = NameRole; + else if(item == XDG_EXEC) + role = ExecRole; + else if (item == XDG_CATEGORIES) + role = CategoryRole; + else if (item == XDG_KEYWORDS) + role = KeywordsRole; + else if (item == XDG_NODISPLAY) + role = NoDisplayRole; + else + qWarning() << "XdgEntries::_dataChanged: Unknwon role " << item; + if(role > 0) + { + for(int i = 0; i < m_entries.count(); i++) + { + const xdg::Entry *entry = m_entries.getEntry(i); + if(entry->appId() == appid) + { + QVector<int> r; + r.append(role); + emit dataChanged(index(i), index(i), r); + } + } + } +} diff --git a/src/lib/xdgdesktopentries.h b/src/lib/xdgdesktopentries.h new file mode 100644 index 0000000..89aff08 --- /dev/null +++ b/src/lib/xdgdesktopentries.h @@ -0,0 +1,58 @@ +#ifndef XDGDESKTOPENTRIES_H +#define XDGDESKTOPENTRIES_H + +#include <QAbstractListModel> +#include <QObject> +#include "xdgentries.h" + +namespace xdg { + class DesktopEntries : public QAbstractListModel + { + Q_OBJECT + Q_PROPERTY(QStringList categories READ categories NOTIFY categoriesChanged) + xdg::Entries m_entries; + + private slots: + bool _contains(QString path); + //void _reload(QString path); + QStringList _getEntriesFrom(QString path = ""); + void _dataChanged(const QString appid, const QString item, const QString value); + + public: + explicit DesktopEntries(QObject *parent = nullptr); + + enum XdgDesktopEntriesRoles { + PathRole = Qt::UserRole + 1, + NameRole, + IconRole, + CategoryRole, + NoDisplayRole, + KeywordsRole, + ExecRole, + DesktopFileRole, + AppIdRole, + AppNameRole, + EntryRole + }; + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + QHash<int, QByteArray> roleNames() const override; + + QStringList categories(); + + public slots: + void start(QString path, const QStringList &args = QStringList()); + void stop(QString path, const QStringList &args = QStringList()); + QString appIdData(QString appid, QString key); + + signals: + void raiseProcess(int pid); + void categoriesChanged(QStringList categories); + + private: + QString m_sources; + }; +} + +#endif // XDGDESKTOPENTRIES_H diff --git a/src/lib/xdgdesktopfilter.cpp b/src/lib/xdgdesktopfilter.cpp new file mode 100644 index 0000000..78e2ca2 --- /dev/null +++ b/src/lib/xdgdesktopfilter.cpp @@ -0,0 +1,131 @@ +#include "xdgdesktopfilter.h" +#include "xdgdesktopentries.h" +#include "xdgentry.h" +#include <qdebug.h> + +xdg::DesktopFilter::DesktopFilter(QObject *parent) + : QSortFilterProxyModel(parent) +{ +} + +QObject *xdg::DesktopFilter::model() const +{ + return static_cast<QObject*>(m_model); +} + +QString xdg::DesktopFilter::category() const +{ + return m_category; +} + +QString xdg::DesktopFilter::section() const +{ + return m_section; +} + + +bool xdg::DesktopFilter::showAll() const +{ + return m_showAll; +} + +bool xdg::DesktopFilter::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const +{ + QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent); + QVariant ventry = sourceModel()->data(index, xdg::DesktopEntries::EntryRole); + const xdg::Entry *entry = qvariant_cast<const xdg::Entry *>(ventry); + if(!entry) + { + qWarning() << "Can't retrive entry for " << sourceModel()->data(index, xdg::DesktopEntries::NameRole).toString(); + return false; + } + + if(!m_section.isEmpty() && entry->data(m_section + "/Exec").isEmpty()) + { + return false; + } + if(entry->data(XDG_NAME).isEmpty()) + return false; + if(!m_showAll && entry->data(XDG_NODISPLAY).toLower() == "true") + return false; + if(m_category.size() > 0 && entry->data(XDG_CATEGORIES) != m_category) + return false; + + return true; +} + + +QVariant xdg::DesktopFilter::data(const QModelIndex &index, int role) const +{ + if(!m_section.isEmpty()) + { + const xdg::Entry *entry = QSortFilterProxyModel::data(index, xdg::DesktopEntries::EntryRole).value<const xdg::Entry *>(); + if(entry) + { + QString res; + switch(role) { + case Qt::DisplayRole: + case xdg::DesktopEntries::NameRole: + res = entry->data(m_section + "/Name"); + return (res.isEmpty())?(res):(entry->data(XDG_NAME)); + case xdg::DesktopEntries::ExecRole: + res = entry->data(m_section + "/Exec"); + return (!res.isEmpty())?(res):(entry->data(XDG_EXEC)); + case xdg::DesktopEntries::IconRole: + res = entry->data(m_section + "/Icon"); + return (!res.isEmpty())?(res):(entry->data(XDG_ICON)); + case xdg::DesktopEntries::CategoryRole: + res = entry->data(m_section + "/Categories"); + return (!res.isEmpty())?(res):(entry->data(XDG_CATEGORIES)); + case xdg::DesktopEntries::KeywordsRole: + res = entry->data(m_section + "/Keywords"); + return (!res.isEmpty())?(res):(entry->data(XDG_KEYWORDS)); + } + } + } + return QSortFilterProxyModel::data(index, role); +} + +void xdg::DesktopFilter::setModel(QObject *model) +{ + if (m_model == model) + return; + + xdg::DesktopEntries *entries = qobject_cast<xdg::DesktopEntries*>(model); + if(entries) + { + m_model = entries; + setSourceModel(m_model); + emit modelChanged(m_model); + } +} + +void xdg::DesktopFilter::setCategory(QString category) +{ + if (m_category == category) + return; + + m_category = category; + invalidateFilter(); + emit categoryChanged(m_category); +} + +void xdg::DesktopFilter::setSection(QString section) +{ + if (m_section == section) + return; + + m_section = section; + invalidateFilter(); + emit sectionChanged(m_section); +} + +void xdg::DesktopFilter::setShowAll(bool showall) +{ + if (m_showAll == showall) + return; + + m_showAll = showall; + invalidateFilter(); + emit showAllChanged(m_showAll); +} diff --git a/src/lib/xdgdesktopfilter.h b/src/lib/xdgdesktopfilter.h new file mode 100644 index 0000000..abe2f06 --- /dev/null +++ b/src/lib/xdgdesktopfilter.h @@ -0,0 +1,49 @@ +#ifndef XDGDESKTOPFILTER_H +#define XDGDESKTOPFILTER_H + +#include <QSortFilterProxyModel> +#include <QObject> +#include "xdgdesktopentries.h" + +namespace xdg { + class DesktopEntries; + + class DesktopFilter : public QSortFilterProxyModel + { + Q_OBJECT + Q_PROPERTY(QObject *model READ model WRITE setModel NOTIFY modelChanged) + Q_PROPERTY(QString category READ category WRITE setCategory NOTIFY categoryChanged) + Q_PROPERTY(QString section READ section WRITE setSection NOTIFY sectionChanged) + Q_PROPERTY(bool showAll READ showAll WRITE setShowAll NOTIFY showAllChanged) + + xdg::DesktopEntries *m_model; + QString m_category; + QString m_section; + bool m_showAll {false}; + + public: + explicit DesktopFilter(QObject *parent = nullptr); + QObject *model() const; + QString category() const; + QString section() const; + bool showAll() const; + bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override; + + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + + public slots: + void setModel(QObject * model); + void setCategory(QString category); + void setSection(QString category); + void setShowAll(bool showall); + + signals: + void raise(int pid); + void modelChanged(QObject * model); + void categoryChanged(QString category); + void sectionChanged(QString category); + void showAllChanged(bool showall); + }; + +} +#endif // XDGDESKTOPFILTER_H diff --git a/src/lib/xdgentries.cpp b/src/lib/xdgentries.cpp new file mode 100644 index 0000000..790e472 --- /dev/null +++ b/src/lib/xdgentries.cpp @@ -0,0 +1,128 @@ +#include "xdgentries.h" +#include "xdgentry.h" +#include <QDebug> +#include <QDir> +#include <QFileInfo> +#include <set> +#include <algorithm> + +xdg::Entries::Entries(QObject *parent) + : QObject(parent) +{ + QObject::connect(&m_watcher, &QFileSystemWatcher::directoryChanged, this, &Entries::_directoryChanged, Qt::QueuedConnection); +} + +void xdg::Entries::_directoryChanged(const QString &path) +{ + QDir directory(path); + QStringList old, now, removed, added; + for(xdg::Entry *entry: m_entries) + { + if(entry->path().startsWith(directory.absolutePath())) + { + old.append(entry->path()); + } + } + for(const QString &item: directory.entryList(QStringList() << "*.desktop", QDir::Files)) + { + QString current = directory.absoluteFilePath(item); + if(current.startsWith(directory.absolutePath())) + { + now.append(current); + } + } + std::set_difference(old.begin(), old.end(), now.begin(), now.end(), std::inserter(removed, removed.begin())); + std::set_difference(now.begin(), now.end(), old.begin(), old.end(), std::inserter(added, added.begin())); + for(const QString &item: removed) + { + unregister(item); + } + for(const QString &item: added) + { + add(item, true); + } +} + +void xdg::Entries::_entryDataChanged(const QString key, const QString value) +{ + xdg::Entry* ent = qobject_cast<xdg::Entry*>(QObject::sender()); + emit entryDataChanged(ent->appId(), key, value); +} + +void xdg::Entries::unregister(const QString &path) +{ + QFileInfo info(path); + QString id = info.completeBaseName(); + int oldid = m_entries.keys().indexOf(id); + emit startRemoveEntry(oldid); + xdg::Entry* old = m_entries[id]; + m_watcher.removePath(old->path()); + m_entries.remove(id); + emit endRemoveEntry(oldid); +} + +int xdg::Entries::add(const QString &path, bool replace) +{ + QFileInfo info(path); + QString id = info.completeBaseName(); + if(!info.isFile() || info.suffix() != "desktop") + { + qWarning() << path << "is not a desktop entry"; + return -1; + } + + if(m_entries.contains(id)) + { + if(replace) + { + unregister(path); + } + else + { + return -1; + } + } + + xdg::Entry* entry = new xdg::Entry(path); + QObject::connect(entry, &xdg::Entry::dataChanged, this, &Entries::_entryDataChanged); + QObject::connect(entry, &xdg::Entry::raiseProcess, this, &Entries::raiseProcess); + + m_entries[id] = entry; + emit startCreateEntry(m_entries.keys().indexOf(id)); + emit endCreateEntry(m_entries.keys().indexOf(id)); + return 0; +} + +int xdg::Entries::addDirectory(const QString &path, bool replace) +{ + QFileInfo info(path.trimmed()); + if(!info.isDir()) + { + return -1; + } + + QDir directory(path.trimmed()); + QStringList items = directory.entryList(QStringList() << "*.desktop", QDir::Files); + for(QString &item: items) + { + add(directory.absoluteFilePath(item), replace); + } + + m_watcher.addPath(path.trimmed()); + return 0; +} + +qsizetype xdg::Entries::count() const +{ + return m_entries.count(); +} + +const xdg::Entry *xdg::Entries::getEntry(int i) const +{ + return m_entries[m_entries.keys()[i]]; +} + +QList<xdg::Entry *> xdg::Entries::getEntries() +{ + return m_entries.values(); +} diff --git a/src/lib/xdgentries.h b/src/lib/xdgentries.h new file mode 100644 index 0000000..fd0458a --- /dev/null +++ b/src/lib/xdgentries.h @@ -0,0 +1,41 @@ +#ifndef XDGENTRIES_H +#define XDGENTRIES_H + +#include <QObject> +#include <QFileSystemWatcher> +#include <QMap> + +namespace xdg { + class Entry; + + class Entries : public QObject + { + Q_OBJECT + QString m_type; + QFileSystemWatcher m_watcher; + QMap<QString, xdg::Entry*> m_entries; + + private slots: + void _directoryChanged(const QString &path); + void _entryDataChanged(const QString key, const QString value); + + public: + explicit Entries(QObject *parent = nullptr); + void unregister(const QString &path); + int add(const QString &path, bool replace = false); + int addDirectory(const QString &path, bool replace = false); + qsizetype count() const; + const Entry *getEntry(int i) const; + QList<xdg::Entry *> getEntries(); + + signals: + void entryDataChanged(const QString appid, const QString key, const QString value); + void raiseProcess(int pid); + void startCreateEntry(int id); + void endCreateEntry(int id); + void startRemoveEntry(int id); + void endRemoveEntry(int id); + }; +} + +#endif // XDGENTRIES_H diff --git a/src/lib/xdgentry.cpp b/src/lib/xdgentry.cpp new file mode 100644 index 0000000..83438dd --- /dev/null +++ b/src/lib/xdgentry.cpp @@ -0,0 +1,315 @@ +#include "xdgentry.h" +#include <QSettings> +#include <qdebug.h> +#include <signal.h> +#include <vector> +#include <string> +#include <QFileInfo> +#include "xdgbasedir.h" + +static inline QStringList tokenize(const QString& command) +{ + std::string s = command.toStdString(); + std::vector<std::string> result; + + std::string token; + char quote{}; + bool escape{false}; + + for (char c : s) + { + if (escape) + { + escape = false; + if (quote && c != '\\' && c != quote) + token += '\\'; + token += c; + } + else if (c == '\\') + { + escape = true; + } + else if (!quote && (c == '\'' || c == '\"')) + { + quote = c; + } + else if (quote && c == quote) + { + quote = '\0'; + if (token.empty()) + result.emplace_back(); + } + else if (!isspace(c) || quote) + { + token += c; + } + else if (!token.empty()) + { + result.push_back(std::move(token)); + token.clear(); + } + } + + if (!token.empty()) + { + result.push_back(std::move(token)); + token.clear(); + } + + + QStringList l; + for (auto t: result) + l.append(QString(t.c_str())); + return l; +} + +static inline QStringList args_substitution(const QStringList &tokens, const QStringList &args) +{ + QStringList out; + for (auto t : tokens) + { + if (t == "%d" || t == "%D" || t == "%n" || t == "%N" || t == "%v" || t == "%m") { //deprectaed stuff + continue; + } else if (t == "%i" || t == "%c") { //unsupported stuff + qWarning() << "XdgEntry: %i and %c are not supported (found in " << tokens.join(" ")<<")"; + continue; + } else if (t == "%f" || t == "%F") { // files + if (t == "%f") + { + if (args.length() > 0) + { + out.append(args[0]); + } + else + { + out.append(args); + } + } + } else if (t == "%u" || t == "%U") { // urls + // TODO : handle URLs properly + if (t == "%u") + { + if (args.length() > 0) + { + out.append(args[0]); + } + else + { + out.append(args); + } + } + } else { + out.append(t); + } + } + return out; +} + + +xdg::Entry::Entry(QString desktopfile, QObject *parent) + : QObject(parent) + , m_path(desktopfile) + , m_file(new QSettings(desktopfile, QSettings::IniFormat, this)) + , m_watcher(QStringList() << desktopfile) +{ + QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); + env.insert("QT_QPA_PLATFORM", "wayland-egl"); + env.remove("QT_IM_MODULE"); + m_process.setProcessEnvironment(env); + QObject::connect(&m_watcher, &QFileSystemWatcher::fileChanged, this, &Entry::_desktopFileChanged, Qt::QueuedConnection); + QObject::connect(&m_process, &QProcess::stateChanged, this, &Entry::_processStateChanged); +} + +xdg::Entry::Entry(QObject *parent) + : QObject(parent) +{ + QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); + env.insert("QT_QPA_PLATFORM", "wayland-egl"); + env.remove("QT_IM_MODULE"); + m_process.setProcessEnvironment(env); + QObject::connect(&m_watcher, &QFileSystemWatcher::fileChanged, this, &Entry::_desktopFileChanged, Qt::QueuedConnection); + QObject::connect(&m_process, &QProcess::stateChanged, this, &Entry::_processStateChanged); +} + +xdg::Entry::Entry(const Entry &origin) + : QObject(origin.parent()) + , m_path(origin.path()) + , m_file(new QSettings(origin.path(), QSettings::IniFormat, this)) + , m_watcher(QStringList() << origin.path()) +{ + QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); + env.insert("QT_QPA_PLATFORM", "wayland-egl"); + env.remove("QT_IM_MODULE"); + m_process.setProcessEnvironment(env); + QObject::connect(&m_watcher, &QFileSystemWatcher::fileChanged, this, &Entry::_desktopFileChanged, Qt::QueuedConnection); + QObject::connect(&m_process, &QProcess::stateChanged, this, &Entry::_processStateChanged); +} + + +QString xdg::Entry::data(const QString &key) const +{ + if(!m_file) + return QString(); + if(key == XDG_KEYWORDS) + { + QString keywords = m_file->value(key).toString(); + if(keywords.contains("RemoteProcess")) + return keywords; + + switch(m_process.state()) + { + case QProcess::Running: + keywords.append(" running"); + break; + case QProcess::NotRunning: + keywords.append(" stopped"); + break; + case QProcess::Starting: + keywords.append(" starting"); + break; + } + return keywords; + } + if(!m_used.contains(key)) + m_used[key] = m_file->value(key).toString(); + return m_file->value(key).toString(); +} + +void xdg::Entry::start(QStringList args) +{ + QString exec = data(XDG_EXEC); + QString startExec = data(XDG_ACTION_START); + + if(exec.size() > 0) + { + QStringList param = tokenize(exec); + QString cmd = param.takeFirst(); + QStringList argv = args_substitution(param, args); + + if(m_process.state() == QProcess::Running) + { + emit raiseProcess(m_process.processId()); + } + else + { + m_process.start(cmd, argv); + } + } + else if(startExec.size() > 0) + { + QStringList param = tokenize(startExec); + QString cmd = param.takeFirst(); + QStringList argv = args_substitution(param, args); + + m_process.start(cmd, argv); + } + else + { + qWarning() << "Can't find a way to start " << m_path; + } +} + +void xdg::Entry::stop(QStringList args) +{ + QString stopExec = data(XDG_ACTION_STOP); + + if(stopExec.size() > 0) + { + QStringList param = tokenize(stopExec); + QString cmd = param.takeFirst(); + QStringList argv = args_substitution(param, args); + + m_process.start(cmd, argv); + } + else + { + if(m_process.state() == QProcess::NotRunning) + return; + kill(m_process.processId(), SIGTERM); + } +} + +QString xdg::Entry::path() const +{ + return m_path; +} + +QString xdg::Entry::appId() const +{ + QFileInfo fi(path()); + return fi.completeBaseName(); +} + +QString xdg::Entry::icon() const +{ + return data(XDG_ICON); +} + +int xdg::Entry::pid() const +{ + return m_process.processId(); +} + +void xdg::Entry::setDesktopFile(QString desktopFile) +{ + m_path = desktopFile; + m_file = new QSettings(desktopFile, QSettings::IniFormat); + m_watcher.addPaths(QStringList() << desktopFile); + emit iconChanged(data(XDG_ICON)); +} + +void xdg::Entry::setAppId(QString appId) +{ + /* Search for appid destopfile */ + QString filename; + + filename = xdg::dataHome() + "/applications/" + appId + ".desktop"; + if(filename.length()) + { + QFileInfo info(filename); + if(info.exists()) + { + setDesktopFile(filename); + return; + } + } + + for(QString dir: xdg::dataDirs()) + { + filename = dir + "/applications/" + appId + ".desktop"; + QFileInfo info(filename); + if(info.exists()) + { + setDesktopFile(filename); + return; + } + } + qWarning() << "Can't find Desktop Entry for " + appId; +} + + +void xdg::Entry::_desktopFileChanged(const QString &path) +{ + if(!m_file) + return; + QFileInfo info(path); + if(!info.isFile()) + { + emit removed(path); + return; + } + m_file->sync(); + for(const QString &key: m_used.keys()) + { + if(m_file->value(key).toString() != m_used[key]) + { + m_used[key] = m_file->value(key).toString(); + emit dataChanged(key, m_used[key]); + } + } +} + +void xdg::Entry::_processStateChanged(QProcess::ProcessState newState) +{ + emit dataChanged(XDG_KEYWORDS, data(XDG_KEYWORDS)); +} diff --git a/src/lib/xdgentry.h b/src/lib/xdgentry.h new file mode 100644 index 0000000..006d3ae --- /dev/null +++ b/src/lib/xdgentry.h @@ -0,0 +1,67 @@ +#ifndef XDGENTRY_H +#define XDGENTRY_H + +#include <QObject> +#include <QFileSystemWatcher> +#include <QProcess> +#include <QSettings> + +#define XDG_EXEC "Desktop Entry/Exec" +#define XDG_ICON "Desktop Entry/Icon" +#define XDG_NAME "Desktop Entry/Name" +#define XDG_KEYWORDS "Desktop Entry/Keywords" +#define XDG_NODISPLAY "Desktop Entry/NoDisplay" +#define XDG_CATEGORIES "Desktop Entry/Categories" + +#define XDG_ACTION_START "Desktop Action Start/Exec" +#define XDG_ACTION_STOP "Desktop Action Stop/Exec" + +namespace xdg { + class Entry: public QObject + { + Q_OBJECT + + Q_PROPERTY(QString appId READ appId WRITE setAppId NOTIFY appIdChanged) + Q_PROPERTY(QString icon READ icon NOTIFY iconChanged) + Q_PROPERTY(QString desktopFile READ path WRITE setDesktopFile NOTIFY desktopFileChanged) + + QString m_path; + QSettings *m_file {nullptr}; + QFileSystemWatcher m_watcher; + QProcess m_process; + mutable QMap<QString, QString> m_used; + + private slots: + void _desktopFileChanged(const QString &path); + void _processStateChanged(QProcess::ProcessState newState); + + public: + explicit Entry(QString desktopfile, QObject *parent = nullptr); + explicit Entry(QObject *parent = nullptr); + explicit Entry(const xdg::Entry& origin); + + QString path() const; + QString appId() const; + QString icon() const; + int pid() const; + void setDesktopFile(QString desktopFile); + void setAppId(QString appId); + + public slots: + void start(QStringList args); + void stop(QStringList args); + QString data(const QString &key) const; + + signals: + void dataChanged(const QString key, const QString value); + void raiseProcess(int pid); + void removed(const QString path); + void appIdChanged(QString appId); + void iconChanged(QString icon); + void desktopFileChanged(QString desktopFile); + }; +} + +Q_DECLARE_METATYPE(const xdg::Entry*) + +#endif // XDGENTRY_H diff --git a/src/lib/xdgnotificationactions.cpp b/src/lib/xdgnotificationactions.cpp new file mode 100644 index 0000000..afa7c19 --- /dev/null +++ b/src/lib/xdgnotificationactions.cpp @@ -0,0 +1,63 @@ +#include "xdgnotificationactions.h" +#include <QDebug> + +xdg::NotificationActions::NotificationActions(QObject *parent) + : QAbstractListModel(parent) +{ + +} + +QStringList xdg::NotificationActions::actionslist() const +{ + return m_actionslist; +} + +void xdg::NotificationActions::setActionslist(const QStringList &newActionslist) +{ + if(newActionslist.count() % 2 != 0) + { + qWarning() << "Error in actionlist:" << newActionslist; + } + + if (m_actionslist == newActionslist) + return; + m_actionslist = newActionslist; + + beginResetModel(); + m_actions.clear(); + for(int i = 0; i < newActionslist.count() / 2; i++) + { + m_actions.append(QPair<QString, QString>(newActionslist[i * 2], newActionslist[(i * 2) + 1])); + } + endResetModel(); + + emit actionslistChanged(); +} + +int xdg::NotificationActions::rowCount(const QModelIndex &parent) const +{ + return m_actions.count(); +} + +QVariant xdg::NotificationActions::data(const QModelIndex &index, int role) const +{ + if(index.row() > m_actions.count()) + return QVariant(); + + switch(role) { + case Qt::DisplayRole: + case TextRole: + return m_actions[index.row()].second; + case KeyRole: + return m_actions[index.row()].first; + } + return QVariant(); +} + +QHash<int, QByteArray> xdg::NotificationActions::roleNames() const +{ + QHash<int, QByteArray> roles = QAbstractItemModel::roleNames(); + roles[KeyRole] = "key"; + roles[TextRole] = "actionText"; + return roles; +} diff --git a/src/lib/xdgnotificationactions.h b/src/lib/xdgnotificationactions.h new file mode 100644 index 0000000..1d1e449 --- /dev/null +++ b/src/lib/xdgnotificationactions.h @@ -0,0 +1,40 @@ +#ifndef XDGNOTIFICATIONACTIONS_H +#define XDGNOTIFICATIONACTIONS_H + +#include <QAbstractListModel> + +namespace xdg { + + class NotificationActions: public QAbstractListModel + { + Q_OBJECT + + QList<QPair<QString, QString>> m_actions; + + Q_PROPERTY(QStringList actionslist READ actionslist WRITE setActionslist NOTIFY actionslistChanged) + + public: + NotificationActions(QObject *parent = nullptr); + + enum NotificationActionsRoles { + KeyRole = Qt::UserRole + 1, + TextRole, + }; + + QStringList actionslist() const; + void setActionslist(const QStringList &newActionslist); + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + QHash<int, QByteArray> roleNames() const override; + + signals: + void actionslistChanged(); + private: + QStringList m_actionslist; + }; + +} + + +#endif // XDGNOTIFICATIONACTIONS_H diff --git a/src/lib/xdgnotificationmanager.cpp b/src/lib/xdgnotificationmanager.cpp new file mode 100644 index 0000000..06e8594 --- /dev/null +++ b/src/lib/xdgnotificationmanager.cpp @@ -0,0 +1,173 @@ +#include "xdgnotificationmanager.h" +#include "xdgnotificationserver.h" +#include "xdgnotificationmessage.h" +#include "notificationsadaptor.h" + +uint xdg::serverNotify(QObject *obj, const QString &app_name, uint replaces_id, const QString &app_icon, const QString &summary, const QString &body, const QStringList &actions, const QVariantMap &hints, int timeout) +{ + xdg::NotificationManager *manager = qobject_cast<xdg::NotificationManager*>(obj); + if(replaces_id != 0 && manager->m_pending.contains(replaces_id)) + { + xdg::NotificationMessage *message = manager->m_pending[replaces_id]; + message->update(app_name, app_icon, summary, body, actions, hints, timeout); + } + else + { + xdg::NotificationMessage *message = new xdg::NotificationMessage(app_name, app_icon, summary, body, actions, hints, timeout, manager); + + QObject::connect(message, &xdg::NotificationMessage::appNameChanged, [manager, replaces_id](){ + const QVector<int> roles = {xdg::NotificationManager::AppNameRole}; + emit manager->dataChanged( + manager->index(replaces_id), + manager->index(replaces_id), + roles); + }); + + QObject::connect(message, &xdg::NotificationMessage::appIconChanged, [manager, replaces_id](){ + const QVector<int> roles = {xdg::NotificationManager::AppIconRole}; + emit manager->dataChanged( + manager->index(replaces_id), + manager->index(replaces_id), + roles); + }); + + QObject::connect(message, &xdg::NotificationMessage::summaryChanged, [manager, replaces_id](){ + const QVector<int> roles = {xdg::NotificationManager::SummaryRole}; + emit manager->dataChanged( + manager->index(replaces_id), + manager->index(replaces_id), + roles); + }); + + QObject::connect(message, &xdg::NotificationMessage::bodyChanged, [manager, replaces_id](){ + const QVector<int> roles = {xdg::NotificationManager::BodyRole}; + emit manager->dataChanged( + manager->index(replaces_id), + manager->index(replaces_id), + roles); + }); + + QObject::connect(message, &xdg::NotificationMessage::actionsChanged, [manager, replaces_id](){ + const QVector<int> roles = {xdg::NotificationManager::ActionsRole}; + emit manager->dataChanged( + manager->index(replaces_id), + manager->index(replaces_id), + roles); + }); + + QObject::connect(message, &xdg::NotificationMessage::hintsChanged, [manager, replaces_id](){ + const QVector<int> roles = {xdg::NotificationManager::HintsRole}; + emit manager->dataChanged( + manager->index(replaces_id), + manager->index(replaces_id), + roles); + }); + + + QObject::connect(message, &xdg::NotificationMessage::expired, [manager](QObject *obj){ + xdg::NotificationMessage *msg = qobject_cast<xdg::NotificationMessage*>(obj); + if(msg) + { + manager->close(manager->m_pending.key(msg), 1); + } + }); + + manager->beginInsertRows(QModelIndex(), 0, 0); + manager->m_pending[++manager->m_last] = message; + message->setId(manager->m_last); + manager->m_ids.insert(0, manager->m_last); + manager->endInsertRows(); + } + return manager->m_last; +} + + +xdg::NotificationManager::NotificationManager(QObject *parent) + : QAbstractListModel(parent) +{ + m_server = new XdgNotificationServer(this); + m_server->setNotify(serverNotify, this); + QObject::connect(m_server, &XdgNotificationServer::closeNotification, this, &NotificationManager::_closeNotification); + QObject::connect(this, &NotificationManager::rowsRemoved, this, &NotificationManager::notificationsChanged); + QObject::connect(this, &NotificationManager::rowsInserted, this, &NotificationManager::notificationsChanged); + new NotificationsAdaptor(m_server); + XdgNotificationServer::RegisterServerToDbus(*m_server); +} + +int xdg::NotificationManager::rowCount(const QModelIndex &parent) const +{ + return m_ids.count(); +} + +QVariant xdg::NotificationManager::data(const QModelIndex &index, int role) const +{ + if(index.row() > m_ids.count()) + return QVariant(); + + const NotificationMessage *message = m_pending[m_ids[index.row()]]; + if(message == nullptr) + return QVariant(); + + switch(role) { + case Qt::DisplayRole: + case AppNameRole: + return message->appName(); + case AppIconRole: + return message->appIcon(); + case SummaryRole: + return message->summary(); + case BodyRole: + return message->body(); + case ActionsRole: + return message->actions(); + case HintsRole: + return message->hints(); + case IdRole: + return message->id(); + } + return QVariant(); +} + +QHash<int, QByteArray> xdg::NotificationManager::roleNames() const +{ + QHash<int, QByteArray> roles = QAbstractItemModel::roleNames(); + roles[AppNameRole] = "appName"; + roles[AppIconRole] = "appIcon"; + roles[SummaryRole] = "summary"; + roles[BodyRole] = "body"; + roles[ActionsRole] = "actions"; + roles[HintsRole] = "hints"; + roles[IdRole] = "appId"; + return roles; +} + + +int xdg::NotificationManager::nbNotifications() +{ + return rowCount(); +} + +void xdg::NotificationManager::close(unsigned int id, unsigned int reason) +{ + unsigned int index = m_ids.indexOf(id); + beginRemoveRows(QModelIndex(), index, index); + m_ids.removeAll(id); + NotificationMessage *message = m_pending[id]; + m_pending.remove(id); + if (message != NULL) { + message->deleteLater(); + } + endRemoveRows(); + m_server->sendNotificationClosed(id, reason); +} + +void xdg::NotificationManager::returnAction(unsigned int appid, const QString &key) +{ + m_server->sendActionInvoked(appid, key); + close(appid, 2); +} + +void xdg::NotificationManager::_closeNotification(unsigned int id) +{ + close(id, 3); +} diff --git a/src/lib/xdgnotificationmanager.h b/src/lib/xdgnotificationmanager.h new file mode 100644 index 0000000..181bf0b --- /dev/null +++ b/src/lib/xdgnotificationmanager.h @@ -0,0 +1,55 @@ +#ifndef SystrayNotifyManager_H +#define SystrayNotifyManager_H + +#include <QAbstractListModel> +#include <QObject> + +class XdgNotificationServer; + +namespace xdg { + uint serverNotify(QObject *manager, const QString &app_name, uint replaces_id, const QString &app_icon, const QString &summary, const QString &body, const QStringList &actions, const QVariantMap &hints, int timeout); + class NotificationMessage; + + class NotificationManager : public QAbstractListModel + { + Q_OBJECT + Q_PROPERTY(int nbNotifications READ nbNotifications NOTIFY notificationsChanged) + + XdgNotificationServer *m_server; + unsigned int m_last = 0; + QMap<unsigned int, NotificationMessage *> m_pending; + QList<unsigned int> m_ids; + + friend uint serverNotify(QObject *manager, const QString &app_name, uint replaces_id, const QString &app_icon, const QString &summary, const QString &body, const QStringList &actions, const QVariantMap &hints, int timeout); + public: + explicit NotificationManager(QObject *parent = nullptr); + + enum SystrayNotifyRoles { + AppNameRole = Qt::UserRole + 1, + AppIconRole, + SummaryRole, + BodyRole, + ActionsRole, + HintsRole, + IdRole, + }; + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + QHash<int, QByteArray> roleNames() const override; + + + public slots: + int nbNotifications(); + void close(unsigned int id, unsigned int reason = 2); + void returnAction(unsigned int appid, const QString &key); + + private slots: + void _closeNotification(unsigned int id); + + signals: + void notificationsChanged(); + }; +} + +#endif // SystrayNotifyManager_H diff --git a/src/lib/xdgnotificationmessage.cpp b/src/lib/xdgnotificationmessage.cpp new file mode 100644 index 0000000..3dc0e2e --- /dev/null +++ b/src/lib/xdgnotificationmessage.cpp @@ -0,0 +1,119 @@ +#include "xdgnotificationmessage.h" +#include <QTimer> + + +QString xdg::NotificationMessage::appName() const +{ + return m_appName; +} + +QString xdg::NotificationMessage::appIcon() const +{ + return m_appIcon; +} + +QString xdg::NotificationMessage::summary() const +{ + return m_summary; +} + +QString xdg::NotificationMessage::body() const +{ + return m_body; +} + +QStringList xdg::NotificationMessage::actions() const +{ + return m_actions; +} + +QVariantMap xdg::NotificationMessage::hints() const +{ + return m_hints; +} + +uint xdg::NotificationMessage::id() const +{ + return m_id; +} + +void xdg::NotificationMessage::_expired() +{ + emit expired(this); +} + + +xdg::NotificationMessage::NotificationMessage( + const QString &app_name, + const QString &app_icon, + const QString &summary, + const QString &body, + const QStringList &actions, + const QVariantMap &hints, + int timeout, + QObject *parent) + : QObject(parent) + , m_appName(app_name) + , m_appIcon(app_icon) + , m_summary(summary) + , m_body(body) + , m_actions(actions) + , m_hints(hints) + , m_timeout(timeout) +{ + if(m_timeout > 0) + QTimer::singleShot(m_timeout, this, &xdg::NotificationMessage::_expired); +} + +void xdg::NotificationMessage::update(const QString &app_name, const QString &app_icon, const QString &summary, const QString &body, const QStringList &actions, const QVariantMap &hints, int timeout) +{ + if(m_appName != app_name) + { + m_appName = app_name; + emit appNameChanged(m_appName); + } + + if(m_appIcon != app_icon) + { + m_appIcon = app_icon; + emit appIconChanged(m_appIcon); + } + + if(m_summary != summary) + { + m_summary = summary; + emit summaryChanged(m_summary); + } + + if(m_body != app_name) + { + m_body = body; + emit bodyChanged(m_body); + } + + if(m_actions != actions) + { + m_actions = actions; + emit actionsChanged(m_actions); + } + + if(m_hints != hints) + { + m_hints = hints; + emit hintsChanged(m_hints); + } + + if(m_timeout != timeout) + { + m_timeout = timeout; + emit timeoutChanged(m_timeout); + } + if(m_timeout > 0) + QTimer::singleShot(m_timeout, this, &xdg::NotificationMessage::_expired); + +} + +void xdg::NotificationMessage::setId(uint id) +{ + m_id = id; +} diff --git a/src/lib/xdgnotificationmessage.h b/src/lib/xdgnotificationmessage.h new file mode 100644 index 0000000..8e62cab --- /dev/null +++ b/src/lib/xdgnotificationmessage.h @@ -0,0 +1,67 @@ +#ifndef NOTIFYMESSAGE_H +#define NOTIFYMESSAGE_H + +#include <QObject> +#include <QVariantMap> + +namespace xdg { + class NotificationMessage : public QObject + { + Q_OBJECT + + QString m_appName; + QString m_appIcon; + QString m_summary; + QString m_body; + QStringList m_actions; + QVariantMap m_hints; + uint m_id; + int m_timeout; + + + public: + explicit NotificationMessage( + const QString &app_name, + const QString &app_icon, + const QString &summary, + const QString &body, + const QStringList &actions, + const QVariantMap &hints, + int timeout, + QObject *parent = nullptr); + + void update( + const QString &app_name, + const QString &app_icon, + const QString &summary, + const QString &body, + const QStringList &actions, + const QVariantMap &hints, + int timeout); + + void setId(uint id); + + QString appName() const; + QString appIcon() const; + QString summary() const; + QString body() const; + QStringList actions() const; + QVariantMap hints() const; + uint id() const; + + private slots: + void _expired(); + + signals: + void appNameChanged(QString appName); + void appIconChanged(QString appIcon); + void summaryChanged(QString summary); + void bodyChanged(QString body); + void actionsChanged(QStringList actions); + void hintsChanged(QVariantMap hints); + void timeoutChanged(int timeout); + void expired(QObject *obj); + }; +} + +#endif // NOTIFYMESSAGE_H diff --git a/src/lib/xdgnotificationserver.cpp b/src/lib/xdgnotificationserver.cpp new file mode 100644 index 0000000..3f83cea --- /dev/null +++ b/src/lib/xdgnotificationserver.cpp @@ -0,0 +1,87 @@ +#include "xdgnotificationserver.h" +#include <QDBusConnection> +#include <QDebug> + +void XdgNotificationServer::setNotify(NotifyCallback newNotify, QObject *obj) +{ + m_notify = newNotify; + m_notifyData = obj; +} + +void XdgNotificationServer::sendActionInvoked(uint id, const QString &action_key) +{ + emit ActionInvoked(id, action_key); +} + +void XdgNotificationServer::sendNotificationClosed(uint id, uint reason) +{ + emit NotificationClosed(id, reason); +} + +XdgNotificationServer::XdgNotificationServer(QObject *parent) + : QObject{parent} +{ + +} + +void XdgNotificationServer::RegisterServerToDbus(XdgNotificationServer &server) +{ + const QString service = "org.freedesktop.Notifications"; + const QString path = "/org/freedesktop/Notifications"; + QDBusConnection bus = QDBusConnection::sessionBus(); + if (!bus.isConnected()) { + qWarning("Cannot connect to the D-Bus session bus.\n" + "Please check your system settings and try again.\n"); + } + bus.registerObject(path, &server); + + bool conres = bus.registerService(service); + if(!conres) qWarning() << "Can't register dbus service " << service; +} + +void XdgNotificationServer::CloseNotification(uint id) +{ + emit closeNotification(id); +} + +QStringList XdgNotificationServer::GetCapabilities() +{ + return QStringList(); +} + +QString XdgNotificationServer::GetServerInformation(QString &vendor, QString &version, QString &spec_version) +{ + vendor = "KaZoe"; + version = "1.0.0"; + spec_version = "1.2"; + return QString(); // Why return string ? spec is return void +} + +uint XdgNotificationServer::Notify(const QString &app_name, uint replaces_id, const QString &app_icon, const QString &summary, const QString &body, const QStringList &actions, const QVariantMap &hints, int timeout) +{ + if(m_notify) + { + return m_notify(m_notifyData, app_name, replaces_id, app_icon, summary, body, actions, hints, timeout); + } + else + { + qDebug() << "XdgNotificationServer::Notify(" + << app_name + << ", " + << replaces_id + << ", " + << app_icon + << ", " + << summary + << ", " + << body + << ", " + << actions + << ", " + << hints + << ", " + << timeout + << ")"; + } + return 0; +} diff --git a/src/lib/xdgnotificationserver.h b/src/lib/xdgnotificationserver.h new file mode 100644 index 0000000..5bdfbdf --- /dev/null +++ b/src/lib/xdgnotificationserver.h @@ -0,0 +1,42 @@ +#ifndef XDGNOTIFICATIONSERVER_H +#define XDGNOTIFICATIONSERVER_H + +#include <QObject> + +typedef uint (*NotifyCallback)(QObject *obj, const QString &app_name, uint replaces_id, const QString &app_icon, const QString &summary, const QString &body, const QStringList &actions, const QVariantMap &hints, int timeout) override; + + +class XdgNotificationServer : public QObject +{ + Q_OBJECT + + NotifyCallback m_notify; + QObject *m_notifyData; + +public: + explicit XdgNotificationServer(QObject *parent = nullptr); + static void RegisterServerToDbus(XdgNotificationServer &server); + + void setNotify(NotifyCallback newNotify, QObject *obj); + void sendActionInvoked(uint id, const QString &action_key); + void sendNotificationClosed(uint id, uint reason); + +public slots: + + /* DBus API */ + virtual void CloseNotification(uint id); + virtual QStringList GetCapabilities(); + virtual QString GetServerInformation(QString &vendor, QString &version, QString &spec_version); + virtual uint Notify(const QString &app_name, uint replaces_id, const QString &app_icon, const QString &summary, const QString &body, const QStringList &actions, const QVariantMap &hints, int timeout); + + + +signals: + void closeNotification(uint id); + + void ActionInvoked(uint id, const QString &action_key); + void NotificationClosed(uint id, uint reason); + +}; + +#endif // XDGNOTIFICATIONSERVER_H diff --git a/src/lib/xdgnotifier.cpp b/src/lib/xdgnotifier.cpp new file mode 100644 index 0000000..da52ec5 --- /dev/null +++ b/src/lib/xdgnotifier.cpp @@ -0,0 +1,99 @@ +#include "xdgnotifier.h" +#include "NotificationsIface.h" +#include <QGuiApplication> + +XdgNotifier::XdgNotifier(QObject *parent) + : QObject{parent} +{ + QDBusConnection connection = QDBusConnection::sessionBus(); + + if (!connection.isConnected()) { + qWarning("Cannot connect to the D-Bus session bus.\n" + "Please check your system settings and try again.\n"); + } + const QString service = "org.freedesktop.Notifications"; + const QString path = "/org/freedesktop/Notifications"; + + m_iface = new OrgFreedesktopNotificationsInterface(service, path, connection); + + QObject::connect(m_iface, &OrgFreedesktopNotificationsInterface::ActionInvoked, [this](uint id, const QString &action_key){ + qDebug() << "Recive signal ActionInvoked(" << id << ", " << action_key << ")"; + emit actionInvoked(action_key); + }); + + QObject::connect(m_iface, &OrgFreedesktopNotificationsInterface::NotificationClosed, [this](uint id, uint reason){ + qDebug() << "Recive signal NotificationClosed(" << id << ", " << reason << ")"; + switch(reason) + { + case 1: + emit notificationClosed("The notification expired."); + break; + case 2: + emit notificationClosed("The notification was dismissed by the user."); + break; + case 3: + emit notificationClosed(" The notification was closed by a call to CloseNotification."); + break; + default: + emit notificationClosed("Undefined/reserved reasons."); + break; + } + }); +} + +void XdgNotifier::notifyTextOnly(QString summary, QString body, bool replace) +{ + qDebug() << "notifyTextOnly(" << summary << ", " << body << ")"; + auto reply = m_iface->Notify( + QGuiApplication::applicationDisplayName(), + (replace)?(m_lastid):(0), + QString(), + summary, + body, + QStringList(), + QVariantMap(), + -1 + ); + reply.waitForFinished(); + if(!replace) + m_lastid = reply.value(); +} + +void XdgNotifier::notifyWithActions(QString summary, QString body, bool replace, QStringList actions) +{ + auto reply = m_iface->Notify( + QGuiApplication::applicationDisplayName(), + (replace)?(m_lastid):(0), + QString(), + summary, + body, + actions, + QVariantMap(), + -1 + ); + reply.waitForFinished(); + if(!replace) + m_lastid = reply.value(); +} + +void XdgNotifier::notify(const QString &summary, const QString &body, bool replace, const QStringList &actions, int timeout) +{ + auto reply = m_iface->Notify( + QGuiApplication::applicationDisplayName(), + (replace)?(m_lastid):(0), + QString(), + summary, + body, + actions, + QVariantMap(), + timeout + ); + reply.waitForFinished(); + if(!replace) + m_lastid = reply.value(); +} + +void XdgNotifier::closeNotification() +{ + auto reply = m_iface->CloseNotification(m_lastid); +} diff --git a/src/lib/xdgnotifier.h b/src/lib/xdgnotifier.h new file mode 100644 index 0000000..7147626 --- /dev/null +++ b/src/lib/xdgnotifier.h @@ -0,0 +1,28 @@ +#ifndef NOTIFIER_H +#define NOTIFIER_H + +#include <QObject> +#include <QDBusConnection> + +class OrgFreedesktopNotificationsInterface; + +class XdgNotifier : public QObject +{ + Q_OBJECT + OrgFreedesktopNotificationsInterface *m_iface; + int m_lastid; +public: + explicit XdgNotifier(QObject *parent = nullptr); + +public slots: + void notifyTextOnly(QString summary, QString body, bool replace); + void notifyWithActions(QString summary, QString body, bool replace, QStringList actions); + void notify(const QString &summary, const QString &body, bool replace, const QStringList &actions, int timeout); + void closeNotification(); + +signals: + void actionInvoked(QString action); + void notificationClosed(QString reason); +}; + +#endif // NOTIFIER_H diff --git a/src/lib/xdgstatusnotifierhost.cpp b/src/lib/xdgstatusnotifierhost.cpp new file mode 100644 index 0000000..49a17b1 --- /dev/null +++ b/src/lib/xdgstatusnotifierhost.cpp @@ -0,0 +1,187 @@ +#include "xdgstatusnotifierhost.h" +#include "XdgStatusNotifierItemIface.h" +#include "XdgStatusNotifierWatcherIface.h" +#include <QDBusConnection> +#include "debug.h" + +using namespace org::kde; + +namespace xdg { + class StatusNotifierHostPrivate { + Q_DISABLE_COPY(StatusNotifierHostPrivate) + Q_DECLARE_PUBLIC(StatusNotifierHost) + StatusNotifierHost * const q_ptr; + StatusNotifierHostPrivate(StatusNotifierHost *parent) + : q_ptr(parent) + , m_bus(QDBusConnection::sessionBus()) + {}; + + QMap<QString, StatusNotifierItem*> m_items; + QList<QString> m_itemsorder; + StatusNotifierWatcher *m_watcher; + QDBusConnection m_bus; + QString m_serviceName; + + void addNotifier(QString item) + { + StatusNotifierItem *stat = new StatusNotifierItem(item.split("/").first(), "/StatusNotifierItem", m_bus, q_ptr); + int index = m_itemsorder.size(); + q_ptr->beginInsertRows(QModelIndex(), index, index); + m_itemsorder.append(item); + m_items[item] = stat; + + QObject::connect(stat, &StatusNotifierItem::NewAttentionIcon, [index, this](){ + QVector<int> r; + r.append(StatusNotifierHost::AttentionIconNameRole); + emit q_ptr->dataChanged(q_ptr->index(index), q_ptr->index(index), r); + }); + + QObject::connect(stat, &StatusNotifierItem::NewIcon, [index, this](){ + QVector<int> r; + r.append(StatusNotifierHost::IconNameRole); + emit q_ptr->dataChanged(q_ptr->index(index), q_ptr->index(index), r); + }); + + QObject::connect(stat, &StatusNotifierItem::NewOverlayIcon, [index, this](){ + QVector<int> r; + r.append(StatusNotifierHost::OverlayIconNameRole); + emit q_ptr->dataChanged(q_ptr->index(index), q_ptr->index(index), r); + }); + + QObject::connect(stat, &StatusNotifierItem::NewStatus, [index, this](){ + QVector<int> r; + r.append(StatusNotifierHost::StatusRole); + emit q_ptr->dataChanged(q_ptr->index(index), q_ptr->index(index), r); + }); + + QObject::connect(stat, &StatusNotifierItem::NewTitle, [index, this](){ + QVector<int> r; + r.append(StatusNotifierHost::TitleRole); + emit q_ptr->dataChanged(q_ptr->index(index), q_ptr->index(index), r); + }); + + q_ptr->endInsertRows(); + } + + void removeNotifier(QString item) + { + int index = m_itemsorder.indexOf(item); + q_ptr->beginRemoveRows(QModelIndex(), index, index); + m_itemsorder.removeAll(item); + m_items[item]->deleteLater(); + m_items.remove(item); + q_ptr->endRemoveRows(); + } + }; +}; + +using namespace xdg; + +StatusNotifierHost::StatusNotifierHost(QObject *parent) + : QAbstractListModel{parent} + , d_ptr(new StatusNotifierHostPrivate(this)) +{ + Q_D(StatusNotifierHost); + + d->m_serviceName = "org.kde.StatusNotifierHost-"; + d->m_serviceName.append(QString::number(QCoreApplication::applicationPid())); + qCDebug(SYSTEM_TRAY) << "Register Host service" << d->m_serviceName << ":" << d->m_bus.registerService(d->m_serviceName); + + d->m_watcher = new StatusNotifierWatcher("org.kde.StatusNotifierWatcher", "/StatusNotifierWatcher", d->m_bus, this); + qCDebug(SYSTEM_TRAY) << "Find Watcher " << d->m_watcher->isValid() << " with protocol " << d->m_watcher->protocolVersion(); + + if(!d->m_watcher->isValid()) + { + qCWarning(SYSTEM_TRAY) << "Can't find watcher org.kde.StatusNotifierWatcher/StatusNotifierWatcher"; + } + + + d->m_watcher->RegisterStatusNotifierHost(d->m_serviceName); + + QObject::connect(d->m_watcher, &StatusNotifierWatcher::StatusNotifierItemRegistered, [this](QString in0){ + Q_D(StatusNotifierHost); + d->addNotifier(in0); + }); + + QObject::connect(d->m_watcher, &StatusNotifierWatcher::StatusNotifierItemUnregistered, [this](QString in0){ + Q_D(StatusNotifierHost); + d->removeNotifier(in0); + }); + + for(QString item: d->m_watcher->registeredStatusNotifierItems()) + { + d->addNotifier(item); + } +} + +StatusNotifierHost::~StatusNotifierHost() = default; + +int StatusNotifierHost::rowCount(const QModelIndex &parent) const +{ + Q_D(const StatusNotifierHost); + return d->m_items.size(); +} + +QVariant StatusNotifierHost::data(const QModelIndex &index, int role) const +{ + Q_D(const StatusNotifierHost); + + if(index.row() > d->m_itemsorder.count()) + return QVariant(); + + const StatusNotifierItem *stat = d->m_items[d->m_itemsorder[index.row()]]; + + switch(role) { + case CategoryRole: + return stat->category(); + case IdRole: + return stat->id(); + case TitleRole: + return stat->title(); + case StatusRole: + return stat->status(); + case WindowIdRole: + return stat->windowId(); + case IconNameRole: + return stat->iconName(); + case OverlayIconNameRole: + return stat->overlayIconName(); + case AttentionIconNameRole: + return stat->attentionIconName(); + case AttentionMovieNameRole: + return stat->attentionMovieName(); + } + + return QVariant(); +} + +QHash<int, QByteArray> StatusNotifierHost::roleNames() const +{ + QHash<int, QByteArray> roles = QAbstractItemModel::roleNames(); + roles[CategoryRole] = "category"; + roles[IdRole] = "idItem"; + roles[TitleRole] = "title"; + roles[StatusRole] = "status"; + roles[WindowIdRole] = "windowId"; + roles[IconNameRole] = "iconName"; + roles[OverlayIconNameRole] = "overlayIconName"; + roles[AttentionIconNameRole] = "attentionIconName"; + roles[AttentionMovieNameRole] = "attentionMovieName"; + return roles; +} + +void StatusNotifierHost::activate(int x, int y, QString id, int mode) +{ + Q_D(const StatusNotifierHost); + for(const QString &key: d->m_itemsorder) + { + if(d->m_items[key]->id() == id) + { + if(mode == 1) + d->m_items[key]->SecondaryActivate(x, y); + else + d->m_items[key]->Activate(x, y); + return; + } + } +} diff --git a/src/lib/xdgstatusnotifierhost.h b/src/lib/xdgstatusnotifierhost.h new file mode 100644 index 0000000..20c36ed --- /dev/null +++ b/src/lib/xdgstatusnotifierhost.h @@ -0,0 +1,44 @@ +#ifndef XDGSTATUSNOTIFIERHOST_H +#define XDGSTATUSNOTIFIERHOST_H + +#include <QAbstractListModel> +#include <QObject> +#include <QScopedPointer> + +namespace xdg { + class StatusNotifierHostPrivate; + + class StatusNotifierHost : public QAbstractListModel + { + Q_OBJECT + public: + explicit StatusNotifierHost(QObject *parent = nullptr); + virtual ~StatusNotifierHost(); + + enum XdgDesktopEntriesRoles { + CategoryRole = Qt::UserRole + 1, + IdRole, + TitleRole, + StatusRole, + WindowIdRole, + IconNameRole, + OverlayIconNameRole, + AttentionIconNameRole, + AttentionMovieNameRole + }; + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + QHash<int, QByteArray> roleNames() const override; + + public slots: + void activate(int x, int y, QString id, int mode); + + private: + Q_DISABLE_COPY(StatusNotifierHost) + Q_DECLARE_PRIVATE(StatusNotifierHost) + QScopedPointer<StatusNotifierHostPrivate> const d_ptr; + }; +}; + +#endif // XDGSTATUSNOTIFIERHOST_H diff --git a/src/lib/xdgstatusnotifieritem.cpp b/src/lib/xdgstatusnotifieritem.cpp new file mode 100644 index 0000000..1a14846 --- /dev/null +++ b/src/lib/xdgstatusnotifieritem.cpp @@ -0,0 +1,169 @@ +#include "xdgstatusnotifieritem.h" +#include <statusnotifieritemadaptor.h> +#include <XdgStatusNotifierWatcherIface.h> +#include <QDBusConnection> +#include <QGuiApplication> +#include <debug.h> + +using namespace xdg; + +StatusNotifierItem::StatusNotifierItem(QObject *parent) + : QObject{parent} +{ + QDBusConnection bus = QDBusConnection::sessionBus(); + if (!bus.isConnected()) { + qCWarning(SYSTEM_TRAY) << "Cannot connect to the D-Bus session bus.\n" << "Please check your system settings and try again.\n"; + } + QString serviceName = "org.xdgutils."; + setId((QGuiApplication::desktopFileName().isEmpty())?(QCoreApplication::applicationName()):(QGuiApplication::desktopFileName())); + serviceName.append(Id()); + serviceName.append("-"); + serviceName.append(QString::number(QCoreApplication::applicationPid())); + qCDebug(SYSTEM_TRAY) << "Register Item service" << serviceName << ":" << bus.registerService(serviceName); + new StatusNotifierItemAdaptor(this); + bus.registerObject("/StatusNotifierItem", this); + OrgKdeStatusNotifierWatcherInterface *watcher = new OrgKdeStatusNotifierWatcherInterface("org.kde.StatusNotifierWatcher", "/StatusNotifierWatcher", bus); + watcher->RegisterStatusNotifierItem(serviceName); +} + +QString StatusNotifierItem::Title() const +{ + return m_Title; +} + +void StatusNotifierItem::setTitle(const QString &newTitle) +{ + if (m_Title == newTitle) + return; + m_Title = newTitle; + emit NewTitle(); + emit TitleChanged(); +} + +QString StatusNotifierItem::IconName() const +{ + return m_IconName; +} + +void StatusNotifierItem::setIconName(const QString &newIconName) +{ + if (m_IconName == newIconName) + return; + m_IconName = newIconName; + emit IconNameChanged(); + emit NewIcon(); +} + + +void StatusNotifierItem::Activate(uint x, uint y) +{ + emit activated(x, y); +} + +void StatusNotifierItem::SecondaryActivate(uint x, uint y) +{ + emit secondaryActivated(x, y); +} + +QString StatusNotifierItem::Status() const +{ + return m_Status; +} + +void StatusNotifierItem::setStatus(const QString &newStatus) +{ + if (m_Status == newStatus) + return; + if(!xdgStatus.contains(newStatus)) + { + qCWarning(SYSTEM_TRAY) << "Status " << newStatus << "not authorized"; + return; + } + m_Status = newStatus; + emit StatusChanged(); + emit NewStatus(m_Status); +} + +QString StatusNotifierItem::OverlayIconName() const +{ + return m_OverlayIconName; +} + +void StatusNotifierItem::setOverlayIconName(const QString &newOverlayIconName) +{ + if (m_OverlayIconName == newOverlayIconName) + return; + m_OverlayIconName = newOverlayIconName; + emit overlayIconNameChanged(); + emit NewOverlayIcon(); +} + +QString StatusNotifierItem::Category() const +{ + return m_Category; +} + +void StatusNotifierItem::setCategory(const QString &newCategory) +{ + if (m_Category == newCategory) + return; + if(!xdgCategories.contains(newCategory)) + { + qCWarning(SYSTEM_TRAY) << "Category " << newCategory << "not authorized"; + return; + } + m_Category = newCategory; + emit CategoryChanged(); +} + +QString StatusNotifierItem::Id() const +{ + return m_Id; +} + +void StatusNotifierItem::setId(const QString &newId) +{ + if (m_Id == newId) + return; + m_Id = newId; + emit IdChanged(); +} + +QString StatusNotifierItem::WindowId() const +{ + return m_WindowId; +} + +void StatusNotifierItem::setWindowId(const QString &newWindowId) +{ + if (m_WindowId == newWindowId) + return; + m_WindowId = newWindowId; + emit WindowIdChanged(); +} + +QString StatusNotifierItem::AttentionIconName() const +{ + return m_AttentionIconName; +} + +void StatusNotifierItem::setAttentionIconName(const QString &newAttentionIconName) +{ + if (m_AttentionIconName == newAttentionIconName) + return; + m_AttentionIconName = newAttentionIconName; + emit AttentionIconNameChanged(); +} + +QString StatusNotifierItem::AttentionMovieName() const +{ + return m_AttentionMovieName; +} + +void StatusNotifierItem::setAttentionMovieName(const QString &newAttentionMovieName) +{ + if (m_AttentionMovieName == newAttentionMovieName) + return; + m_AttentionMovieName = newAttentionMovieName; + emit AttentionMovieNameChanged(); +} diff --git a/src/lib/xdgstatusnotifieritem.h b/src/lib/xdgstatusnotifieritem.h new file mode 100644 index 0000000..7f881a4 --- /dev/null +++ b/src/lib/xdgstatusnotifieritem.h @@ -0,0 +1,119 @@ +#ifndef XDGSTATUSNOTIFIERITEM_H +#define XDGSTATUSNOTIFIERITEM_H + +#include <QObject> + +#define XDG_CATEGORY_APPLICATIONSTATUS "ApplicationStatus" +#define XDG_CATEGORY_COMMUNICATIONS "Communications" +#define XDG_CATEGORY_SYSTEMSERVICE "SystemServices" +#define XDG_CATEGORY_HARDWARE "Hardware" + +#define XDG_STATUS_PASSIVE "Passive" +#define XDG_STATUS_ACTIVE "Active" +#define XDG_STATUS_NEEDSATTENTION "NeedsAttention" + +namespace xdg { + + class StatusNotifierItem : public QObject + { + Q_OBJECT + + /* This properties are used for DBus object connection */ + Q_PROPERTY(QString Category READ Category WRITE setCategory NOTIFY CategoryChanged) + Q_PROPERTY(QString Id READ Id WRITE setId NOTIFY IdChanged) + Q_PROPERTY(QString Title READ Title WRITE setTitle NOTIFY TitleChanged) + Q_PROPERTY(QString Status READ Status WRITE setStatus NOTIFY StatusChanged) + Q_PROPERTY(QString WindowId READ WindowId WRITE setWindowId NOTIFY WindowIdChanged) + Q_PROPERTY(QString IconName READ IconName WRITE setIconName NOTIFY IconNameChanged) + Q_PROPERTY(QString OverlayIconName READ OverlayIconName WRITE setOverlayIconName NOTIFY overlayIconNameChanged) + Q_PROPERTY(QString AttentionIconName READ AttentionIconName WRITE setAttentionIconName NOTIFY AttentionIconNameChanged) + Q_PROPERTY(QString AttentionMovieName READ AttentionMovieName WRITE setAttentionMovieName NOTIFY AttentionMovieNameChanged) + + /* This properties are used for QML */ + Q_PROPERTY(QString category READ Category WRITE setCategory NOTIFY CategoryChanged) + Q_PROPERTY(QString idItem READ Id WRITE setId NOTIFY IdChanged) + Q_PROPERTY(QString title READ Title WRITE setTitle NOTIFY TitleChanged) + Q_PROPERTY(QString status READ Status WRITE setStatus NOTIFY StatusChanged) + Q_PROPERTY(QString windowId READ WindowId WRITE setWindowId NOTIFY WindowIdChanged) + Q_PROPERTY(QString iconName READ IconName WRITE setIconName NOTIFY IconNameChanged) + Q_PROPERTY(QString overlayIconName READ OverlayIconName WRITE setOverlayIconName NOTIFY overlayIconNameChanged) + Q_PROPERTY(QString attentionIconName READ AttentionIconName WRITE setAttentionIconName NOTIFY AttentionIconNameChanged) + Q_PROPERTY(QString attentionMovieName READ AttentionMovieName WRITE setAttentionMovieName NOTIFY AttentionMovieNameChanged) + + public: + const QStringList xdgCategories {XDG_CATEGORY_APPLICATIONSTATUS, XDG_CATEGORY_COMMUNICATIONS, XDG_CATEGORY_SYSTEMSERVICE, XDG_CATEGORY_HARDWARE}; + const QStringList xdgStatus {XDG_STATUS_PASSIVE, XDG_STATUS_ACTIVE, XDG_STATUS_NEEDSATTENTION}; + + explicit StatusNotifierItem(QObject *parent = nullptr); + + QString Title() const; + void setTitle(const QString &newTitle); + + QString IconName() const; + void setIconName(const QString &newIconName); + + QString Status() const; + void setStatus(const QString &newStatus); + + + QString OverlayIconName() const; + void setOverlayIconName(const QString &newOverlayIconName); + + QString Category() const; + void setCategory(const QString &newCategory); + + QString Id() const; + void setId(const QString &newId); + + QString WindowId() const; + void setWindowId(const QString &newWindowId); + + QString AttentionIconName() const; + void setAttentionIconName(const QString &newAttentionIconName); + + QString AttentionMovieName() const; + void setAttentionMovieName(const QString &newAttentionMovieName); + + public slots: + /* DBus API */ + virtual void Activate(uint x, uint y); + virtual void SecondaryActivate(uint x, uint y); + + signals: + void NewTitle(); + void NewIcon(); + void NewAttentionIcon(); + void NewOverlayIcon(); + void NewToolTip(); + void NewStatus(QString status); + + void TitleChanged(); + void IconNameChanged(); + void StatusChanged(); + void overlayIconNameChanged(); + void CategoryChanged(); + void IdChanged(); + void WindowIdChanged(); + + void activated(uint x, uint y); + void secondaryActivated(uint x, uint y); + + + void AttentionIconNameChanged(); + + void AttentionMovieNameChanged(); + + private: + QString m_Title; + QString m_IconName; + QString m_Status {XDG_STATUS_ACTIVE}; + QString m_OverlayIconName; + QString m_Category {XDG_CATEGORY_APPLICATIONSTATUS}; + QString m_Id; + QString m_WindowId; + QString m_AttentionIconName; + QString m_AttentionMovieName; + }; +}; + +#endif // XDGSTATUSNOTIFIERITEM_H diff --git a/src/lib/xdgstatusnotifierwatcher.cpp b/src/lib/xdgstatusnotifierwatcher.cpp new file mode 100644 index 0000000..c07a0f6 --- /dev/null +++ b/src/lib/xdgstatusnotifierwatcher.cpp @@ -0,0 +1,116 @@ +#include "xdgstatusnotifierwatcher.h" +#include <statusnotifierwatcheradaptor.h> +#include <XdgStatusNotifierItemIface.h> +#include <QDBusConnection> +#include <debug.h> + +using namespace xdg; + +StatusNotifierWatcher::StatusNotifierWatcher(QObject *parent) + : QObject{parent} +{ + QDBusConnection bus = QDBusConnection::sessionBus(); + if (!bus.isConnected()) { + qCWarning(SYSTEM_TRAY) << "Cannot connect to the D-Bus session bus.\n" << "Please check your system settings and try again.\n"; + } + if(!bus.registerService("org.kde.StatusNotifierWatcher")) + { + qCWarning(SYSTEM_TRAY) << "Don't register StatusNotifierWatcher service, probably already exists"; + return; + } + qCDebug(SYSTEM_TRAY) << "Register StatusNotifierWatcher service"; + new StatusNotifierWatcherAdaptor(this); + bus.registerObject("/StatusNotifierWatcher", this); + + m_serviceWatcher = new QDBusServiceWatcher(this); + m_serviceWatcher->setConnection(bus); + m_serviceWatcher->setWatchMode(QDBusServiceWatcher::WatchForUnregistration); + + connect(m_serviceWatcher, &QDBusServiceWatcher::serviceUnregistered, this, &StatusNotifierWatcher::_serviceUnregistered); +} + +StatusNotifierWatcher::~StatusNotifierWatcher() +{ + +} + +void StatusNotifierWatcher::RegisterStatusNotifierItem(const QString &service) +{ + QString path; + path = QStringLiteral("/StatusNotifierItem"); + qCDebug(SYSTEM_TRAY) << "Watcher: RegisterStatusNotifierItem(" << service << ")"; + QString notifierItemId = service + path; + if (m_items.contains(notifierItemId)) { + return; + } + m_serviceWatcher->addWatchedService(service); + if (QDBusConnection::sessionBus().interface()->isServiceRegistered(service).value()) { + // check if the service has registered a SystemTray object + org::kde::StatusNotifierItem trayclient(service, path, QDBusConnection::sessionBus()); + if (trayclient.isValid()) { + qDebug() << "Registering" << notifierItemId << "to system tray"; + m_items.append(notifierItemId); + emit StatusNotifierItemRegistered(notifierItemId); + } else { + m_serviceWatcher->removeWatchedService(service); + } + } else { + m_serviceWatcher->removeWatchedService(service); + } +} + +void StatusNotifierWatcher::RegisterStatusNotifierHost(const QString &service) +{ + qCDebug(SYSTEM_TRAY) << "Watcher: RegisterStatusNotifierHost(" << service << ")"; + if (m_hosts.contains(service)) { + return; + } + m_serviceWatcher->addWatchedService(service); + m_hosts.append(service); +} + +QStringList StatusNotifierWatcher::RegisteredStatusNotifierItems() const +{ + return m_items; +} + + +bool StatusNotifierWatcher::IsStatusNotifierHostRegistered() const +{ + return m_hosts.size() > 0; +} + + +int StatusNotifierWatcher::ProtocolVersion() const +{ + return XDG_STATUS_NOTIFIER_WATCHER_PROTOCOL_VERSION; +} + +void StatusNotifierWatcher::_serviceUnregistered(const QString &name) +{ + qCDebug(SYSTEM_TRAY) << "Service " << name << "unregistered (ITEMS:" << m_items << " HOSTS:" << m_hosts << ")"; + m_serviceWatcher->removeWatchedService(name); + + const QString match = name + QLatin1Char('/'); + QStringList::Iterator it = m_items.begin(); + while (it != m_items.end()) { + if (it->startsWith(match)) { + QString name = *it; + it = m_items.erase(it); + emit StatusNotifierItemUnregistered(name); + } else { + ++it; + } + } + + it = m_hosts.begin(); + while (it != m_hosts.end()) { + if (it->startsWith(match)) { + QString name = *it; + it = m_hosts.erase(it); + emit StatusNotifierHostUnregistered(); + } else { + ++it; + } + } +} diff --git a/src/lib/xdgstatusnotifierwatcher.h b/src/lib/xdgstatusnotifierwatcher.h new file mode 100644 index 0000000..8e2275c --- /dev/null +++ b/src/lib/xdgstatusnotifierwatcher.h @@ -0,0 +1,48 @@ +#ifndef XDGSTATUSNOTIFIERWATCHER_H +#define XDGSTATUSNOTIFIERWATCHER_H + +#include <QObject> + +class QDBusServiceWatcher; +#define XDG_STATUS_NOTIFIER_WATCHER_PROTOCOL_VERSION 1 + +namespace xdg { + + class StatusNotifierWatcher: public QObject + { + Q_OBJECT + + Q_PROPERTY(QStringList RegisteredStatusNotifierItems READ RegisteredStatusNotifierItems NOTIFY RegisteredStatusNotifierItemsChanged) + Q_PROPERTY(bool IsStatusNotifierHostRegistered READ IsStatusNotifierHostRegistered NOTIFY IsStatusNotifierHostRegisteredChanged) + Q_PROPERTY(int ProtocolVersion READ ProtocolVersion CONSTANT) + + public: + StatusNotifierWatcher(QObject *parent = nullptr); + virtual ~StatusNotifierWatcher(); + + void RegisterStatusNotifierItem(const QString &service); + void RegisterStatusNotifierHost(const QString &service); + + QStringList RegisteredStatusNotifierItems() const; + bool IsStatusNotifierHostRegistered() const; + int ProtocolVersion() const; + + signals: + void StatusNotifierItemRegistered(QString id); + void StatusNotifierItemUnregistered(QString id); + void StatusNotifierHostRegistered(); + void StatusNotifierHostUnregistered(); + void RegisteredStatusNotifierItemsChanged(); + void IsStatusNotifierHostRegisteredChanged(); + + private: + QStringList m_hosts; + QStringList m_items; + QDBusServiceWatcher *m_serviceWatcher = nullptr; + + private slots: + void _serviceUnregistered(const QString &name); + }; +}; + +#endif // XDGSTATUSNOTIFIERWATCHER_H |