mirror of https://github.com/cutefishos/core
Add notification module
parent
335f79aaf8
commit
bf487ebb2d
@ -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
|
||||
)
|
||||
@ -0,0 +1,10 @@
|
||||
<svg width="30" height="30" version="1.1" viewBox="0 0 7.9375 7.9375" xmlns="http://www.w3.org/2000/svg">
|
||||
<g transform="translate(0 -289.06)">
|
||||
<rect x=".86407" y="289.92" width="6.2094" height="6.2267" ry="6.589e-6" opacity="0" stroke-width=".18433" style="paint-order:markers stroke fill"/>
|
||||
<g transform="matrix(.0065117 0 0 .0065117 2.3018 291.36)" fill="#fff">
|
||||
<g fill="#fff">
|
||||
<path d="m284.29 256 221.86-221.86c7.811-7.811 7.811-20.475 0-28.285s-20.475-7.811-28.285 0l-221.86 221.86-221.86-221.86c-7.811-7.811-20.475-7.811-28.285 0s-7.811 20.475 0 28.285l221.86 221.86-221.86 221.86c-7.811 7.811-7.811 20.475 0 28.285 3.905 3.905 9.024 5.857 14.143 5.857s10.237-1.952 14.143-5.857l221.86-221.86 221.86 221.86c3.905 3.905 9.024 5.857 14.143 5.857s10.237-1.952 14.143-5.857c7.811-7.811 7.811-20.475 0-28.285z" fill="#fff"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 866 B |
@ -0,0 +1,10 @@
|
||||
<svg width="30" height="30" version="1.1" viewBox="0 0 7.9375 7.9375" xmlns="http://www.w3.org/2000/svg">
|
||||
<g transform="translate(0 -289.06)">
|
||||
<rect x=".86407" y="289.92" width="6.2094" height="6.2267" ry="6.589e-6" opacity="0" stroke-width=".18433" style="paint-order:markers stroke fill"/>
|
||||
<g transform="matrix(.0065117 0 0 .0065117 2.3018 291.36)" fill="#383838">
|
||||
<g fill="#383838">
|
||||
<path d="m284.29 256 221.86-221.86c7.811-7.811 7.811-20.475 0-28.285s-20.475-7.811-28.285 0l-221.86 221.86-221.86-221.86c-7.811-7.811-20.475-7.811-28.285 0s-7.811 20.475 0 28.285l221.86 221.86-221.86 221.86c-7.811 7.811-7.811 20.475 0 28.285 3.905 3.905 9.024 5.857 14.143 5.857s10.237-1.952 14.143-5.857l221.86-221.86 221.86 221.86c3.905 3.905 9.024 5.857 14.143 5.857s10.237-1.952 14.143-5.857c7.811-7.811 7.811-20.475 0-28.285z" fill="#383838"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 875 B |
@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright (C) 2021 CutefishOS Team.
|
||||
*
|
||||
* Author: Reion Wong <reion@cutefishos.com>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include <QApplication>
|
||||
#include <QQmlApplicationEngine>
|
||||
#include <QQmlContext>
|
||||
|
||||
#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<NotificationsModel>("Cutefish.Notification", 1, 0, "NotificationsModel");
|
||||
qmlRegisterType<ScreenHelper>("Cutefish.Notification", 1, 0, "ScreenHelper");
|
||||
|
||||
QQmlApplicationEngine engine;
|
||||
engine.load(QUrl("qrc:/qml/main.qml"));
|
||||
|
||||
return a.exec();
|
||||
}
|
||||
@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright (C) 2021 CutefishOS Team.
|
||||
*
|
||||
* Author: Reion Wong <reion@cutefishos.com>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "notification.h"
|
||||
@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright (C) 2021 CutefishOS Team.
|
||||
*
|
||||
* Author: Reion Wong <reion@cutefishos.com>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef NOTIFICATION_H
|
||||
#define NOTIFICATION_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QDateTime>
|
||||
|
||||
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
|
||||
@ -0,0 +1,155 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2021 Reion Wong <reion@cutefishos.com>
|
||||
* SPDX-FileCopyrightText: 2018-2019 Kai Uwe Broulik <kde@privat.broulik.de>
|
||||
* SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
|
||||
*/
|
||||
|
||||
#include "notificationserver.h"
|
||||
#include "notificationsadaptor.h"
|
||||
|
||||
#include <QDebug>
|
||||
|
||||
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<uint> 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);
|
||||
}
|
||||
@ -0,0 +1,76 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2021 Reion Wong <reion@cutefishos.com>
|
||||
* SPDX-FileCopyrightText: 2018-2019 Kai Uwe Broulik <kde@privat.broulik.de>
|
||||
* SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
|
||||
*/
|
||||
|
||||
#ifndef NOTIFICATIONSERVER_H
|
||||
#define NOTIFICATIONSERVER_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QDBusConnection>
|
||||
#include <QDBusServiceWatcher>
|
||||
#include <QDBusContext>
|
||||
|
||||
#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
|
||||
@ -0,0 +1,221 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2021 Reion Wong <reion@cutefishos.com>
|
||||
* SPDX-FileCopyrightText: 2018-2019 Kai Uwe Broulik <kde@privat.broulik.de>
|
||||
* SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
|
||||
*/
|
||||
|
||||
#include "notificationsmodel.h"
|
||||
#include "notification.h"
|
||||
|
||||
#include <QMetaEnum>
|
||||
#include <QDebug>
|
||||
|
||||
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<int> 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<int, QByteArray> NotificationsModel::roleNames() const
|
||||
{
|
||||
static QHash<int, QByteArray> 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<NotificationsModel::Roles>();
|
||||
|
||||
// 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<int> &rows)
|
||||
{
|
||||
if (rows.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
QVector<int> rowsToBeRemoved(rows);
|
||||
std::sort(rowsToBeRemoved.begin(), rowsToBeRemoved.end());
|
||||
|
||||
QVector<QPair<int, int>> clearQueue;
|
||||
|
||||
QPair<int, int> 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();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,56 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2021 Reion Wong <reion@cutefishos.com>
|
||||
* SPDX-FileCopyrightText: 2018-2019 Kai Uwe Broulik <kde@privat.broulik.de>
|
||||
* SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
|
||||
*/
|
||||
|
||||
#ifndef NOTIFICATIONSMODEL_H
|
||||
#define NOTIFICATIONSMODEL_H
|
||||
|
||||
#include <QAbstractListModel>
|
||||
#include <QVector>
|
||||
#include <QTimer>
|
||||
|
||||
#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<int, QByteArray> roleNames() const override;
|
||||
|
||||
Q_INVOKABLE void close(uint id);
|
||||
|
||||
int rowOfNotification(uint id) const;
|
||||
void removeRows(const QVector<int> &rows);
|
||||
|
||||
private slots:
|
||||
void onNotificationAdded(const Notification ¬ification);
|
||||
void onNotificationReplaced(uint replacedId, const Notification ¬ification);
|
||||
void onNotificationRemoved(uint notificationId, NotificationServer::CloseReason reason);
|
||||
|
||||
private:
|
||||
QVector<Notification> m_notifications;
|
||||
QVector<uint /*notificationId*/> m_pendingRemovals;
|
||||
QTimer m_pendingRemovalTimer;
|
||||
};
|
||||
|
||||
#endif // NOTIFICATIONSMODEL_H
|
||||
@ -0,0 +1,69 @@
|
||||
<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN"
|
||||
"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
|
||||
<!--
|
||||
Based on the output of command:
|
||||
qdbus org.freedesktop.Notifications /org/freedesktop/Notifications org.freedesktop.DBus.Introspectable.Introspect
|
||||
against xfce4-notifyd
|
||||
-->
|
||||
<node>
|
||||
<!--interface name="org.freedesktop.DBus.Introspectable">
|
||||
<method name="Introspect">
|
||||
<arg name="data" direction="out" type="s"/>
|
||||
</method>
|
||||
</interface>
|
||||
<interface name="org.freedesktop.DBus.Properties">
|
||||
<method name="Get">
|
||||
<arg name="interface" direction="in" type="s"/>
|
||||
<arg name="propname" direction="in" type="s"/>
|
||||
<arg name="value" direction="out" type="v"/>
|
||||
</method>
|
||||
<method name="Set">
|
||||
<arg name="interface" direction="in" type="s"/>
|
||||
<arg name="propname" direction="in" type="s"/>
|
||||
<arg name="value" direction="in" type="v"/>
|
||||
</method>
|
||||
<method name="GetAll">
|
||||
<arg name="interface" direction="in" type="s"/>
|
||||
<arg name="props" direction="out" type="a{sv}"/>
|
||||
</method>
|
||||
</interface>
|
||||
<interface name="org.xfce.Notifyd">
|
||||
<method name="Quit">
|
||||
</method>
|
||||
</interface-->
|
||||
<interface name="org.freedesktop.Notifications">
|
||||
<method name="GetServerInformation">
|
||||
<arg name="name" type="s" direction="out"/>
|
||||
<arg name="vendor" type="s" direction="out"/>
|
||||
<arg name="version" type="s" direction="out"/>
|
||||
<arg name="spec_version" type="s" direction="out"/>
|
||||
</method>
|
||||
<method name="CloseNotification">
|
||||
<arg name="id" type="u" direction="in"/>
|
||||
</method>
|
||||
<method name="Notify">
|
||||
<annotation name="org.qtproject.QtDBus.QtTypeName.In6" value="QVariantMap"/>
|
||||
<arg name="app_name" type="s" direction="in"/>
|
||||
<arg name="replaces_id" type="u" direction="in"/>
|
||||
<arg name="app_icon" type="s" direction="in"/>
|
||||
<arg name="summary" type="s" direction="in"/>
|
||||
<arg name="body" type="s" direction="in"/>
|
||||
<arg name="actions" type="as" direction="in"/>
|
||||
<arg name="hints" type="a{sv}" direction="in"/>
|
||||
<arg name="expire_timeout" type="i" direction="in"/>
|
||||
<arg name="id" type="u" direction="out"/>
|
||||
</method>
|
||||
<method name="GetCapabilities">
|
||||
<arg name="capabilities" type="as" direction="out"/>
|
||||
</method>
|
||||
<signal name="ActionInvoked">
|
||||
<arg type="u"/>
|
||||
<arg type="s"/>
|
||||
</signal>
|
||||
<signal name="NotificationClosed">
|
||||
<arg type="u"/>
|
||||
<arg type="u"/>
|
||||
</signal>
|
||||
</interface>
|
||||
</node>
|
||||
|
||||
@ -0,0 +1,131 @@
|
||||
/*
|
||||
* Copyright (C) 2021 CutefishOS Team.
|
||||
*
|
||||
* Author: Reion Wong <reion@cutefishos.com>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,90 @@
|
||||
/*
|
||||
SPDX-FileCopyrightText: 2021 Reion Wong <reion@cutefishos.com>
|
||||
SPDX-FileCopyrightText: 2019 Kai Uwe Broulik <kde@privat.broulik.de>
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,8 @@
|
||||
<RCC>
|
||||
<qresource prefix="/">
|
||||
<file>qml/main.qml</file>
|
||||
<file>qml/NotificationPopup.qml</file>
|
||||
<file>images/dark/close.svg</file>
|
||||
<file>images/light/close.svg</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright (C) 2021 CutefishOS Team.
|
||||
*
|
||||
* Author: Reion Wong <reion@cutefishos.com>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "screenhelper.h"
|
||||
#include <QApplication>
|
||||
#include <QScreen>
|
||||
|
||||
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();
|
||||
}
|
||||
@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright (C) 2021 CutefishOS Team.
|
||||
*
|
||||
* Author: Reion Wong <reion@cutefishos.com>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef SCREENHELPER_H
|
||||
#define SCREENHELPER_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QRect>
|
||||
|
||||
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
|
||||
@ -0,0 +1,16 @@
|
||||
#include "view.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QQmlContext>
|
||||
|
||||
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);
|
||||
}
|
||||
@ -0,0 +1,15 @@
|
||||
#ifndef VIEW_H
|
||||
#define VIEW_H
|
||||
|
||||
#include <QQuickView>
|
||||
|
||||
class View : public QQuickView
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit View(QQuickView *parent = nullptr);
|
||||
|
||||
};
|
||||
|
||||
#endif // VIEW_H
|
||||
Loading…
Reference in New Issue