diff --git a/CMakeLists.txt b/CMakeLists.txt index 00003dc..96e4e0b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -133,6 +133,29 @@ else() endif() endif() +# configure support for D-Bus notifications +option(DBUS_NOTIFICATIONS "enables support for D-Bus notifications" ${UNIX}) +set(DBUS_NOTIFICATIONS_FILE_NAME misc/dbusnotification) +if(DBUS_NOTIFICATIONS) + list(APPEND HEADER_FILES + ${DBUS_NOTIFICATIONS_FILE_NAME}.h + ) + list(APPEND SRC_FILES + ${DBUS_NOTIFICATIONS_FILE_NAME}.cpp + ) + list(APPEND DBUS_FILES + dbus/org.freedesktop.Notifications.xml + ) + list(APPEND META_PUBLIC_COMPILE_DEFINITIONS QT_UTILITIES_SUPPORT_DBUS_NOTIFICATIONS) + message(STATUS "D-Bus notifications enabled") +else() + list(APPEND DOC_ONLY_FILES + ${DBUS_NOTIFICATIONS_FILE_NAME}.h + ${DBUS_NOTIFICATIONS_FILE_NAME}.cpp + ) + message(STATUS "D-Bus notifications disabled") +endif() + # find c++utilities find_package(c++utilities 4.0.0 REQUIRED) use_cpp_utilities() diff --git a/dbus/org.freedesktop.Notifications.xml b/dbus/org.freedesktop.Notifications.xml new file mode 100644 index 0000000..62345f2 --- /dev/null +++ b/dbus/org.freedesktop.Notifications.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/misc/dbusnotification.cpp b/misc/dbusnotification.cpp new file mode 100644 index 0000000..8835dc6 --- /dev/null +++ b/misc/dbusnotification.cpp @@ -0,0 +1,281 @@ +#include "./dbusnotification.h" +#include "notificationsinterface.h" + +#include +#include +#include + +#include + +using namespace std; + +namespace MiscUtils { + +/*! + * \class DBusNotification + * \brief The DBusNotification class emits D-Bus notifications. + * + * D-Bus notifications are only available if the library has been compiled with support for it by specifying + * CMake option `DBUS_NOTIFICATIONS`. If support is available, the macro `QT_UTILITIES_SUPPORT_DBUS_NOTIFICATIONS` + * is defined. + * + * **Usage** + * + * First create a new instance. The constructor allows to set basic parameters. To set more parameters, use + * setter methods. Call show() to actually show the notification. This method can also be used to update + * the currently shown notification (it will not be updated automatically by just using the setter methods). + * + * \sa https://developer.gnome.org/notification-spec + */ + +/// \cond +static std::map pendingNotifications; +OrgFreedesktopNotificationsInterface *DBusNotification::m_dbusInterface = nullptr; +/// \endcond + +/*! + * \brief Creates a new notification (which is *not* shown instantly). + */ +DBusNotification::DBusNotification(const QString &title, NotificationIcon icon, int timeout, QObject *parent) : + QObject(parent), + m_id(0), + m_watcher(nullptr), + m_title(title), + m_timeout(timeout) +{ + initInterface(); + setIcon(icon); +} + +/*! + * \brief Creates a new notification (which is *not* shown instantly). + */ +DBusNotification::DBusNotification(const QString &title, const QString &icon, int timeout, QObject *parent) : + QObject(parent), + m_id(0), + m_watcher(nullptr), + m_title(title), + m_icon(icon), + m_timeout(timeout) +{ + initInterface(); +} + +/*! + * \brief Initializes the static interface object if not done yet. + */ +void DBusNotification::initInterface() +{ + if(!m_dbusInterface) { + m_dbusInterface = new OrgFreedesktopNotificationsInterface(QStringLiteral("org.freedesktop.Notifications"), QStringLiteral("/org/freedesktop/Notifications"), QDBusConnection::sessionBus()); + connect(m_dbusInterface, &OrgFreedesktopNotificationsInterface::ActionInvoked, &DBusNotification::handleActionInvoked); + connect(m_dbusInterface, &OrgFreedesktopNotificationsInterface::NotificationClosed, &DBusNotification::handleNotificationClosed); + } +} + +/*! + * \brief Closes the notification if still shown and delete the object. + */ +DBusNotification::~DBusNotification() +{ + auto i = pendingNotifications.find(m_id); + if(i != pendingNotifications.end()) { + pendingNotifications.erase(i); + } + hide(); +} + +/*! + * \brief Returns whether the notification D-Bus daemon is running. + */ +bool DBusNotification::isAvailable() +{ + initInterface(); + return m_dbusInterface->isValid(); +} + +/*! + * \brief Sets the icon to one of the pre-defined notification icons. + */ +void DBusNotification::setIcon(NotificationIcon icon) +{ + switch(icon) { + case NotificationIcon::Information: + m_icon = QStringLiteral("dialog-information"); break; + case NotificationIcon::Warning: + m_icon = QStringLiteral("dialog-warning"); break; + case NotificationIcon::Critical: + m_icon = QStringLiteral("dialog-critical"); break; + default: + ; + } +} + +/*! + * \brief Makes the notification object delete itself when the notification has been closed or an error occured. + */ +void DBusNotification::deleteOnCloseOrError() +{ + connect(this, &DBusNotification::closed, this, &DBusNotification::deleteLater); + connect(this, &DBusNotification::error, this, &DBusNotification::deleteLater); +} + +/*! + * \brief Shows the notification. + * \remarks If called when a previous notification is still shown, the previous notification is updated. + * \returns Returns false is the D-Bus daemon isn't reachable and true otherwise. + */ +bool DBusNotification::show() +{ + if(!m_dbusInterface->isValid()) { + emit error(); + return false; + } + + delete m_watcher; + m_watcher = new QDBusPendingCallWatcher(m_dbusInterface->Notify(QCoreApplication::applicationName(), m_id, m_icon, m_title, m_msg, m_actions, m_hints, m_timeout), this); + connect(m_watcher, &QDBusPendingCallWatcher::finished, this, &DBusNotification::handleNotifyResult); + return true; +} + +/*! + * \brief Updates the message and shows/updates the notification. + * \remarks If called when a previous notification is still shown, the previous notification is updated. + * \returns Returns false is the D-Bus daemon isn't reachable and true otherwise. The message is updated in any case. + */ +bool DBusNotification::show(const QString &message) +{ + m_msg = message; + return show(); +} + +/*! + * \brief Updates the message and shows/updates the notification. + * \remarks + * - If called when a previous notification is still shown, the previous notification is updated. In this + * case the specified \a line will be appended to the current message. + * - If called when no previous notification is still shown, the previous message is completely replaced + * by \a line and shown as a new notification. + * \returns Returns false is the D-Bus daemon isn't reachable and true otherwise. The message is updated in any case. + */ +bool DBusNotification::update(const QString &line) +{ + if(!isVisible() || m_msg.isEmpty()) { + m_msg = line; + } else { + if(!m_msg.startsWith(QStringLiteral("•"))) { + m_msg.insert(0, QStringLiteral("• ")); + } + m_msg.append(QStringLiteral("\n• ")); + m_msg.append(line); + } + return show(); +} + +/*! + * \brief Hides the notification (if still visible). + * \remarks On success, the signal closed() is emitted with the reason NotificationCloseReason::Manually. + */ +void DBusNotification::hide() +{ + if(m_id) { + m_dbusInterface->CloseNotification(m_id); + } +} + +/*! + * \brief Handles the results of the Notify D-Bus call. + */ +void DBusNotification::handleNotifyResult(QDBusPendingCallWatcher *watcher) +{ + if(watcher != m_watcher) { + return; + } + + watcher->deleteLater(); + m_watcher = nullptr; + + QDBusPendingReply returnValue = *watcher; + if (returnValue.isError()) { + deleteLater(); + emit error(); + } else { + pendingNotifications[m_id = returnValue.argumentAt<0>()] = this; + emit shown(); + } +} + +/*! + * \brief Handles the NotificationClosed D-Bus signal. + */ +void DBusNotification::handleNotificationClosed(uint id, uint reason) +{ + auto i = pendingNotifications.find(id); + if(i != pendingNotifications.end()) { + DBusNotification *notification = i->second; + notification->m_id = 0; + emit notification->closed(reason >= 1 && reason <= 3 ? static_cast(reason) : NotificationCloseReason::Undefined); + pendingNotifications.erase(i); + } +} + +/*! + * \brief Handles the ActionInvoked D-Bus signal. + */ +void DBusNotification::handleActionInvoked(uint id, const QString &action) +{ + auto i = pendingNotifications.find(id); + if(i != pendingNotifications.end()) { + emit i->second->actionInvoked(action); + } +} + +/*! + * \fn DBusNotification::message() + * \brief Returns the assigned message. + * \sa setMessage() for more details. + */ + +/*! + * \fn DBusNotification::setMessage() + * \brief Sets the message to be shown. + * \remarks + * - Might also be set via show() and update(). + * - Can contain the following HTML tags: ``, ``, ``, `` and `...` + */ + +/*! + * \fn DBusNotification::timeout() + * \brief Returns the number of milliseconds the notification will be visible after calling show(). + * \sa setTimeout() for more details. + */ + +/*! + * \fn DBusNotification::setTimeout() + * \brief Sets the number of milliseconds the notification will be visible after calling show(). + * \remarks + * - Set to 0 for non-expiring notifications. + * - Set to -1 to let the notification daemon decide. + */ + +/*! + * \fn DBusNotification::actions() + * \brief Returns the assigned actions. + * \sa setActions() for more details. + */ + +/*! + * \fn DBusNotification::setActions() + * \brief Sets the list of available actions. + * \remarks + * The list consists of pairs of action IDs and action labels, eg. + * `QStringList({QStringLiteral("first_id"), tr("First action"), QStringLiteral("second_id"), tr("Second action"), ...})` + * \sa actionInvoked() signal + */ + +/*! + * \fn DBusNotification::isVisible() + * \brief Returns whether the notification is (still) visible. + */ + +} diff --git a/misc/dbusnotification.h b/misc/dbusnotification.h new file mode 100644 index 0000000..d1f5cee --- /dev/null +++ b/misc/dbusnotification.h @@ -0,0 +1,175 @@ +#ifndef MISC_UTILS_NOTIFICATION_H +#define MISC_UTILS_NOTIFICATION_H + +#include "../global.h" + +#include +#include + +QT_FORWARD_DECLARE_CLASS(QDBusPendingCallWatcher) + +class OrgFreedesktopNotificationsInterface; + +namespace MiscUtils { + +enum class NotificationIcon +{ + NoIcon, + Information, + Warning, + Critical +}; + +enum class NotificationCloseReason +{ + Undefined, + Expired, + Dismissed, + Manually +}; + +class QT_UTILITIES_EXPORT DBusNotification : public QObject +{ + Q_OBJECT + Q_PROPERTY(QString title READ title WRITE setTitle) + Q_PROPERTY(QString message READ message WRITE setMessage) + Q_PROPERTY(QString icon READ icon WRITE setIcon) + Q_PROPERTY(int timeout READ timeout WRITE setTimeout) + Q_PROPERTY(QStringList actions READ actions WRITE setActions) + Q_PROPERTY(bool visible READ isVisible) + +public: + explicit DBusNotification(const QString &title, NotificationIcon icon = NotificationIcon::Information, int timeout = 10000, QObject *parent = nullptr); + explicit DBusNotification(const QString &title, const QString &icon, int timeout = 10000, QObject *parent = nullptr); + ~DBusNotification(); + + static bool isAvailable(); + const QString &title() const; + void setTitle(const QString &title); + const QString &message() const; + void setMessage(const QString &message); + const QString &icon() const; + void setIcon(const QString &icon); + void setIcon(NotificationIcon icon); + int timeout() const; + void setTimeout(int timeout); + const QStringList &actions() const; + void setActions(const QStringList &actions); + const QVariantMap &hints() const; + QVariantMap &hints(); + bool isVisible() const; + void deleteOnCloseOrError(); + +public Q_SLOTS: + bool show(); + bool show(const QString &message); + bool update(const QString &line); + void hide(); + +Q_SIGNALS: + /// \brief Emitted when the notification could be shown successful. + void shown(); + /// \brief Emitted when the notification couldn't be shown. + void error(); + /// \brief Emitted when the notification has been closed. + void closed(NotificationCloseReason reason); + /// \brief Emitted when \a action has been invoked. + void actionInvoked(const QString &action); + +private Q_SLOTS: + void handleNotifyResult(QDBusPendingCallWatcher *); + static void handleNotificationClosed(uint id, uint reason); + static void handleActionInvoked(uint id, const QString &action); + +private: + static void initInterface(); + + uint m_id; + QDBusPendingCallWatcher *m_watcher; + QString m_title; + QString m_msg; + QString m_icon; + int m_timeout; + QStringList m_actions; + QVariantMap m_hints; + static OrgFreedesktopNotificationsInterface *m_dbusInterface; +}; + +inline const QString &DBusNotification::title() const +{ + return m_title; +} + +inline void DBusNotification::setTitle(const QString &title) +{ + m_title = title; +} + +inline const QString &DBusNotification::message() const +{ + return m_msg; +} + +inline void DBusNotification::setMessage(const QString &message) +{ + m_msg = message; +} + +/*! + * \brief Returns the icon name. + * \sa setIcon() for more details + */ +inline const QString &DBusNotification::icon() const +{ + return m_icon; +} + +/*! + * \brief Sets the icon name. + * \remarks + * The specified \a icon should be either an URI (file:// is the only URI schema supported + * right now) or a name in an icon theme. + */ +inline void DBusNotification::setIcon(const QString &icon) +{ + m_icon = icon; +} + +inline int DBusNotification::timeout() const +{ + return m_timeout; +} + +inline void DBusNotification::setTimeout(int timeout) +{ + m_timeout = timeout; +} + +inline const QStringList &DBusNotification::actions() const +{ + return m_actions; +} + +inline void DBusNotification::setActions(const QStringList &actions) +{ + m_actions = actions; +} + +inline const QVariantMap &DBusNotification::hints() const +{ + return m_hints; +} + +inline QVariantMap &DBusNotification::hints() +{ + return m_hints; +} + +inline bool DBusNotification::isVisible() const +{ + return m_id != 0; +} + +} + +#endif // MISC_UTILS_NOTIFICATION_H