kdelibs/kdeui/notifications/knotification.cpp
Ivailo Monev a4724b9d3a kdeui: do not send timeout data to the plasma applet
it is not matter of timeout, it is matter of the KNotification instance
being around for the actions. once it is not the notification actions
become non-operational. other than that notification stay indefinetly
in the notification area

Signed-off-by: Ivailo Monev <xakepa10@gmail.com>
2024-04-08 19:50:02 +03:00

503 lines
16 KiB
C++

/* This file is part of the KDE libraries
Copyright (C) 2023 Ivailo Monev <xakepa10@gmail.com>
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 "kdbusconnectionpool.h"
#include "kiconloader.h"
#include "kpassivepopup.h"
#include "kdirwatch.h"
#include "kdebug.h"
#include <QMutex>
#include <QDBusConnectionInterface>
#include <QDBusInterface>
#include <QDBusReply>
#include <QTimer>
// 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:
QMutex m_mutex;
KConfig *m_config;
KDirWatch m_configwatch;
QDBusInterface* m_notificationsiface;
QDBusInterface* m_kaudioplayeriface;
QMap<KNotification*,QVariantMap> m_notifications;
};
K_GLOBAL_STATIC(KNotificationManager, kNotificationManager);
KNotificationManager::KNotificationManager()
: m_config(nullptr),
m_configwatch(this),
m_notificationsiface(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_notificationsiface) {
m_notificationsiface = new QDBusInterface(
"org.kde.plasma-desktop", "/Notifications", "org.kde.Notifications",
QDBusConnection::sessionBus(), this
);
connect(
m_notificationsiface, SIGNAL(closeRequested(QString,QString)),
this, SLOT(slotCloseRequested(QString,QString))
);
connect(
m_notificationsiface, SIGNAL(actionRequested(QString,QString)),
this, SLOT(slotActionRequested(QString,QString))
);
}
if (!m_notificationsiface || !m_notificationsiface->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++;
}
QString eventapp = globalcomment;
if (eventapp.isEmpty()) {
eventapp = KGlobal::mainComponent().componentName();
}
eventdata.insert("appName", eventapp); // unused
eventdata.insert("appIcon", eventicon);
eventdata.insert("summary", eventtitle); // unused
eventdata.insert("body", eventtext);
eventdata.insert("actions", eventactions);
eventdata.insert("configurable", true);
// NOTE: has to be set to be configurable via plasma notifications applet
eventdata.insert("appRealName", spliteventid.at(0));
bool updatenotification = false;
QDBusReply<void> notifyreply;
if (addnotification) {
notifyreply = m_notificationsiface->call(s_addmethod, notifyid);
if (!notifyreply.isValid()) {
kWarning(s_knotificationarea) << "invalid add reply" << notifyreply.error().message();
} else {
updatenotification = true;
m_notifications.insert(notification, eventdata);
}
}
if (updatenotification) {
notifyreply = m_notificationsiface->call(s_updatemethod, notifyid, eventdata);
if (!notifyreply.isValid()) {
kWarning(s_knotificationarea) << "invalid update reply" << notifyreply.error().message();
}
}
}
}
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<void> 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<KNotification*,QVariantMap> iter(m_notifications);
while (iter.hasNext()) {
iter.next();
if (iter.key() == notification) {
const QString notifyid = kNotifyID(iter.key());
iter.remove();
QDBusReply<void> closereply = m_notificationsiface->call(s_closemethod, notifyid);
if (!closereply.isValid()) {
kWarning(s_knotificationarea) << "invalid close reply" << closereply.error().message();
}
break;
}
}
}
void KNotificationManager::slotCloseRequested(const QString &eventid)
{
kDebug(s_knotificationarea) << "closing notifications due to interface" << eventid;
QMutableMapIterator<KNotification*,QVariantMap> 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<KNotification*,QVariantMap> 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::CloseOnTimeout
);
}
#include "moc_knotification.cpp"
#include "knotification.moc"