Commit 723b6d13 authored by Kai Uwe Broulik's avatar Kai Uwe Broulik 🍇

Completely wire up old dataengine and further touches

- Old dataengine is fully functional now
  Except the inhibition stuff but I'm not sure if this is worth keeping the way it is
- "More" menu opens on press now and highlights
- Invoking any action closes the notification now
  KNotification explicitly does that for us but e.g. GTK does not
- Add clear button for history
- Restore kbroadcastnotification support
- Allow forgetting seen application (for KCM)
- Let users "create" notifications by calling NotificationServer::add
- When no application name is provided look up the sender's process name as last resort
- Add kdebugsettings categories file
parent 1c4de1d4
......@@ -37,7 +37,8 @@ NotificationApplet::NotificationApplet(QObject *parent, const QVariantList &data
{
static bool s_typesRegistered = false;
if (!s_typesRegistered) {
const char uri[] = "org.kde.plasma.private.notifications";
// FIXME register into org.kde.plasma.private.notifications once old applet is gone
const char uri[] = "org.kde.plasma.private.notificationsng";
qmlRegisterType<FileMenu>(uri, 2, 0, "FileMenu");
qmlRegisterType<Thumbnailer>(uri, 2, 0, "Thumbnailer");
qmlProtectModule(uri, 2);
......
......@@ -105,20 +105,30 @@ ColumnLayout {
}
}
Item {
RowLayout {
Layout.fillWidth: true
Layout.fillHeight: true
Layout.preferredWidth: units.gridUnit * 18
Layout.preferredHeight: units.gridUnit * 20
PlasmaExtras.Heading {
width: parent.width
Layout.fillWidth: true
level: 3
opacity: 0.6
visible: list.count === 0
text: i18n("No unread notifications.")
text: list.count === 0 ? i18n("No unread notifications.") : i18n("Notifications")
}
PlasmaComponents.ToolButton {
iconName: "edit-clear-history"
tooltip: i18n("Clear History")
visible: historyModel.expiredNotificationsCount > 0
onClicked: historyModel.clear(NotificationManager.Notifications.ClearExpired)
}
}
Item {
Layout.fillWidth: true
Layout.fillHeight: true
Layout.preferredWidth: units.gridUnit * 18
Layout.preferredHeight: units.gridUnit * 20
PlasmaExtras.ScrollArea {
anchors.fill: parent
......@@ -207,7 +217,10 @@ ColumnLayout {
onDismissClicked: model.dismissed = false
onConfigureClicked: historyModel.configure(historyModel.index(index, 0))
onActionInvoked: historyModel.invokeAction(historyModel.index(index, 0), actionName)
onActionInvoked: {
historyModel.invokeAction(historyModel.index(index, 0), actionName);
//historyModel.close(historyModel.index(index, 0));
}
onOpenUrl: {
Qt.openUrlExternally(url);
//historyModel.close(historyModel.index(index, 0))
......
......@@ -26,7 +26,8 @@ import org.kde.plasma.core 2.0 as PlasmaCore
import org.kde.plasma.components 2.0 as PlasmaComponents
import org.kde.notificationmanager 1.0 as NotificationManager
import org.kde.plasma.private.notifications 2.0 as Notifications
import org.kde.plasma.private.notificationsng 2.0 as Notifications // FIXME
ColumnLayout {
id: jobItem
......@@ -149,7 +150,15 @@ ColumnLayout {
id: otherFileActionsButton
iconName: "application-menu"
tooltip: i18n("More Options...")
onClicked: otherFileActionsMenu.open(-1, -1)
checkable: true
onPressedChanged: {
if (pressed) {
checked = Qt.binding(function() {
return otherFileActionsMenu.visible;
});
otherFileActionsMenu.open(-1, -1);
}
}
Notifications.FileMenu {
id: otherFileActionsMenu
......
......@@ -27,7 +27,7 @@ import org.kde.plasma.extras 2.0 as PlasmaExtras
import org.kde.kquickcontrolsaddons 2.0 as KQCAddons
import org.kde.plasma.private.notifications 2.0 as Notifications
import org.kde.plasma.private.notificationsng 2.0 as Notifications // FIXME
MouseArea {
id: thumbnailArea
......@@ -167,17 +167,20 @@ MouseArea {
}
tooltip: i18n("More Options...")
Accessible.name: tooltip
checkable: true
iconName: "application-menu"
checkable: true
onClicked: {
checked = Qt.binding(function() {
return thumbnailer.menuVisible;
});
fileMenu.visualParent = this;
// -1 tells it to "align bottom left of visualParent (this)"
fileMenu.open(-1, -1);
onPressedChanged: {
if (pressed) {
// fake "pressed" while menu is open
checked = Qt.binding(function() {
return fileMenu.visible;
});
fileMenu.visualParent = this;
// -1 tells it to "align bottom left of visualParent (this)"
fileMenu.open(-1, -1);
}
}
}
}
......
......@@ -266,8 +266,14 @@ QtObject {
onCloseClicked: popupNotificationsModel.close(popupNotificationsModel.index(index, 0))
onDismissClicked: model.dismissed = true
onConfigureClicked: popupNotificationsModel.configure(popupNotificationsModel.index(index, 0))
onDefaultActionInvoked: popupNotificationsModel.invokeDefaultAction(popupNotificationsModel.index(index, 0))
onActionInvoked: popupNotificationsModel.invokeAction(popupNotificationsModel.index(index, 0), actionName)
onDefaultActionInvoked: {
popupNotificationsModel.invokeDefaultAction(popupNotificationsModel.index(index, 0))
popupNotificationsModel.close(popupNotificationsModel.index(index, 0))
}
onActionInvoked: {
popupNotificationsModel.invokeAction(popupNotificationsModel.index(index, 0), actionName)
popupNotificationsModel.close(popupNotificationsModel.index(index, 0))
}
onOpenUrl: {
Qt.openUrlExternally(url);
popupNotificationsModel.close(popupNotificationsModel.index(index, 0))
......@@ -286,6 +292,7 @@ QtObject {
// Apps with notifyrc can already be configured anyway
if (model.desktopEntry && !model.notifyRcName) {
notificationSettings.registerKnownApplication(model.desktopEntry);
notificationSettings.save();
}
}
}
......
......@@ -19,7 +19,7 @@
#include "notificationaction.h"
#include "notificationsengine.h"
#include <notificationmanager/notificationserver.h>
#include "notificationserver.h"
#include <klocalizedstring.h>
......
......@@ -22,47 +22,41 @@
#include "notificationsadaptor.h"
#include "notificationsanitizer.h"
#include <notificationmanager/notificationserver.h>
#include <notificationmanager/notification.h>
#include "notificationserver.h"
#include "notification.h"
#include <KConfig>
#include <KConfigGroup>
#include <klocalizedstring.h>
#include <KSharedConfig>
#include <KNotifyConfigWidget>
#include <KUser>
#include <QGuiApplication>
#include <QRegularExpression>
#include <Plasma/DataContainer>
#include <Plasma/Service>
#include <QImage>
#include <kiconloader.h>
#include <KConfig>
// for ::kill
#include <signal.h>
#include "debug.h"
using namespace NotificationManager;
NotificationsEngine::NotificationsEngine( QObject* parent, const QVariantList& args )
: Plasma::DataEngine( parent, args ), m_nextId( 1 ), m_alwaysReplaceAppsList({QStringLiteral("Clementine"), QStringLiteral("Spotify"), QStringLiteral("Amarok")})
: Plasma::DataEngine( parent, args )
{
connect(&NotificationServer::self(), &NotificationServer::notificationAdded, this, [this](const Notification &notification) {
// FIXME handle replaced
notificationAdded(notification);
});
connect(&NotificationServer::self(), &NotificationServer::notificationReplaced, this, [this](uint replacedId, const Notification &notification) {
// Notification will already have the correct identical ID
Q_UNUSED(replacedId);
notificationAdded(notification);
});
connect(&NotificationServer::self(), &NotificationServer::notificationRemoved, this, [this](uint id, NotificationServer::CloseReason reason) {
Q_UNUSED(reason);
const QString source = QStringLiteral("notification %1").arg(id);
// if we don't have that notification in our local list,
// it has already been closed so don't notify a second time
......@@ -70,44 +64,6 @@ NotificationsEngine::NotificationsEngine( QObject* parent, const QVariantList& a
removeSource(source);
}
});
// FIXME let the new notification plasmoid do the killing
/*
if (!registerDBusService()) {
QDBusConnection dbus = QDBusConnection::sessionBus();
// Retrieve the pid of the current o.f.Notifications service
QDBusReply<uint> pidReply = dbus.interface()->servicePid(QStringLiteral("org.freedesktop.Notifications"));
uint pid = pidReply.value();
// Check if it's not the same app as our own
if (pid != qApp->applicationPid()) {
QDBusReply<uint> plasmaPidReply = dbus.interface()->servicePid(QStringLiteral("org.kde.plasmashell"));
// It's not the same but check if it isn't plasma,
// we don't want to kill Plasma
if (pid != plasmaPidReply.value()) {
qCDebug(NOTIFICATIONS) << "Terminating current Notification service with pid" << pid;
// Now finally terminate the service and register our own
::kill(pid, SIGTERM);
// Wait 3 seconds and then try registering it again
QTimer::singleShot(3000, this, &NotificationsEngine::registerDBusService);
}
}
}*/
// FIXME implement in notification server
/*
KConfigGroup config(KSharedConfig::openConfig(), QStringLiteral("Notifications"));
const bool broadcastsEnabled = config.readEntry("ListenForBroadcasts", false);
if (broadcastsEnabled) {
qCDebug(NOTIFICATIONS) << "Notifications engine is configured to listen for broadcasts";
QDBusConnection::systemBus().connect({}, {}, QStringLiteral("org.kde.BroadcastNotifications"),
QStringLiteral("Notify"), this, SLOT(onBroadcastNotification(QMap<QString,QVariant>)));
}*/
// Read additional single-notification-popup-only from a config file
//KConfig singlePopupConfig(QStringLiteral("plasma_single_popup_notificationrc"));
//KConfigGroup singlePopupConfigGroup(&singlePopupConfig, "General");
//m_alwaysReplaceAppsList += QSet<QString>::fromList(singlePopupConfigGroup.readEntry("applications", QStringList()));
}
NotificationsEngine::~NotificationsEngine()
......@@ -121,79 +77,29 @@ void NotificationsEngine::init()
void NotificationsEngine::notificationAdded(const Notification &notification)
{
// FIXME
/*foreach(NotificationInhibiton *ni, m_inhibitions) {
if (hints[ni->hint] == ni->value) {
qCDebug(NOTIFICATIONS) << "notification inhibited. Skipping";
return -1;
}
}*/
uint partOf = 0;
const QString appRealName; // FIXME = hints[QStringLiteral("x-kde-appname")].toString();
const QString eventId; // FIXME = hints[QStringLiteral("x-kde-eventId")].toString();
const bool skipGrouping = false;// FIXME hints[QStringLiteral("x-kde-skipGrouping")].toBool();
const QStringList urls; // FIXME there's no QUrl toStringList? = notification.urls();
const QString desktopEntry; // FIXME = hints[QStringLiteral("desktop-entry")].toString();
// group notifications that have the same title coming from the same app
// or if they are on the "blacklist", honor the skipGrouping hint sent
// FIXME
/*if (!replaces_id && m_activeNotifications.values().contains(app_name + summary) && !skipGrouping && urls.isEmpty() && !m_alwaysReplaceAppsList.contains(app_name)) {
// cut off the "notification " from the source name
partOf = m_activeNotifications.key(app_name + summary).midRef(13).toUInt();
}*/
//qCDebug(NOTIFICATIONS) << "Currrent active notifications:" << m_activeNotifications;
//qCDebug(NOTIFICATIONS) << "Guessing partOf as:" << partOf;
//qCDebug(NOTIFICATIONS) << " New Notification: " << summary << body << timeout << "& Part of:" << partOf;
QString bodyFinal = notification.body(); // is already sanitized
const QString app_name = notification.applicationName();
const QString appRealName = notification.notifyRcName();
const QString eventId = notification.eventId(); // FIXME = hints[QStringLiteral("x-kde-eventId")].toString();
const QStringList urls = QUrl::toStringList(notification.urls());
const QString desktopEntry = notification.desktopEntry();
const QString summary = notification.summary();
QString bodyFinal = notification.body(); // is already sanitized by NotificationManager
QString summaryFinal = notification.summary();
int timeout = notification.timeout();
if (partOf > 0) {
const QString source = QStringLiteral("notification %1").arg(partOf);
Plasma::DataContainer *container = containerForSource(source);
if (container) {
// append the body text
const QString previousBody = container->data()[QStringLiteral("body")].toString();
if (previousBody != bodyFinal) {
// FIXME: This will just append the entire old XML document to another one, leading to:
// <?xml><html>old</html><br><?xml><html>new</html>
// It works but is not very clean.
bodyFinal = previousBody + QStringLiteral("<br/>") + bodyFinal;
}
//replaces_id = partOf;
// remove the old notification and replace it with the new one
// TODO: maybe just update the current notification?
//CloseNotification(partOf);
}
} else if (bodyFinal.isEmpty()) {
if (bodyFinal.isEmpty()) {
//some ridiculous apps will send just a title (#372112), in that case, treat it as though there's only a body
//bodyFinal = summary;
//summaryFinal = app_name;
bodyFinal = summary;
summaryFinal = app_name;
}
uint id = notification.id();// replaces_id ? replaces_id : m_nextId++;
// If the current app is in the "blacklist"...
/*if (m_alwaysReplaceAppsList.contains(app_name)) {
// ...check if we already have a notification from that particular
// app and if yes, use its id to replace it
if (m_notificationsFromReplaceableApp.contains(app_name)) {
id = m_notificationsFromReplaceableApp.value(app_name);
} else {
m_notificationsFromReplaceableApp.insert(app_name, id);
}
}*/
/*QString appname_str = app_name;
QString appname_str = app_name;
if (appname_str.isEmpty()) {
appname_str = i18n("Unknown Application");
}*/
}
bool isPersistent = (timeout == 0);
......@@ -222,7 +128,21 @@ void NotificationsEngine::notificationAdded(const Notification &notification)
notificationData.insert(QStringLiteral("appIcon"), notification.applicationIconName());
notificationData.insert(QStringLiteral("summary"), summaryFinal);
notificationData.insert(QStringLiteral("body"), bodyFinal);
notificationData.insert(QStringLiteral("actions"), QStringList()); // FIXME
QStringList actions;
for (int i = 0; i < notification.actionNames().count(); ++i) {
actions << notification.actionNames().at(i) << notification.actionLabels().at(i);
}
// NotificationManager hides the configure and default stuff from us but we need to re-add them
// to the actions list for compatibility
if (!notification.configureActionLabel().isEmpty()) {
actions << QStringLiteral("settings") << notification.configureActionLabel();
}
if (notification.hasDefaultAction()) {
actions << QStringLiteral("default") << QString();
}
notificationData.insert(QStringLiteral("actions"), actions);
notificationData.insert(QStringLiteral("isPersistent"), isPersistent);
notificationData.insert(QStringLiteral("expireTimeout"), timeout);
......@@ -234,32 +154,30 @@ void NotificationsEngine::notificationAdded(const Notification &notification)
notificationData.insert(QStringLiteral("appServiceIcon"), service->icon());
}
bool configurable = false;
if (!appRealName.isEmpty()) {
if (m_configurableApplications.contains(appRealName)) {
configurable = m_configurableApplications.value(appRealName);
} else {
// Check whether the application actually has notifications we can configure
KConfig config(appRealName + QStringLiteral(".notifyrc"), KConfig::NoGlobals);
config.addConfigSources(QStandardPaths::locateAll(QStandardPaths::GenericDataLocation,
QStringLiteral("knotifications5/") + appRealName + QStringLiteral(".notifyrc")));
const QRegularExpression regexp(QStringLiteral("^Event/([^/]*)$"));
configurable = !config.groupList().filter(regexp).isEmpty();
m_configurableApplications.insert(appRealName, configurable);
}
}
notificationData.insert(QStringLiteral("appRealName"), appRealName);
notificationData.insert(QStringLiteral("configurable"), configurable);
// NotificationManager configurable is anything that has a notifyrc or desktop entry
// but the old stuff assumes only stuff with notifyrc to be configurable
notificationData.insert(QStringLiteral("configurable"), !notification.notifyRcName().isEmpty());
QImage image = notification.image();
notificationData.insert(QStringLiteral("image"), image.isNull() ? QVariant() : image);
// FIXME did we even have this?
/*if (hints.contains(QStringLiteral("urgency"))) {
notificationData.insert(QStringLiteral("urgency"), hints[QStringLiteral("urgency")].toInt());
}*/
int urgency = -1;
switch (notification.urgency()) {
case Notifications::LowUrgency:
urgency = 0;
break;
case Notifications::NormalUrgency:
urgency = 1;
break;
case Notifications::CriticalUrgency:
urgency = 2;
break;
}
if (urgency > -1) {
notificationData.insert(QStringLiteral("urgency"), urgency);
}
notificationData.insert(QStringLiteral("urls"), urls);
......@@ -268,12 +186,15 @@ void NotificationsEngine::notificationAdded(const Notification &notification)
m_activeNotifications.insert(source, notification.applicationName() + notification.summary());
}
uint NotificationsEngine::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)
void NotificationsEngine::removeNotification(uint id, uint closeReason)
{
// FIXME wire this thing up to the new one, it's used by notification action job or something
return 0;
const QString source = QStringLiteral("notification %1").arg(id);
// if we don't have that notification in our local list,
// it has already been closed so don't notify a second time
if (m_activeNotifications.remove(source) > 0) {
removeSource(source);
NotificationServer::self().closeNotification(id, static_cast<NotificationServer::CloseReason>(closeReason));
}
}
Plasma::Service* NotificationsEngine::serviceForSource(const QString& source)
......@@ -284,8 +205,16 @@ Plasma::Service* NotificationsEngine::serviceForSource(const QString& source)
int NotificationsEngine::createNotification(const QString &appName, const QString &appIcon, const QString &summary,
const QString &body, int timeout, const QStringList &actions, const QVariantMap &hints)
{
Notify(appName, 0, appIcon, summary, body, actions, hints, timeout);
return m_nextId;
Notification notification;
notification.setApplicationName(appName);
notification.setApplicationIconName(appIcon);
notification.setSummary(summary);
notification.setBody(body); // sanitizes
notification.setActions(actions);
notification.setTimeout(timeout);
notification.processHints(hints);
NotificationServer::self().add(notification);
return 0;
}
void NotificationsEngine::configureNotification(const QString &appName, const QString &eventId)
......@@ -312,45 +241,6 @@ QSharedPointer<NotificationInhibiton> NotificationsEngine::createInhibition(cons
return rc;
}
void NotificationsEngine::onBroadcastNotification(const QMap<QString, QVariant> &properties)
{
qCDebug(NOTIFICATIONS) << "Received broadcast notification";
const auto currentUserId = KUserId::currentEffectiveUserId().nativeId();
// a QVariantList with ints arrives as QDBusArgument here, using a QStringList for simplicity
const QStringList &userIds = properties.value(QStringLiteral("uids")).toStringList();
if (!userIds.isEmpty()) {
auto it = std::find_if(userIds.constBegin(), userIds.constEnd(), [currentUserId](const QVariant &id) {
bool ok;
auto uid = id.toString().toLongLong(&ok);
return ok && uid == currentUserId;
});
if (it == userIds.constEnd()) {
qCDebug(NOTIFICATIONS) << "It is not meant for us, ignoring";
return;
}
}
bool ok;
int timeout = properties.value(QStringLiteral("timeout")).toInt(&ok);
if (!ok) {
timeout = -1; // -1 = server default, 0 would be "persistent"
}
Notify(
properties.value(QStringLiteral("appName")).toString(),
0, // replaces_id
properties.value(QStringLiteral("appIcon")).toString(),
properties.value(QStringLiteral("summary")).toString(),
properties.value(QStringLiteral("body")).toString(),
{}, // no actions
properties.value(QStringLiteral("hints")).toMap(),
timeout
);
}
K_EXPORT_PLASMA_DATAENGINE_WITH_JSON(notifications, NotificationsEngine, "plasma-dataengine-notifications.json")
#include "notificationsengine.moc"
......@@ -73,34 +73,13 @@ public:
NotificationInhibitonPtr createInhibition(const QString &hint, const QString &value);
public Q_SLOTS:
void onBroadcastNotification(const QMap<QString, QVariant> &properties);
void removeNotification(uint id, uint closeReason);
private:
void notificationAdded(const NotificationManager::Notification &notification);
/**
* Holds the id that will be assigned to the next notification source
* that will be created
*/
uint m_nextId;
QHash<QString, QString> m_activeNotifications;
QHash<QString, bool> m_configurableApplications;
/**
* A "blacklist" of apps for which always the previous notification from this app
* is replaced by the newer one. This is the case for eg. media players
* as we simply want to update the notification, not get spammed by tens
* of notifications for quickly changing songs in playlist
*/
QSet<QString> m_alwaysReplaceAppsList;
/**
* This holds the notifications sent from apps from the list above
* for fast lookup
*/
QHash<QString, uint> m_notificationsFromReplaceableApp;
QList<NotificationInhibiton*> m_inhibitions;
friend class NotificationAction;
......
......@@ -26,6 +26,7 @@ ecm_qt_declare_logging_category(notificationmanager_LIB_SRCS
HEADER debug.h
IDENTIFIER NOTIFICATIONMANAGER
CATEGORY_NAME org.kde.plasma.notifications)
install(FILES libnotificationmanager.categories DESTINATION ${KDE_INSTALL_CONFDIR})
# Settings
kconfig_add_kcfg_files(notificationmanager_LIB_SRCS kcfg/donotdisturbsettings.kcfgc)
......@@ -59,6 +60,7 @@ target_link_libraries(notificationmanager
KF5::Plasma
KF5::I18n
KF5::IconThemes
KF5::ProcessCore
)
set_target_properties(notificationmanager PROPERTIES
......
......@@ -42,7 +42,7 @@ QString JobDetails::text() const
if (m_destUrl.isLocalFile()) {
destUrlString = m_destUrl.toLocalFile();
const QString homePath = QDir::homePath(); // TODO profile if this is heavy
const QString homePath = QDir::homePath();
if (destUrlString.startsWith(homePath)) {
destUrlString = QStringLiteral("~") + destUrlString.mid(homePath.length());
}
......
......@@ -306,3 +306,9 @@ void JobsModel::kill(const QString &jobId)
{
d->operationCall(jobId, QStringLiteral("stop"));
}
void JobsModel::clear(Notifications::ClearFlags flags)
{
Q_UNUSED(flags);
// TODO
}
......@@ -26,6 +26,8 @@
#include <Plasma/DataEngine>
#include "notifications.h"
namespace NotificationManager
{
......@@ -45,12 +47,14 @@ public:
bool setData(const QModelIndex &index, const QVariant &value, int role) override;
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
Q_INVOKABLE void close(const QString &jobId);
Q_INVOKABLE void expire(const QString &jobId);
void close(const QString &jobId);
void expire(const QString &jobId);
void suspend(const QString &jobId);
void resume(const QString &jobId);
void kill(const QString &jobId);
Q_INVOKABLE void suspend(const QString &jobId);
Q_INVOKABLE void resume(const QString &jobId);
Q_INVOKABLE void kill(const QString &jobId);