/* This file is part of the KDE libraries Copyright (C) 2023 Ivailo Monev This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2, as published by the Free Software Foundation. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "knotification.h" #include "kglobal.h" #include "kcomponentdata.h" #include "kconfig.h" #include "kconfiggroup.h" #include "kstandarddirs.h" #include "kwindowsystem.h" #include "kiconloader.h" #include "kpassivepopup.h" #include "kdirwatch.h" #include "kdebug.h" #include #include #include #include #include // see kdebug.areas static const int s_knotificationarea = 299; static const int s_closedelay = 1000; // ms // strings cache static const QString s_popupaction = QString::fromLatin1("Popup"); static const QString s_taskbaraction = QString::fromLatin1("Taskbar"); static const QString s_soundaction = QString::fromLatin1("Sound"); static const QString s_closemethod = QString::fromLatin1("closeNotification"); static const QString s_playmethod = QString::fromLatin1("play"); static const QString s_addmethod = QString::fromLatin1("addNotification"); static const QString s_updatemethod = QString::fromLatin1("updateNotification"); static QString kNotifyID(const KNotification *notification) { return QString::number(quintptr(notification), 16); } class KNotificationManager : public QObject { Q_OBJECT public: KNotificationManager(); ~KNotificationManager(); void send(KNotification *notification, const bool persistent); void close(KNotification *notification); private Q_SLOTS: void slotCloseRequested(const QString &eventid); void slotActionRequested(const QString &eventid, const QString &action); void slotDirty(const QString &path); private: QDBusInterface* createInterface(const QString &service); bool callAddMethod(const QString ¬ifyid); void callUpdateMethod(const QString ¬ifyid, const QVariantMap &eventdata); void callCloseMethod(const QString ¬ifyid); QMutex m_mutex; KConfig *m_config; KDirWatch m_configwatch; QDBusInterface* m_desktopiface; QDBusInterface* m_windowediface; QDBusInterface* m_kaudioplayeriface; QMap m_notifications; }; K_GLOBAL_STATIC(KNotificationManager, kNotificationManager); KNotificationManager::KNotificationManager() : m_config(nullptr), m_configwatch(this), m_desktopiface(nullptr), m_windowediface(nullptr), m_kaudioplayeriface(nullptr) { // NOTE: the default poll interval of QFileSystemWatcher is 1sec m_configwatch.setInterval(5000); const QString knotificationrc = KGlobal::dirs()->saveLocation("config") + QLatin1String("knotificationrc"); // qDebug() << Q_FUNC_INFO << knotificationrc; Q_ASSERT(!knotificationrc.isEmpty()); m_configwatch.addFile(knotificationrc); const QStringList configdirs = KGlobal::dirs()->resourceDirs("config"); foreach (const QString &configdir, configdirs) { const QString notificationdir = configdir + QLatin1String("notifications/"); // qDebug() << Q_FUNC_INFO << notificationdir; m_configwatch.addDir(notificationdir); } slotDirty(QString()); connect(&m_configwatch, SIGNAL(dirty(QString)), this, SLOT(slotDirty(QString))); } KNotificationManager::~KNotificationManager() { delete m_config; } void KNotificationManager::send(KNotification *notification, const bool persistent) { Q_ASSERT(m_config); QMutexLocker locker(&m_mutex); const QString eventid = notification->eventID(); const QStringList spliteventid = eventid.split(QLatin1Char('/')); // qDebug() << Q_FUNC_INFO << spliteventid; if (spliteventid.size() != 2) { kWarning(s_knotificationarea) << "invalid notification ID" << eventid; return; } KConfigGroup globalgroup(m_config, spliteventid.at(0)); const QString globalcomment = globalgroup.readEntry("Comment"); KConfigGroup eventgroup(m_config, eventid); QString eventtitle = notification->title(); if (eventtitle.isEmpty()) { eventtitle = eventgroup.readEntry("Comment"); } if (eventtitle.isEmpty()) { eventtitle = globalcomment; } QString eventtext = notification->text(); if (eventtext.isEmpty()) { eventtext = eventgroup.readEntry("Name"); } if (eventtext.isEmpty()) { eventtext = globalgroup.readEntry("Name"); } QString eventicon = notification->icon(); if (eventicon.isEmpty()) { eventicon = eventgroup.readEntry("IconName"); } if (eventicon.isEmpty()) { eventicon = globalgroup.readEntry("IconName"); } QStringList eventactions = eventgroup.readEntry("Actions", QStringList()); if (eventactions.isEmpty()) { eventactions = globalgroup.readEntry("Actions", QStringList()); } // qDebug() << Q_FUNC_INFO << eventactions << notification->actions(); if (eventactions.contains(s_popupaction)) { if (!m_desktopiface) { m_desktopiface = createInterface("org.kde.plasma-desktop"); } if (!m_windowediface) { m_windowediface = createInterface("org.kde.plasma-windowed"); } if (!m_desktopiface->isValid() && !m_windowediface->isValid()) { kWarning(s_knotificationarea) << "notifications interface is not valid"; const QPixmap eventpixmap = KIconLoader::global()->loadIcon(eventicon, KIconLoader::Small); KPassivePopup* kpassivepopup = new KPassivePopup(notification->widget()); kpassivepopup->setTimeout(persistent ? 0 : -1); kpassivepopup->setView(eventtitle, eventtext, eventpixmap); kpassivepopup->setAutoDelete(true); // NOTE: KPassivePopup positions itself depending on the windows kpassivepopup->show(); } else { bool addnotification = false; const QString notifyid = kNotifyID(notification); QVariantMap eventdata = m_notifications.value(notification, QVariantMap()); if (eventdata.isEmpty()) { addnotification = true; } QStringList eventactions; // NOTE: there has to be id for each action, starting from 1 int actionscounter = 1; foreach (const QString &eventaction, notification->actions()) { eventactions.append(QString::number(actionscounter)); eventactions.append(eventaction); actionscounter++; } eventdata.insert("appIcon", eventicon); eventdata.insert("summary", eventtitle); // unused eventdata.insert("body", eventtext); eventdata.insert("actions", eventactions); // NOTE: has to be set to be configurable via plasma notifications applet eventdata.insert("appRealName", spliteventid.at(0)); if (addnotification && callAddMethod(notifyid)) { m_notifications.insert(notification, eventdata); } callUpdateMethod(notifyid, eventdata); } } if (eventactions.contains(s_soundaction)) { QString eventsound = eventgroup.readEntry("Sound"); if (eventsound.isEmpty()) { eventsound = globalgroup.readEntry("Sound"); } const QStringList eventsoundfiles = KGlobal::dirs()->findAllResources("sound", eventsound, KStandardDirs::Recursive); if (eventsoundfiles.isEmpty()) { kWarning(s_knotificationarea) << "sound not found" << eventsound; } else { kDebug(s_knotificationarea) << "playing notification sound" << eventsound; if (!m_kaudioplayeriface) { m_kaudioplayeriface = new QDBusInterface( "org.kde.kded", "/modules/kaudioplayer", "org.kde.kaudioplayer", QDBusConnection::sessionBus(), this ); } // the sound player is configurable and is used by the bball plasma applet for example QDBusReply playreply = m_kaudioplayeriface->call(s_playmethod, eventsoundfiles.first()); if (!playreply.isValid()) { kWarning(s_knotificationarea) << "invalid play reply" << playreply.error().message(); } } } if (eventactions.contains(s_taskbaraction)) { const QWidget* eventwidget = notification->widget(); if (!eventwidget) { kWarning(s_knotificationarea) << "taskbar event with no widget set" << eventid; } else { const WId eventwidgetid = eventwidget->winId(); kDebug(s_knotificationarea) << "marking notification task" << eventid << eventwidgetid; KWindowSystem::demandAttention(eventwidgetid); } } } void KNotificationManager::close(KNotification *notification) { QMutexLocker locker(&m_mutex); QMutableMapIterator iter(m_notifications); while (iter.hasNext()) { iter.next(); if (iter.key() == notification) { const QString notifyid = kNotifyID(iter.key()); iter.remove(); callCloseMethod(notifyid); break; } } } QDBusInterface* KNotificationManager::createInterface(const QString &service) { QDBusInterface* interface = new QDBusInterface( service, "/Notifications", "org.kde.Notifications", QDBusConnection::sessionBus(), this ); connect( interface, SIGNAL(closeRequested(QString)), this, SLOT(slotCloseRequested(QString)) ); connect( interface, SIGNAL(actionRequested(QString,QString)), this, SLOT(slotActionRequested(QString,QString)) ); return interface; } bool KNotificationManager::callAddMethod(const QString ¬ifyid) { QDBusReply desktopreply; QDBusReply windowedreply; if (m_desktopiface && m_desktopiface->isValid()) { desktopreply = m_desktopiface->call(s_addmethod, notifyid); if (!desktopreply.isValid()) { kWarning(s_knotificationarea) << "invalid desktop add reply" << desktopreply.error().message(); } } if (m_windowediface && m_windowediface->isValid()) { windowedreply = m_windowediface->call(s_addmethod, notifyid); if (!windowedreply.isValid()) { kWarning(s_knotificationarea) << "invalid windowed add reply" << windowedreply.error().message(); } } return (desktopreply.isValid() || windowedreply.isValid()); } void KNotificationManager::callUpdateMethod(const QString ¬ifyid, const QVariantMap &eventdata) { if (m_desktopiface && m_desktopiface->isValid()) { QDBusReply updatereply = m_desktopiface->call(s_updatemethod, notifyid, eventdata); if (!updatereply.isValid()) { kWarning(s_knotificationarea) << "invalid desktop update reply" << updatereply.error().message(); } } if (m_windowediface && m_windowediface->isValid()) { QDBusReply updatereply = m_windowediface->call(s_updatemethod, notifyid, eventdata); if (!updatereply.isValid()) { kWarning(s_knotificationarea) << "invalid windowed update reply" << updatereply.error().message(); } } } void KNotificationManager::callCloseMethod(const QString ¬ifyid) { if (m_desktopiface && m_desktopiface->isValid()) { QDBusReply closereply = m_desktopiface->call(s_closemethod, notifyid); if (!closereply.isValid()) { kWarning(s_knotificationarea) << "invalid desktop close reply" << closereply.error().message(); } } if (m_windowediface && m_windowediface->isValid()) { QDBusReply closereply = m_windowediface->call(s_closemethod, notifyid); if (!closereply.isValid()) { kWarning(s_knotificationarea) << "invalid windowed close reply" << closereply.error().message(); } } } void KNotificationManager::slotCloseRequested(const QString &eventid) { kDebug(s_knotificationarea) << "closing notifications due to interface" << eventid; QMutableMapIterator iter(m_notifications); while (iter.hasNext()) { iter.next(); KNotification* notification = iter.key(); const QString notifyid = kNotifyID(notification); if (notifyid == eventid) { notification->close(); break; } } } void KNotificationManager::slotActionRequested(const QString &eventid, const QString &action) { kDebug(s_knotificationarea) << "notification action invoked" << action; QMutableMapIterator iter(m_notifications); while (iter.hasNext()) { iter.next(); KNotification* notification = iter.key(); const QString notifyid = kNotifyID(notification); if (notifyid == eventid) { notification->activate(action.toUInt()); break; } } } void KNotificationManager::slotDirty(const QString &path) { kDebug(s_knotificationarea) << "dirty" << path; QMutexLocker locker(&m_mutex); delete m_config; m_config = new KConfig("knotificationrc", KConfig::NoGlobals); const QStringList notifyconfigs = KGlobal::dirs()->findAllResources("config", "notifications/*.notifyrc"); if (!notifyconfigs.isEmpty()) { m_config->addConfigSources(notifyconfigs); } // qDebug() << Q_FUNC_INFO << notifyconfigs; } class KNotificationPrivate { public: KNotificationPrivate(); QString eventid; QString title; QString text; QString icon; QWidget *widget; QStringList actions; KNotification::NotificationFlags flags; }; KNotificationPrivate::KNotificationPrivate() : widget(nullptr) { } KNotification::KNotification(QObject *parent) : QObject(parent), d(new KNotificationPrivate()) { } KNotification::~KNotification() { close(); delete d; } QString KNotification::eventID() const { return d->eventid; } void KNotification::setEventID(const QString &eventid) { d->eventid = eventid; } QString KNotification::title() const { return d->title; } void KNotification::setTitle(const QString &title) { d->title = title; } QString KNotification::text() const { return d->text; } void KNotification::setText(const QString &text) { d->text = text; } QString KNotification::icon() const { return d->icon; } void KNotification::setIcon(const QString &icon) { d->icon = icon; } QWidget* KNotification::widget() const { return d->widget; } void KNotification::setWidget(QWidget *widget) { d->widget = widget; setParent(widget); if (widget && (d->flags & KNotification::CloseWhenWidgetActivated)) { widget->installEventFilter(this); } } QStringList KNotification::actions() const { return d->actions; } void KNotification::setActions(const QStringList &actions) { d->actions = actions; } KNotification::NotificationFlags KNotification::flags() const { return d->flags; } void KNotification::setFlags(const NotificationFlags flags) { d->flags = flags; } void KNotification::send() { kDebug(s_knotificationarea) << "sending notification" << d->eventid; const bool persistent = (flags() & KNotification::Persistent); kNotificationManager->send(this, persistent); if (!persistent) { QTimer::singleShot(s_closedelay, this, SLOT(close())); } } void KNotification::activate(unsigned int action) { kDebug(s_knotificationarea) << "activating notification action" << d->eventid << action; switch (action) { case 1: { emit action1Activated(); break; } case 2: { emit action2Activated(); break; } case 3: { emit action3Activated(); break; } default: { kWarning(s_knotificationarea) << "invalid action" << action; break; } } close(); } void KNotification::close() { kDebug(s_knotificationarea) << "closing notification" << d->eventid; kNotificationManager->close(this); emit closed(); deleteLater(); } bool KNotification::eventFilter(QObject *watched, QEvent *event) { if (watched == d->widget) { if (event->type() == QEvent::WindowActivate && (d->flags & KNotification::CloseWhenWidgetActivated)) { kDebug(s_knotificationarea) << "closing due to widget activation" << d->eventid; QTimer::singleShot(s_closedelay, this, SLOT(close())); } } return false; } void KNotification::event(const QString &eventid, const QString &title, const QString &text, const QString &icon, QWidget *widget, const NotificationFlags flags) { KNotification* knotification = new KNotification(widget); knotification->setEventID(eventid); knotification->setTitle(title); knotification->setText(text); knotification->setIcon(icon); knotification->setWidget(widget); knotification->setFlags(flags); QTimer::singleShot(0, knotification, SLOT(send())); } void KNotification::beep(const QString &reason, QWidget *widget) { event( QString::fromLatin1("kde/beep"), QString(), reason, QString(), widget, KNotification::AutoClose ); } #include "moc_knotification.cpp" #include "knotification.moc"