From bf487ebb2d0aaaca1288b83af473f389bed77759 Mon Sep 17 00:00:00 2001 From: reionwong Date: Fri, 24 Sep 2021 12:01:30 +0800 Subject: [PATCH] Add notification module --- CMakeLists.txt | 3 +- notificationd/CMakeLists.txt | 23 ++ notificationd/images/dark/close.svg | 10 + notificationd/images/light/close.svg | 10 + notificationd/main.cpp | 42 ++++ notificationd/main.qml | 0 notificationd/notification.cpp | 20 ++ notificationd/notification.h | 42 ++++ notificationd/notificationserver.cpp | 155 ++++++++++++ notificationd/notificationserver.h | 76 ++++++ notificationd/notificationsmodel.cpp | 221 ++++++++++++++++++ notificationd/notificationsmodel.h | 56 +++++ .../org.freedesktop.Notifications.xml | 69 ++++++ notificationd/qml/NotificationPopup.qml | 131 +++++++++++ notificationd/qml/main.qml | 90 +++++++ notificationd/resources.qrc | 8 + notificationd/screenhelper.cpp | 39 ++++ notificationd/screenhelper.h | 43 ++++ notificationd/view.cpp | 16 ++ notificationd/view.h | 15 ++ session/processmanager.cpp | 1 + 21 files changed, 1069 insertions(+), 1 deletion(-) create mode 100644 notificationd/CMakeLists.txt create mode 100644 notificationd/images/dark/close.svg create mode 100644 notificationd/images/light/close.svg create mode 100644 notificationd/main.cpp delete mode 100644 notificationd/main.qml create mode 100644 notificationd/org.freedesktop.Notifications.xml create mode 100644 notificationd/qml/NotificationPopup.qml create mode 100644 notificationd/qml/main.qml create mode 100644 notificationd/resources.qrc create mode 100644 notificationd/screenhelper.cpp create mode 100644 notificationd/screenhelper.h create mode 100644 notificationd/view.cpp create mode 100644 notificationd/view.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 47d2adc..f92f2f1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -32,5 +32,6 @@ add_subdirectory(cpufreq) add_subdirectory(chotkeys) add_subdirectory(cupdatecursor) add_subdirectory(gmenuproxy) +add_subdirectory(notificationd) -install(FILES cutefish DESTINATION /etc/ COMPONENT Runtime) \ No newline at end of file +install(FILES cutefish DESTINATION /etc/ COMPONENT Runtime) diff --git a/notificationd/CMakeLists.txt b/notificationd/CMakeLists.txt new file mode 100644 index 0000000..8da6083 --- /dev/null +++ b/notificationd/CMakeLists.txt @@ -0,0 +1,23 @@ +set(SRCS main.cpp + screenhelper.cpp + notificationsmodel.cpp + notificationserver.cpp + notification.cpp + resources.qrc +) + +qt_add_dbus_adaptor(SRCS org.freedesktop.Notifications.xml notificationserver.h NotificationServer) + +add_executable(cutefish-notificationd ${SRCS}) + +target_link_libraries(cutefish-notificationd + Qt5::Core + Qt5::DBus + Qt5::Quick + Qt5::Widgets +) + +install(TARGETS cutefish-notificationd + DESTINATION /usr/bin + COMPONENT Runtime +) diff --git a/notificationd/images/dark/close.svg b/notificationd/images/dark/close.svg new file mode 100644 index 0000000..c36640a --- /dev/null +++ b/notificationd/images/dark/close.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/notificationd/images/light/close.svg b/notificationd/images/light/close.svg new file mode 100644 index 0000000..7c3a953 --- /dev/null +++ b/notificationd/images/light/close.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/notificationd/main.cpp b/notificationd/main.cpp new file mode 100644 index 0000000..dc13c61 --- /dev/null +++ b/notificationd/main.cpp @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2021 CutefishOS Team. + * + * Author: Reion Wong + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include +#include +#include + +#include "view.h" +#include "notificationsmodel.h" +#include "screenhelper.h" + +int main(int argc, char *argv[]) +{ + // QCoreApplication::setAttribute(Qt::AA_UseHighDpiPixmaps, true); + QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling, true); + + QApplication a(argc, argv); + + qmlRegisterType("Cutefish.Notification", 1, 0, "NotificationsModel"); + qmlRegisterType("Cutefish.Notification", 1, 0, "ScreenHelper"); + + QQmlApplicationEngine engine; + engine.load(QUrl("qrc:/qml/main.qml")); + + return a.exec(); +} diff --git a/notificationd/main.qml b/notificationd/main.qml deleted file mode 100644 index e69de29..0000000 diff --git a/notificationd/notification.cpp b/notificationd/notification.cpp index e69de29..37c8f91 100644 --- a/notificationd/notification.cpp +++ b/notificationd/notification.cpp @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2021 CutefishOS Team. + * + * Author: Reion Wong + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "notification.h" diff --git a/notificationd/notification.h b/notificationd/notification.h index e69de29..53d89e7 100644 --- a/notificationd/notification.h +++ b/notificationd/notification.h @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2021 CutefishOS Team. + * + * Author: Reion Wong + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef NOTIFICATION_H +#define NOTIFICATION_H + +#include +#include + +class Notification +{ +public: + uint id = 0; + QString service; + QString summary; + QString body; + QString appName; + QString appIcon; + QStringList actions; + int timeout = -1; + + QDateTime created; + QDateTime updated; +}; + +#endif // NOTIFICATION_H diff --git a/notificationd/notificationserver.cpp b/notificationd/notificationserver.cpp index e69de29..8324fe2 100644 --- a/notificationd/notificationserver.cpp +++ b/notificationd/notificationserver.cpp @@ -0,0 +1,155 @@ +/* + * SPDX-FileCopyrightText: 2021 Reion Wong + * SPDX-FileCopyrightText: 2018-2019 Kai Uwe Broulik + * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL +*/ + +#include "notificationserver.h" +#include "notificationsadaptor.h" + +#include + +static NotificationServer *NOTIFICATION_SERVER_SELF = nullptr; + +NotificationServer *NotificationServer::self() +{ + if (!NOTIFICATION_SERVER_SELF) + NOTIFICATION_SERVER_SELF = new NotificationServer; + + return NOTIFICATION_SERVER_SELF; +} + +NotificationServer::NotificationServer(QObject *parent) + : QObject(parent) + , m_notificationWatcher(nullptr) +{ + new NotificationsAdaptor(this); + + QDBusConnectionInterface *iface = QDBusConnection::sessionBus().interface(); + + QDBusConnection::sessionBus().registerObject("/org/freedesktop/Notifications", this); + auto registration = iface->registerService("org.freedesktop.Notifications", + QDBusConnectionInterface::ReplaceExistingService, + QDBusConnectionInterface::DontAllowReplacement); +} + +uint NotificationServer::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) +{ + Q_UNUSED(hints); + + uint id = 0; + + const bool wasReplaced = replaces_id > 0; + + if (wasReplaced) { + id = replaces_id; + } else { + if (!m_highestId) + ++m_highestId; + + id = m_highestId; + ++m_highestId; + } + + Notification notification; + notification.id = id; + notification.created = QDateTime::currentDateTimeUtc(); + notification.service = message().service(); + notification.summary = summary; + notification.body = body; + notification.appName = app_name; + notification.appIcon = app_icon; + notification.actions = actions; + notification.timeout = timeout; + + uint pid = 0; + QDBusReply pidReply = connection().interface()->servicePid(message().service()); + if (pidReply.isValid()) { + pid = pidReply.value(); + } + + if (pid > 0) { + // 查找 app name + if (notification.appName.isEmpty()) { + + } + } + + if (wasReplaced) { + notification.updated = QDateTime::currentDateTimeUtc(); + emit notificationReplaced(replaces_id, notification); + } else { + emit notificationAdded(notification); + } + + return id; +} + +void NotificationServer::CloseNotification(uint id) +{ + Q_UNUSED(id); +} + +QStringList NotificationServer::GetCapabilities() const +{ + return QStringList{ QStringLiteral("body"), + QStringLiteral("body-hyperlinks"), + QStringLiteral("body-markup"), + QStringLiteral("body-images"), + QStringLiteral("icon-static"), + QStringLiteral("actions"), + QStringLiteral("persistence"), + QStringLiteral("inline-reply"), + QStringLiteral("inhibitions") }; +} + +QString NotificationServer::GetServerInformation(QString &vendor, QString &version, QString &specVersion) const +{ + vendor = "CutefishOS"; + version = "1.0"; + specVersion = "1.2"; + + return "Cutefish"; +} + +uint NotificationServer::Inhibit(const QString &desktop_entry, const QString &reason, const QVariantMap &hints) +{ + return 0; +} + +void NotificationServer::UnInhibit(uint cookie) +{ + Q_UNUSED(cookie) +} + +bool NotificationServer::inhibited() const +{ + return false; +} + +void NotificationServer::RegisterWatcher() +{ + m_notificationWatcher->addWatchedService(message().service()); +} + +void NotificationServer::UnRegisterWatcher() +{ + m_notificationWatcher->removeWatchedService(message().service()); +} + +void NotificationServer::InvokeAction(uint id, const QString &actionKey) +{ + Q_EMIT ActionInvoked(id, actionKey); +} + +void NotificationServer::closeNotification(uint id, NotificationServer::CloseReason reason) +{ + emit notificationRemoved(id, reason); +} diff --git a/notificationd/notificationserver.h b/notificationd/notificationserver.h index e69de29..6f2b7cc 100644 --- a/notificationd/notificationserver.h +++ b/notificationd/notificationserver.h @@ -0,0 +1,76 @@ +/* + * SPDX-FileCopyrightText: 2021 Reion Wong + * SPDX-FileCopyrightText: 2018-2019 Kai Uwe Broulik + * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL +*/ + +#ifndef NOTIFICATIONSERVER_H +#define NOTIFICATIONSERVER_H + +#include +#include +#include +#include + +#include "notification.h" + +class NotificationServer : public QObject, protected QDBusContext +{ + Q_OBJECT + +public: + enum class CloseReason { + Expired = 1, ///< The notification timed out + DismissedByUser = 2, ///< The user explicitly closed or acknowledged the notification + Revoked = 3, ///< The notification was revoked by the issuing app because it is no longer relevant + }; + Q_ENUM(CloseReason) + + static NotificationServer *self(); + + explicit NotificationServer(QObject *parent = nullptr); + + // DBus + uint 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 CloseNotification(uint id); + 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); + bool inhibited() const; // property getter + + // Notifition watcher + void RegisterWatcher(); + void UnRegisterWatcher(); + + void InvokeAction(uint id, const QString &actionKey); + + // Self + void closeNotification(uint id, CloseReason reason); + +Q_SIGNALS: + // DBus + void NotificationClosed(uint id, uint reason); + void ActionInvoked(uint id, const QString &actionKey); + + // Self + void notificationAdded(const Notification ¬ification); + void notificationReplaced(uint replacedId, const Notification ¬ification); + void notificationRemoved(uint id, CloseReason reason); + +private: + uint m_highestId = -1; + Notification m_lastNotification; + QDBusServiceWatcher *m_notificationWatcher; +}; + +#endif // NOTIFICATIONSERVER_H diff --git a/notificationd/notificationsmodel.cpp b/notificationd/notificationsmodel.cpp index e69de29..66dfd69 100644 --- a/notificationd/notificationsmodel.cpp +++ b/notificationd/notificationsmodel.cpp @@ -0,0 +1,221 @@ +/* + * SPDX-FileCopyrightText: 2021 Reion Wong + * SPDX-FileCopyrightText: 2018-2019 Kai Uwe Broulik + * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL +*/ + +#include "notificationsmodel.h" +#include "notification.h" + +#include +#include + +static const int s_notificationsLimit = 1000; + +NotificationsModel::NotificationsModel(QObject *parent) + : QAbstractListModel(parent) +{ + m_pendingRemovalTimer.setSingleShot(true); + m_pendingRemovalTimer.setInterval(50); + + connect(&m_pendingRemovalTimer, &QTimer::timeout, this, [this] { + QVector rowsToBeRemoved; + rowsToBeRemoved.reserve(m_pendingRemovals.count()); + for (uint id : qAsConst(m_pendingRemovals)) { + int row = rowOfNotification(id); // oh the complexity... + if (row == -1) { + continue; + } + rowsToBeRemoved.append(row); + } + + removeRows(rowsToBeRemoved); + }); + + connect(NotificationServer::self(), &NotificationServer::notificationAdded, this, &NotificationsModel::onNotificationAdded); + connect(NotificationServer::self(), &NotificationServer::notificationReplaced, this, &NotificationsModel::onNotificationReplaced); + connect(NotificationServer::self(), &NotificationServer::notificationRemoved, this, &NotificationsModel::onNotificationRemoved); +} + +QVariant NotificationsModel::data(const QModelIndex &index, int role) const +{ + const Notification ¬ification = m_notifications.at(index.row()); + + switch (role) { + case NotificationsModel::IdRole: + return notification.id; + case NotificationsModel::SummaryRole: + return notification.summary; + case NotificationsModel::ImageRole: + return ""; + case NotificationsModel::CreatedRole: + return notification.created; + case NotificationsModel::BodyRole: + return notification.body; + case NotificationsModel::IconNameRole: + return notification.appIcon; + default: + break; + } + + return QVariant(); +} + +bool NotificationsModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + // Notification ¬ification = m_notifications[index.row()]; + bool dirty = false; + return dirty; +} + +int NotificationsModel::rowCount(const QModelIndex &parent) const +{ + if (parent.isValid()) + return 0; + + return m_notifications.size(); +} + +QHash NotificationsModel::roleNames() const +{ + static QHash s_roles; + + if (s_roles.isEmpty()) { + // This generates role names from the Roles enum in the form of: FooRole -> foo + const QMetaEnum e = QMetaEnum::fromType(); + + // Qt built-in roles we use + s_roles.insert(Qt::DisplayRole, QByteArrayLiteral("display")); + s_roles.insert(Qt::DecorationRole, QByteArrayLiteral("decoration")); + + for (int i = 0; i < e.keyCount(); ++i) { + const int value = e.value(i); + + QByteArray key(e.key(i)); + key[0] = key[0] + 32; // lower case first letter + key.chop(4); // strip "Role" suffix + + s_roles.insert(value, key); + } + + s_roles.insert(NotificationsModel::IdRole, QByteArrayLiteral("notificationId")); // id is QML-reserved + } + + return s_roles; +} + +void NotificationsModel::close(uint id) +{ + if (rowOfNotification(id) > -1) { + NotificationServer::self()->closeNotification(id, NotificationServer::CloseReason::DismissedByUser); + } +} + +int NotificationsModel::rowOfNotification(uint id) const +{ + auto it = std::find_if(m_notifications.constBegin(), m_notifications.constEnd(), [id](const Notification &item) { + return item.id == id; + }); + + if (it == m_notifications.constEnd()) { + return -1; + } + + return std::distance(m_notifications.constBegin(), it); +} + +void NotificationsModel::removeRows(const QVector &rows) +{ + if (rows.isEmpty()) { + return; + } + + QVector rowsToBeRemoved(rows); + std::sort(rowsToBeRemoved.begin(), rowsToBeRemoved.end()); + + QVector> clearQueue; + + QPair clearRange{rowsToBeRemoved.first(), rowsToBeRemoved.first()}; + + for (int row : rowsToBeRemoved) { + if (row > clearRange.second + 1) { + clearQueue.append(clearRange); + clearRange.first = row; + } + + clearRange.second = row; + } + + if (clearQueue.isEmpty() || clearQueue.last() != clearRange) { + clearQueue.append(clearRange); + } + + int rowsRemoved = 0; + + for (int i = clearQueue.count() - 1; i >= 0; --i) { + const auto &range = clearQueue.at(i); + + beginRemoveRows(QModelIndex(), range.first, range.second); + for (int j = range.second; j >= range.first; --j) { + m_notifications.removeAt(j); + ++rowsRemoved; + } + endRemoveRows(); + } + + Q_ASSERT(rowsRemoved == rowsToBeRemoved.count()); + + m_pendingRemovals.clear(); +} + +void NotificationsModel::onNotificationAdded(const Notification ¬ification) +{ + if (m_notifications.size() >= s_notificationsLimit) { + const int cleanupCount = s_notificationsLimit / 2; + beginRemoveRows(QModelIndex(), 0, cleanupCount - 1); + for (int i = 0; i < cleanupCount; ++i) { + m_notifications.removeAt(0); + } + endRemoveRows(); + } + + beginInsertRows(QModelIndex(), m_notifications.size(), m_notifications.size()); + m_notifications.append(std::move(notification)); + endInsertRows(); +} + +void NotificationsModel::onNotificationReplaced(uint replacedId, const Notification ¬ification) +{ + +} + +void NotificationsModel::onNotificationRemoved(uint removedId, NotificationServer::CloseReason reason) +{ + const int row = rowOfNotification(removedId); + + if (row == -1) { + return; + } + + // When a notification expired, keep it around in the history and mark it as such + if (reason == NotificationServer::CloseReason::Expired) { + const QModelIndex idx = NotificationsModel::index(row, 0); + Notification ¬ification = m_notifications[row]; + // notification.setExpired(true); + notification.actions.clear(); + emit dataChanged(idx, idx); + return; + } + + // Otherwise if explicitly closed by either user or app, mark it for removal + // some apps are notorious for closing a bunch of notifications at once + // causing newer notifications to move up and have a dialogs created for them + // just to then be discarded causing excess CPU usage + if (!m_pendingRemovals.contains(removedId)) { + m_pendingRemovals.append(removedId); + } + + if (!m_pendingRemovalTimer.isActive()) { + m_pendingRemovalTimer.start(); + } +} diff --git a/notificationd/notificationsmodel.h b/notificationd/notificationsmodel.h index e69de29..61ce050 100644 --- a/notificationd/notificationsmodel.h +++ b/notificationd/notificationsmodel.h @@ -0,0 +1,56 @@ +/* + * SPDX-FileCopyrightText: 2021 Reion Wong + * SPDX-FileCopyrightText: 2018-2019 Kai Uwe Broulik + * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL +*/ + +#ifndef NOTIFICATIONSMODEL_H +#define NOTIFICATIONSMODEL_H + +#include +#include +#include + +#include "notificationserver.h" + +class Notification; +class NotificationsModel : public QAbstractListModel +{ + Q_OBJECT + +public: + enum Roles { + IdRole = Qt::UserRole + 1, + SummaryRole = Qt::DisplayRole, + ImageRole = Qt::DecorationRole, + CreatedRole, + UpdatedRole, + BodyRole, + IconNameRole, + }; + Q_ENUM(Roles) + + explicit NotificationsModel(QObject *parent = nullptr); + + QVariant data(const QModelIndex &index, int role) const override; + bool setData(const QModelIndex &index, const QVariant &value, int role) override; + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QHash roleNames() const override; + + Q_INVOKABLE void close(uint id); + + int rowOfNotification(uint id) const; + void removeRows(const QVector &rows); + +private slots: + void onNotificationAdded(const Notification ¬ification); + void onNotificationReplaced(uint replacedId, const Notification ¬ification); + void onNotificationRemoved(uint notificationId, NotificationServer::CloseReason reason); + +private: + QVector m_notifications; + QVector m_pendingRemovals; + QTimer m_pendingRemovalTimer; +}; + +#endif // NOTIFICATIONSMODEL_H diff --git a/notificationd/org.freedesktop.Notifications.xml b/notificationd/org.freedesktop.Notifications.xml new file mode 100644 index 0000000..5cf538e --- /dev/null +++ b/notificationd/org.freedesktop.Notifications.xml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/notificationd/qml/NotificationPopup.qml b/notificationd/qml/NotificationPopup.qml new file mode 100644 index 0000000..2d37116 --- /dev/null +++ b/notificationd/qml/NotificationPopup.qml @@ -0,0 +1,131 @@ +/* + * Copyright (C) 2021 CutefishOS Team. + * + * Author: Reion Wong + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import QtQuick 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.12 +import QtQuick.Window 2.12 +import QtGraphicalEffects 1.0 +import FishUI 1.0 as FishUI +import Cutefish.Notification 1.0 + +Window { + id: control + + flags: Qt.WindowDoesNotAcceptFocus | Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint | Qt.Popup + + width: 400 + height: 70 + color: "transparent" + + visible: false + + onVisibleChanged: if (visible) timer.restart() + + FishUI.WindowShadow { + view: control + radius: _background.radius + } + + FishUI.WindowBlur { + view: control + geometry: Qt.rect(control.x, control.y, control.width, control.height) + windowRadius: _background.radius + enabled: true + } + + Rectangle { + id: _background + anchors.fill: parent + radius: height * 0.2 + color: FishUI.Theme.backgroundColor + opacity: 0.5 + } + + MouseArea { + id: _mouseArea + z: 999 + anchors.fill: parent + onClicked: notificationsModel.close(model.notificationId) + hoverEnabled: true + onEntered: timer.stop() + onExited: timer.restart() + } + + RowLayout { + anchors.fill: parent + anchors.leftMargin: FishUI.Units.largeSpacing + anchors.rightMargin: FishUI.Units.smallSpacing + anchors.topMargin: FishUI.Units.smallSpacing + anchors.bottomMargin: FishUI.Units.smallSpacing + spacing: FishUI.Units.largeSpacing + + Image { + id: _icon + width: 48 + height: width + source: "image://icontheme/%1".arg(model.iconName) + sourceSize: Qt.size(width, height) + Layout.alignment: Qt.AlignVCenter + } + + ColumnLayout { + spacing: 0 + + Item { + Layout.fillHeight: true + } + + Label { + text: model.summary + visible: text + elide: Text.ElideRight + Layout.fillWidth: true + } + + Label { + text: model.body + visible: text + elide: Text.ElideRight + Layout.fillWidth: true + } + + Item { + Layout.fillHeight: true + } + } + + Image { + width: 24 + height: 24 + source: "qrc:/images/" + (FishUI.Theme.darkMode ? "dark" : "light") + "/close.svg" + sourceSize: Qt.size(width, height) + visible: _mouseArea.containsMouse + Layout.alignment: Qt.AlignTop + } + } + + Timer { + id: timer + interval: 5000 + onTriggered: { + notificationsModel.close(model.notificationId) + } + } +} diff --git a/notificationd/qml/main.qml b/notificationd/qml/main.qml new file mode 100644 index 0000000..57be550 --- /dev/null +++ b/notificationd/qml/main.qml @@ -0,0 +1,90 @@ +/* + SPDX-FileCopyrightText: 2021 Reion Wong + SPDX-FileCopyrightText: 2019 Kai Uwe Broulik + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +import QtQuick 2.12 +import QtQml 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.12 +import QtQuick.Window 2.12 +import QtGraphicalEffects 1.0 +import FishUI 1.0 as FishUI +import Cutefish.Notification 1.0 + +Item { + id: root + + width: 0 + height: 0 + visible: false + + property int popupEdgeDistance: FishUI.Units.largeSpacing + property int popupSpacing: FishUI.Units.largeSpacing + readonly property real popupMaximumScreenFill: 0.8 + + readonly property rect screenRect: { + let rect = Qt.rect(screen.screenGeometry.x + screen.availableScreenRect.x, + screen.screenGeometry.y + screen.availableScreenRect.y, + screen.availableScreenRect.width, + screen.availableScreenRect.height) + return rect + } + + property Instantiator popupInstantiator: Instantiator { + model: notificationsModel + delegate: NotificationPopup {} + + onObjectAdded: { + positionPopups() + } + + onObjectRemoved: { + positionPopups() + } + } + + ScreenHelper { + id: screen + } + + NotificationsModel { + id: notificationsModel + } + + function positionPopups() { + const screenRect = root.screenRect + + if (screenRect.width <= 0 || screenRect.height <= 0) { + return + } + + let y = screenRect.y + y += root.popupEdgeDistance + + for (var i = 0; i < popupInstantiator.count; ++i) { + let popup = popupInstantiator.objectAt(i) + + if (!popup) + continue + + var popupEffectiveWidth = popup.width + const leftMostX = screenRect.x + root.popupEdgeDistance + const rightMostX = screenRect.x + screenRect.width - root.popupEdgeDistance - popupEffectiveWidth + + // right + popup.x = rightMostX + popup.y = y + y += popup.height + (popup.height > 0 ? popupSpacing : 0) + + // don't let notifications take more than popupMaximumScreenFill of the screen + var visible = true + if (i > 0) { // however always show at least one popup + visible = (popup.y + popup.height < screenRect.y + (screenRect.height * popupMaximumScreenFill)) + } + + popup.visible = visible + } + } +} diff --git a/notificationd/resources.qrc b/notificationd/resources.qrc new file mode 100644 index 0000000..14901a9 --- /dev/null +++ b/notificationd/resources.qrc @@ -0,0 +1,8 @@ + + + qml/main.qml + qml/NotificationPopup.qml + images/dark/close.svg + images/light/close.svg + + diff --git a/notificationd/screenhelper.cpp b/notificationd/screenhelper.cpp new file mode 100644 index 0000000..a6d4ecc --- /dev/null +++ b/notificationd/screenhelper.cpp @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2021 CutefishOS Team. + * + * Author: Reion Wong + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "screenhelper.h" +#include +#include + +ScreenHelper::ScreenHelper(QObject *parent) + : QObject(parent) +{ + connect(qApp->primaryScreen(), &QScreen::geometryChanged, this, &ScreenHelper::screenGeometryChanged); + connect(qApp->primaryScreen(), &QScreen::availableGeometryChanged, this, &ScreenHelper::availableScreenRectChanged); +} + +QRect ScreenHelper::screenGeometry() const +{ + return qApp->primaryScreen()->geometry(); +} + +QRect ScreenHelper::availableScreenRect() const +{ + return qApp->primaryScreen()->availableGeometry(); +} diff --git a/notificationd/screenhelper.h b/notificationd/screenhelper.h new file mode 100644 index 0000000..4ca4f1d --- /dev/null +++ b/notificationd/screenhelper.h @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2021 CutefishOS Team. + * + * Author: Reion Wong + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef SCREENHELPER_H +#define SCREENHELPER_H + +#include +#include + +class ScreenHelper : public QObject +{ + Q_OBJECT + Q_PROPERTY(QRect screenGeometry READ screenGeometry NOTIFY screenGeometryChanged) + Q_PROPERTY(QRect availableScreenRect READ availableScreenRect NOTIFY availableScreenRectChanged) + +public: + explicit ScreenHelper(QObject *parent = nullptr); + + QRect screenGeometry() const; + QRect availableScreenRect() const; + +signals: + void screenGeometryChanged(); + void availableScreenRectChanged(); +}; + +#endif // SCREENHELPER_H diff --git a/notificationd/view.cpp b/notificationd/view.cpp new file mode 100644 index 0000000..fed8e0d --- /dev/null +++ b/notificationd/view.cpp @@ -0,0 +1,16 @@ +#include "view.h" + +#include +#include + +View::View(QQuickView *parent) + : QQuickView(parent) +{ + rootContext()->setContextProperty("View", this); + + setFlags(Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint); + // setResizeMode(QQuickView::SizeViewToRootObject); + setSource(QUrl("qrc:/qml/main.qml")); + setScreen(qApp->primaryScreen()); + setColor(Qt::transparent); +} diff --git a/notificationd/view.h b/notificationd/view.h new file mode 100644 index 0000000..1870452 --- /dev/null +++ b/notificationd/view.h @@ -0,0 +1,15 @@ +#ifndef VIEW_H +#define VIEW_H + +#include + +class View : public QQuickView +{ + Q_OBJECT + +public: + explicit View(QQuickView *parent = nullptr); + +}; + +#endif // VIEW_H diff --git a/session/processmanager.cpp b/session/processmanager.cpp index 7b5fd25..cccea9e 100644 --- a/session/processmanager.cpp +++ b/session/processmanager.cpp @@ -118,6 +118,7 @@ void ProcessManager::startDesktopProcess() QList> list; // Desktop components + list << qMakePair(QString("cutefish-notificationd"), QStringList()); list << qMakePair(QString("cutefish-statusbar"), QStringList()); list << qMakePair(QString("cutefish-dock"), QStringList()); list << qMakePair(QString("cutefish-filemanager"), QStringList("--desktop"));