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>
......
......@@ -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);
void clear(Notifications::ClearFlags flags);
private slots:
void dataUpdated(const QString &sourceName, const Plasma::DataEngine::Data &data);
......
# Logging categories (for kdebugsettings)
org.kde.plasma.notifications Plasma Notifications
......@@ -37,6 +37,8 @@
#include <KIconLoader>
#include <KService>
#include "debug.h"
#include "notifications.h"
using namespace NotificationManager;
......@@ -383,7 +385,7 @@ QDateTime Notification::updated() const
return d->updated;
}
void Notification::setUpdated()
void Notification::resetUpdated()
{
d->updated = QDateTime::currentDateTimeUtc();
}
......@@ -438,6 +440,11 @@ QString Notification::notifyRcName() const
return d->notifyRcName;
}
QString Notification::eventId() const
{
return d->eventId;
}
QString Notification::applicationName() const
{
return d->applicationName;
......@@ -486,8 +493,7 @@ bool Notification::hasDefaultAction() const
void Notification::setActions(const QStringList &actions)
{
if (actions.count() % 2 != 0) {
// FIXME qCWarning
qWarning() << "List of actions must contain an even number of items, tried to set actions to" << actions;
qCWarning(NOTIFICATIONMANAGER) << "List of actions must contain an even number of items, tried to set actions to" << actions;
return;
}
......@@ -575,7 +581,7 @@ void Notification::setDismissed(bool dismissed)
d->dismissed = dismissed;
}
/*bool Notification::operator==(const Notification &other) const
void Notification::processHints(const QVariantMap &hints)
{
return other.d->id == d->id;
}*/
d->processHints(hints);
}
......@@ -62,7 +62,7 @@ public:
QDateTime created() const;
QDateTime updated() const;
void setUpdated(); // FIXME find better name
void resetUpdated();
QString summary() const;
// FIXME remove all those setters as Notification is pretty much immutable
......@@ -80,6 +80,7 @@ public:
QString desktopEntry() const;
QString notifyRcName() const;
QString eventId() const;
QString applicationName() const;
void setApplicationName(const QString &applicationName);
......@@ -115,9 +116,7 @@ public:
bool dismissed() const;
void setDismissed(bool dismissed);
//bool operator==(const Notification &other) const;
//Notification(Notification &&other);
void processHints(const QVariantMap &hints);
private:
friend class NotificationModel;
......
......@@ -30,6 +30,9 @@
#include "notification_p.h"
#include <QDebug>
#include <QProcess>
#include <KShell>
#include <algorithm>
#include <functional>
......@@ -283,9 +286,30 @@ void NotificationModel::configure(uint notificationId)
return;
}
if (!notification.d->notifyRcName.isEmpty()) {
// TODO show knotifyconfigwidget thingie or emit a signal so we don't have any widget deps in this lib
qDebug() << "IMPLEMENT ME configure" << notification.d->id << "event" << notification.d->eventId << "of" << notification.d->notifyRcName;
if (!notification.desktopEntry().isEmpty() || !notification.notifyRcName().isEmpty()) {
// TODO would be nice to just have a signal but since NotificationModel is shared,
// if we connect to this from Notifications you would get a signal in every instance
// and potentialy open the config dialog multiple times.
QStringList args;
if (!notification.desktopEntry().isEmpty()) {
args.append(QStringLiteral("--desktop-entry"));
args.append(notification.desktopEntry());
}
if (!notification.notifyRcName().isEmpty()) {
args.append(QStringLiteral("--notifyrc"));
args.append(notification.notifyRcName());
}
if (!notification.d->eventId.isEmpty()) {
args.append(QStringLiteral("--event-id"));
args.append(notification.d->eventId);
}
QProcess::startDetached(QStringLiteral("kcmshell5"), {
QStringLiteral("notifications"),
QStringLiteral("--args"),
KShell::joinArgs(args)
});
return;
}
......@@ -308,7 +332,7 @@ void NotificationModel::invokeDefaultAction(uint notificationId)
NotificationServer::self().invokeAction(notificationId, QStringLiteral("default")); // FIXME make a static Notification::defaultActionName() or something
}
void NotificationModel::invoke(uint notificationId, const QString &actionName)
void NotificationModel::invokeAction(uint notificationId, const QString &actionName)
{
const int idx = d->indexOfNotification(notificationId);
if (idx == -1) {
......@@ -323,3 +347,53 @@ void NotificationModel::invoke(uint notificationId, const QString &actionName)
NotificationServer::self().invokeAction(notificationId, actionName);
}
void NotificationModel::clear(Notifications::ClearFlags flags)
{
if (d->notifications.isEmpty()) {
return;
}
// Tries to remove a contiguous group if possible as the likely case is
// you have n unread notifications at the end of the list, we don't want to
// remove and signal each item individually
QVector<QPair<int, int>> clearQueue;
QPair<int, int> clearRange{-1, -1};
for (int i = d->notifications.count() - 1; i >= 0; --i) {
const Notification &notification = d->notifications.at(i);
bool clear = (flags.testFlag(Notifications::ClearExpired) && notification.expired())
|| (flags.testFlag(Notifications::ClearDismissed) && notification.dismissed());
if (clear) {
if (clearRange.second == -1) {
clearRange.second = i;
}
clearRange.first = i;
} else {
if (clearRange.first != -1) {
clearQueue.append(clearRange);
clearRange.first = -1;
clearRange.second = -1;
}
}
}
if (clearRange.first != -1) {
clearQueue.append(clearRange);
clearRange.first = -1;
clearRange.second = -1;
}
for (const auto &range : clearQueue) {
beginRemoveRows(QModelIndex(), range.first, range.second);
for (int i = range.second; i >= range.first; --i) {
d->notifications.removeAt(i);
}
endRemoveRows();
}
qDebug() << "clearing the following ranges" << clearQueue;
}
......@@ -24,6 +24,8 @@
#include <QScopedPointer>
#include <QSharedPointer>
#include "notifications.h"
namespace NotificationManager
{
......@@ -46,12 +48,13 @@ public:
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
// TODO should we have an API taking int row, or QModelIndex?
Q_INVOKABLE void expire(uint notificationId);
Q_INVOKABLE void close(uint notificationId);
Q_INVOKABLE void configure(uint notificationId);
Q_INVOKABLE void invokeDefaultAction(uint notificationId);
// FIXME rename invokeAction
Q_INVOKABLE void invoke(uint notificationId, const QString &actionName);
void expire(uint notificationId);
void close(uint notificationId);
void configure(uint notificationId);
void invokeDefaultAction(uint notificationId);
void invokeAction(uint notificationId, const QString &actionName);
void clear(Notifications::ClearFlags flags);
signals:
void lastReadChanged();
......
......@@ -499,7 +499,7 @@ void Notifications::invokeDefaultAction(const QModelIndex &idx)
void Notifications::invokeAction(const QModelIndex &idx, const QString &actionId)
{
d->notificationsModel->invoke(Private::notificationId(idx), actionId);
d->notificationsModel->invokeAction(Private::notificationId(idx), actionId);
}
void Notifications::suspendJob(const QModelIndex &idx)
......@@ -519,8 +519,8 @@ void Notifications::killJob(const QModelIndex &idx)
void Notifications::clear(ClearFlags flags)
{
Q_UNUSED(flags);
// TODO implement
d->notificationsModel->clear(flags);
d->jobsModel->clear(flags);
}
QVariant Notifications::data(const QModelIndex &index, int role) const
......
......@@ -179,7 +179,7 @@ public:
*/
ExpiredRole,
/**
* The notificaton got dismissed by the user or not presented
* The notification got dismissed by the user or not presented
* because of Do Not Disturb settings, but can still be
* listed in e.g. a missed notifications list.
*/
......
......@@ -22,6 +22,9 @@
#include "notificationserver_p.h"
#include "notification.h"
#include "notification_p.h"
#include "debug.h"
#include <QDBusConnection>
......@@ -63,6 +66,11 @@ void NotificationServer::invokeAction(uint notificationId, const QString &action
emit d->ActionInvoked(notificationId, actionName);
}
uint NotificationServer::add(const Notification &notification)
{
return d->add(notification);
}
bool NotificationServer::inhibited() const
{
return d->inhibited();
......
......@@ -85,6 +85,16 @@ public:
*/
void invokeAction(uint id, const QString &actionName);
/**
* Adds a notification
*
* @note The notification isn't actually broadcast
* but just emitted locally.
*
* @return the ID of the notification
*/
uint add(const Notification &notification);
Q_SIGNALS:
/**
* Emitted when a notification was added.
......
......@@ -32,6 +32,13 @@
#include <QDBusConnection>
#include <QDBusServiceWatcher>
#include <KConfigGroup>
#include <KSharedConfig>
#include <KUser>
#include <processcore/processes.h>
#include <processcore/process.h>
using namespace NotificationManager;
NotificationServerPrivate::NotificationServerPrivate(QObject *parent)
......@@ -75,6 +82,15 @@ NotificationServerPrivate::NotificationServerPrivate(QObject *parent)
qCDebug(NOTIFICATIONMANAGER) << "Registered Notification service on DBus";
m_valid = true;
KConfigGroup config(KSharedConfig::openConfig(), QStringLiteral("Notifications"));
const bool broadcastsEnabled = config.readEntry("ListenForBroadcasts", false);
if (broadcastsEnabled) {
qCDebug(NOTIFICATIONMANAGER) << "Notification server is configured to listen for broadcasts";
QDBusConnection::systemBus().connect({}, {}, QStringLiteral("org.kde.BroadcastNotifications"),
QStringLiteral("Notify"), this, SLOT(onBroadcastNotification(QMap<QString,QVariant>)));
}
}
NotificationServerPrivate::~NotificationServerPrivate() = default;
......@@ -107,8 +123,26 @@ uint NotificationServerPrivate::Notify(const QString &app_name, uint replaces_id
// might override some of the things we set above (like application name)
notification.d->processHints(hints);
// No application name? Try to figure out the process name using the sender's PID
if (notification.applicationName().isEmpty()) {
qCDebug(NOTIFICATIONMANAGER) << "Notification from service" << message().service() << "didn't contain any identification information, this is an application bug";
QDBusReply<uint> pidReply = connection().interface()->servicePid(message().service());
if (pidReply.isValid()) {
const auto pid = pidReply.value();
KSysGuard::Processes procs;
procs.updateOrAddProcess(pid);
if (KSysGuard::Process *proc = procs.getProcess(pid)) {
qCDebug(NOTIFICATIONMANAGER) << "Resolved notification to be from PID" << pid << "which is" << proc->name();
notification.setApplicationName(proc->name());
}
}
}
if (wasReplaced) {
notification.setUpdated();
notification.resetUpdated();
emit static_cast<NotificationServer*>(parent())->notificationReplaced(replaces_id, notification);
} else {
emit static_cast<NotificationServer*>(parent())->notificationAdded(notification);
......@@ -150,6 +184,60 @@ QString NotificationServerPrivate::GetServerInformation(QString &vendor, QString
return QStringLiteral("Plasma");
}
void NotificationServerPrivate::onBroadcastNotification(const QMap<QString, QVariant> &properties)
{
qCDebug(NOTIFICATIONMANAGER) << "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) {