Commit eab83389 authored by Kai Uwe Broulik's avatar Kai Uwe Broulik 🍇

Begin on inhibition / do not disturb stuff

- Also moe the NotificationPopup item back to the root folder
- Implement tracking of unknown apps, ie. once an app shows a notificaton it will show up
  in the KCM so it can be configured
- Make blacklisting of notification popups (via setting in KCM) work
- Start on inhibition stuff (DBus API proposal sent to XDG mailing list)
parent 1b9b7bcc
......@@ -29,6 +29,8 @@ import org.kde.kquickcontrolsaddons 2.0
import org.kde.notificationmanager 1.0 as NotificationManager
import ".."
QtObject {
id: popupHandler
......@@ -78,6 +80,7 @@ QtObject {
limit: Math.ceil(popupHandler.screenRect.height / (theme.mSize(theme.defaultFont).height * 4))
showExpired: false
showDismissed: false
blacklistedDesktopEntries: notificationSettings.popupBlacklistedApplications
showJobs: notificationSettings.jobsInNotifications
groupMode: NotificationManager.Notifications.GroupDisabled
urgencies: NotificationManager.Notifications.NormalUrgency | NotificationManager.Notifications.CriticalUrgency
......@@ -212,6 +215,13 @@ QtObject {
onHeightChanged: Qt.callLater(positionPopups)
onWidthChanged: Qt.callLater(positionPopups)
Component.onCompleted: {
// Register apps that were seen spawning a popup so they can be configured later
if (model.desktopEntry) {
notificationSettings.registerKnownApplication(model.desktopEntry);
}
}
}
onObjectAdded: {
// also needed for it to correctly layout its contents
......
......@@ -19,8 +19,6 @@ set(notificationmanager_LIB_SRCS
notificationgroupingproxymodel.cpp
limitedrowcountproxymodel.cpp
inhibitionserver.cpp
settings.cpp
)
......@@ -36,8 +34,7 @@ kconfig_add_kcfg_files(notificationmanager_LIB_SRCS kcfg/jobsettings.kcfgc)
kconfig_add_kcfg_files(notificationmanager_LIB_SRCS kcfg/badgesettings.kcfgc)
# DBus
qt5_add_dbus_adaptor(notificationmanager_LIB_SRCS dbus/org.freedesktop.Notifications.xml notificationserver_p.h NotificationManager::NotificationServerPrivate fdonotificationsadaptor)
qt5_add_dbus_adaptor(notificationmanager_LIB_SRCS dbus/org.kde.Notifications.xml inhibitionserver.h NotificationManager::InhibitionServer kdenotificationsadaptor InhibitionAdaptor)
qt5_add_dbus_adaptor(notificationmanager_LIB_SRCS dbus/org.freedesktop.Notifications.xml notificationserver_p.h NotificationManager::NotificationServerPrivate)
add_library(notificationmanager ${notificationmanager_LIB_SRCS})
add_library(PW::LibNotificationManager ALIAS notificationmanager)
......
......@@ -33,5 +33,28 @@
<arg type="s" name="version" direction="out"/>
<arg type="s" name="spec_version" direction="out"/>
</method>
<!-- Inhibitions -->
<method name="Inhibit">
<annotation name="org.qtproject.QtDBus.QtTypeName.In2" value="QVariantMap"/>
<arg type="u" direction="out"/>
<arg name="desktop_entry" type="s" direction="in"/>
<arg name="reason" type="s" direction="in"/>
<arg name="hints" type="a{sv}" direction="in"/>
</method>
<method name="UnInhibit">
<arg type="u" direction="in"/>
</method>
<property name="Inhibited" type="b" access="read">
<annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="true"/>
</property>
<method name="ListInhibitors">
<annotation name="org.qtproject.QtDBus.QtTypeName.Out0" value="QList&lt;Inhibition&gt;"/>
<arg name="inhibitors" type="a(ssa{sv})" direction="out"/>
</method>
</interface>
</node>
<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN" "http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
<node>
<interface name="org.kde.Notifications.Inhibit">
<method name="Inhibit">
<annotation name="org.qtproject.QtDBus.QtTypeName.In2" value="QVariantMap"/>
<!-- Desktop entry of the application, e.g. org.kde.okular -->
<arg name="desktop_entry" type="s" direction="in"/>
<!-- Human-readable reason for the inhibition, e.g. "Giving a presentation -->
<arg name="reason" type="s" direction="in"/>
<!-- Reserved -->
<arg name="hints" type="a{sv}" direction="in"/>
<arg name="fd" type="h" direction="out"/>
</method>
<!-- TODO property for whether it is currently enabled? -->
</interface>
</node>
/*
* Copyright 2018 Kai Uwe Broulik <kde@privat.broulik.de>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) version 3, or any
* later version accepted by the membership of KDE e.V. (or its
* successor approved by the membership of KDE e.V.), which shall
* act as a proxy defined in Section 6 of version 3 of the license.
*
* 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library. If not, see <http://www.gnu.org/licenses/>.
*/
#include "inhibitionserver.h"
#include "kdenotificationsadaptor.h"
#include "notification.h"
#include <QDBusConnection>
using namespace NotificationManager;
InhibitionServer::InhibitionServer(QObject *parent)
: QObject(parent)
{
new InhibitionAdaptor(this);
QDBusConnection::sessionBus().registerObject(QStringLiteral("/org/kde/Notifications"), this);
if (!QDBusConnection::sessionBus().registerService(QStringLiteral("org.kde.Notifications.Inhibit"))) {
qWarning() << "Failed to register Notification Inhibit service";
}
}
InhibitionServer::~InhibitionServer() = default;
InhibitionServer &InhibitionServer::self()
{
static InhibitionServer s_self;
return s_self;
}
QDBusUnixFileDescriptor InhibitionServer::Inhibit(const QString &app_name, const QString &reason, const QVariantMap &hints)
{
qDebug() << "INHIBIT" << app_name << reason << hints;
return QDBusUnixFileDescriptor();
}
/*
* Copyright 2018 Kai Uwe Broulik <kde@privat.broulik.de>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) version 3, or any
* later version accepted by the membership of KDE e.V. (or its
* successor approved by the membership of KDE e.V.), which shall
* act as a proxy defined in Section 6 of version 3 of the license.
*
* 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <QObject>
#include <QDBusContext>
#include <QDBusUnixFileDescriptor>
#include <QVector>
//#include "notificationmanager_export.h"
namespace NotificationManager
{
class Notification;
/**
* @short Registers an inhibition server on the DBus
*
* TODO
*
* @author Kai Uwe Broulik <kde@privat.broulik.de>
**/
class InhibitionServer : public QObject, protected QDBusContext
{
Q_OBJECT
public:
~InhibitionServer() override;
static InhibitionServer &self();
// DBus
QDBusUnixFileDescriptor Inhibit(const QString &app_name, const QString &reason, const QVariantMap &hints);
Q_SIGNALS:
void inhibitionAdded();
void inhibitionRemoved();
private:
explicit InhibitionServer(QObject *parent = nullptr);
Q_DISABLE_COPY(InhibitionServer)
// FIXME we also need to disable move and other stuff?
// TODO list of fds
};
} // namespace NotificationManager
......@@ -218,7 +218,7 @@ void Notification::Private::processHints(const QVariantMap &hints)
{
auto end = hints.end();
const QString desktopEntry = hints.value(QStringLiteral("desktop-entry")).toString();
desktopEntry = hints.value(QStringLiteral("desktop-entry")).toString();
if (!desktopEntry.isEmpty()) {
KService::Ptr service = KService::serviceByStorageId(desktopEntry);
if (service) {
......@@ -406,6 +406,11 @@ void Notification::setImage(const QImage &image)
d->image = image;
}
QString Notification::desktopEntry() const
{
return d->desktopEntry;
}
QString Notification::applicationName() const
{
return d->applicationName;
......
......@@ -77,6 +77,8 @@ public:
QImage image() const;
void setImage(const QImage &image);
QString desktopEntry() const;
QString applicationName() const;
void setApplicationName(const QString &applicationName);
......
......@@ -57,6 +57,7 @@ public:
QImage image;
QString applicationName;
QString desktopEntry;
QString serviceName;
QString applicationIconName;
......
......@@ -72,6 +72,20 @@ void NotificationFilterProxyModel::setShowDismissed(bool show)
}
}
QStringList NotificationFilterProxyModel::blacklistedDesktopEntries() const
{
return m_blacklistedDesktopEntries;
}
void NotificationFilterProxyModel::setBlackListedDesktopEntries(const QStringList &blacklistedDesktopEntries)
{
if (m_blacklistedDesktopEntries != blacklistedDesktopEntries) {
m_blacklistedDesktopEntries = blacklistedDesktopEntries;
invalidateFilter();
emit blacklistedDesktopEntriesChanged();
}
}
bool NotificationFilterProxyModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const
{
const QModelIndex sourceIdx = sourceModel()->index(source_row, 0, source_parent);
......@@ -107,5 +121,14 @@ bool NotificationFilterProxyModel::filterAcceptsRow(int source_row, const QModel
}
}
if (!m_blacklistedDesktopEntries.isEmpty()) {
const QString desktopEntry = sourceIdx.data(Notifications::DesktopEntryRole).toString();
if (!desktopEntry.isEmpty()) {
if (m_blacklistedDesktopEntries.contains(desktopEntry)) {
return false;
}
}
}
return true;
}
......@@ -44,10 +44,14 @@ public:
bool showDismissed() const;
void setShowDismissed(bool show);
QStringList blacklistedDesktopEntries() const;
void setBlackListedDesktopEntries(const QStringList &blacklistedDesktopEntries);
signals:
void urgenciesChanged();
void showExpiredChanged();
void showDismissedChanged();
void blacklistedDesktopEntriesChanged();
protected:
bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override;
......@@ -58,6 +62,7 @@ private:
| Notifications::CriticalUrgency;
bool m_showDismissed = false;
bool m_showExpired = false;
QStringList m_blacklistedDesktopEntries;
};
......
......@@ -203,6 +203,7 @@ QVariant NotificationModel::data(const QModelIndex &index, int role) const
return notification.image();
}
break;
case Notifications::DesktopEntryRole: return notification.desktopEntry();
case Notifications::ApplicationNameRole: return notification.applicationName();
case Notifications::ApplicationIconNameRole: return notification.applicationIconName();
......
......@@ -106,6 +106,7 @@ void Notifications::Private::initModels()
connect(filterModel, &NotificationFilterProxyModel::urgenciesChanged, q, &Notifications::urgenciesChanged);
connect(filterModel, &NotificationFilterProxyModel::showExpiredChanged, q, &Notifications::showExpiredChanged);
connect(filterModel, &NotificationFilterProxyModel::showDismissedChanged, q, &Notifications::showDismissedChanged);
connect(filterModel, &NotificationFilterProxyModel::blacklistedDesktopEntriesChanged, q, &Notifications::blacklistedDesktopEntriesChanged);
filterModel->setSourceModel(notificationsAndJobsModel);
}
......@@ -290,6 +291,16 @@ void Notifications::setShowDismissed(bool show)
d->filterModel->setShowDismissed(show);
}
QStringList Notifications::blacklistedDesktopEntries() const
{
return d->filterModel->blacklistedDesktopEntries();
}
void Notifications::setBlacklistedDesktopEntries(const QStringList &blacklistedDesktopEntries)
{
d->filterModel->setBlackListedDesktopEntries(blacklistedDesktopEntries);
}
bool Notifications::showSuppressed() const
{
// TODO
......@@ -484,6 +495,7 @@ QHash<int, QByteArray> Notifications::roleNames() const
{BodyRole, QByteArrayLiteral("body")},
{IconNameRole, QByteArrayLiteral("iconName")},
{ImageRole, QByteArrayLiteral("image")},
{DesktopEntryRole, QByteArrayLiteral("desktopEntry")},
{ApplicationNameRole, QByteArrayLiteral("applicationName")},
{ApplicationIconNameRole, QByteArrayLiteral("applicationIconName")},
......
......@@ -71,6 +71,11 @@ class NOTIFICATIONMANAGER_EXPORT Notifications : public QSortFilterProxyModel, p
*/
Q_PROPERTY(bool showDismissed READ showDismissed WRITE setShowDismissed NOTIFY showDismissedChanged)
/**
* A list of desktop entries for which no notifications should be shown.
*/
Q_PROPERTY(QStringList blacklistedDesktopEntries READ blacklistedDesktopEntries WRITE setBlacklistedDesktopEntries NOTIFY blacklistedDesktopEntriesChanged)
/**
* Whether to show suppressed notifications.
*
......@@ -127,7 +132,8 @@ public:
BodyRole = Qt::UserRole + 6,
IconNameRole = Qt::UserRole + 7,
ImageRole = Qt::DecorationRole,
ApplicationNameRole = Qt::UserRole + 8,
DesktopEntryRole = Qt::UserRole + 8,
ApplicationNameRole,
ApplicationIconNameRole,
// Jobs
......@@ -215,6 +221,9 @@ public:
bool showDismissed() const;
void setShowDismissed(bool show);
QStringList blacklistedDesktopEntries() const;
void setBlacklistedDesktopEntries(const QStringList &blacklistedDesktopEntries);
bool showSuppressed() const;
void setShowSuppressed(bool show);
......@@ -259,6 +268,7 @@ signals:
void limitChanged();
void showExpiredChanged();
void showDismissedChanged();
void blacklistedDesktopEntriesChanged();
void showSuppressedChanged();
void showJobsChanged();
void urgenciesChanged();
......
......@@ -44,7 +44,7 @@ NotificationServer &NotificationServer::self()
bool NotificationServer::isValid() const
{
return d->valid;
return d->m_valid;
}
void NotificationServer::closeNotification(uint notificationId, CloseReason reason)
......
......@@ -22,7 +22,7 @@
#include "debug.h"
#include "fdonotificationsadaptor.h"
#include "notificationsadaptor.h"
#include "notification.h"
#include "notification_p.h"
......@@ -30,11 +30,13 @@
#include "notificationserver.h"
#include <QDBusConnection>
#include <QDBusServiceWatcher>
using namespace NotificationManager;
NotificationServerPrivate::NotificationServerPrivate(QObject *parent)
: QObject(parent)
, m_inhibitionWatcher(new QDBusServiceWatcher(this))
{
new NotificationsAdaptor(this);
......@@ -48,8 +50,12 @@ NotificationServerPrivate::NotificationServerPrivate(QObject *parent)
return;
}
qCInfo(NOTIFICATIONMANAGER) << "Registered Notification service on DBus";
valid = true;
m_inhibitionWatcher->setConnection(QDBusConnection::sessionBus());
m_inhibitionWatcher->setWatchMode(QDBusServiceWatcher::WatchForUnregistration);
connect(m_inhibitionWatcher, &QDBusServiceWatcher::serviceUnregistered, this, &NotificationServerPrivate::onServiceUnregistered);
qCDebug(NOTIFICATIONMANAGER) << "Registered Notification service on DBus";
m_valid = true;
}
NotificationServerPrivate::~NotificationServerPrivate() = default;
......@@ -58,16 +64,14 @@ uint NotificationServerPrivate::Notify(const QString &app_name, uint replaces_id
const QString &summary, const QString &body, const QStringList &actions,
const QVariantMap &hints, int timeout)
{
Q_ASSERT(calledFromDBus());
const bool wasReplaced = replaces_id > 0;
int notificationId = 0;
if (wasReplaced) {
notificationId = replaces_id;
} else {
// TODO according to spec should wrap around once INT_MAX is exceeded
++highestNotificationId;
notificationId = highestNotificationId;
++m_highestNotificationId;
notificationId = m_highestNotificationId;
}
Notification notification(notificationId);
......@@ -113,7 +117,9 @@ QStringList NotificationServerPrivate::GetCapabilities() const
// should we support "persistence" where notification stays present with "resident"
// but that is basically an SNI isn't it?
QStringLiteral("x-kde-urls")
QStringLiteral("x-kde-urls"),
QStringLiteral("inhibitions")
};
}
......@@ -124,3 +130,76 @@ QString NotificationServerPrivate::GetServerInformation(QString &vendor, QString
specVersion = QStringLiteral("1.2");
return QStringLiteral("Plasma");
}
uint NotificationServerPrivate::Inhibit(const QString &desktop_entry, const QString &reason, const QVariantMap &hints)
{
const QString service = message().service();
qCDebug(NOTIFICATIONMANAGER) << "Request inhibit from service" << service << "which is" << desktop_entry << "with reason" << reason;
// should we check for this and/or if it's actually a valid service?
if (desktop_entry.isEmpty()) {
// TODO return error
return 0;
}
m_inhibitionWatcher->addWatchedService(service);
++m_highestInhibitionCookie;
m_inhibitions.insert(m_highestInhibitionCookie, {
desktop_entry,
reason,
hints
});
m_inhibitionServices.insert(m_highestInhibitionCookie, service);
emit inhibitedChanged();
return m_highestInhibitionCookie;
}
void NotificationServerPrivate::onServiceUnregistered(const QString &serviceName)
{
qCDebug(NOTIFICATIONMANAGER) << "Inhibition service unregistered" << serviceName;
const uint cookie = m_inhibitionServices.key(serviceName);
if (!cookie) {
qCInfo(NOTIFICATIONMANAGER) << "Unknown inhibition service unregistered" << serviceName;
return;
}
// We do lookups in there again...
UnInhibit(cookie);
}
void NotificationServerPrivate::UnInhibit(uint cookie)
{
qCDebug(NOTIFICATIONMANAGER) << "Request release inhibition for cookie" << cookie;
const QString service = m_inhibitionServices.value(cookie);
if (service.isEmpty()) {
qCInfo(NOTIFICATIONMANAGER) << "Requested to release inhibition with cookie" << cookie << "that doesn't exist";
// TODO if called from dbus raise error
return;
}
m_inhibitionWatcher->removeWatchedService(service);
m_inhibitions.remove(cookie);
m_inhibitionServices.remove(cookie);
if (m_inhibitions.isEmpty()) {
emit inhibitedChanged();
}
}
QList<Inhibition> NotificationServerPrivate::ListInhibitors() const
{
return {};
}
bool NotificationServerPrivate::inhibited() const
{
return !m_inhibitions.isEmpty();
}
......@@ -23,6 +23,15 @@
#include <QObject>
#include <QDBusContext>
class QDBusServiceWatcher;
struct Inhibition
{
QString desktopEntry;
QString reason;
QVariantMap hints;
};
namespace NotificationManager
{
......@@ -32,6 +41,10 @@ class Q_DECL_HIDDEN NotificationServerPrivate : public QObject, protected QDBusC
{
Q_OBJECT
// DBus
// Inhibitions
Q_PROPERTY(bool Inhibited READ inhibited NOTIFY inhibitedChanged)
public:
NotificationServerPrivate(QObject *parent);
~NotificationServerPrivate() override;
......@@ -44,14 +57,34 @@ public:
QStringList GetCapabilities() const;
QString GetServerInformation(QString &vendor, QString &version, QString &specVersion) const;
// Inhibitions
uint Inhibit(const QString &desktop_entry,
const QString &reason,
const QVariantMap &hints);
void UnInhibit(uint cookie);
QList<Inhibition> ListInhibitors() const;
bool inhibited() const; // property getter
Q_SIGNALS:
// DBus
void NotificationClosed(uint id, uint reason);
void ActionInvoked(uint id, const QString &actionKey);
public:
int highestNotificationId = 0;
bool valid = false;
// FIXME connect this to properties changed dbus signal
void inhibitedChanged();
public: // stuff used by public class
bool m_valid = false;
private:
void onServiceUnregistered(const QString &serviceName);
uint m_highestNotificationId = 0;
QDBusServiceWatcher *m_inhibitionWatcher = nullptr;
uint m_highestInhibitionCookie = 0;
QHash<uint /*cookie*/, Inhibition> m_inhibitions;
QHash<uint /*cookie*/, QString> m_inhibitionServices;
};
......
......@@ -23,6 +23,9 @@
#include <QDebug>
#include <KConfigWatcher>
#include <KService>
#include "debug.h"
// Settings
#include "donotdisturbsettings.h"
......@@ -86,9 +89,23 @@ void Settings::Private::setGroupBehavior(KConfigGroup &group, const Settings::No
return;
}
group.writeEntry("ShowPopups", behavior.testFlag(Settings::ShowPopups));