You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
core/notificationd/notificationsmodel.cpp

269 lines
7.8 KiB
C++

/*
* 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 "historymodel.h"
#include "notification.h"
#include "settings.h"
#include <QMetaEnum>
#include <QDebug>
static const int s_notificationsLimit = 1000;
static NotificationsModel *NOTIFICATIONS_MODEL = nullptr;
NotificationsModel *NotificationsModel::self()
{
if (!NOTIFICATIONS_MODEL)
NOTIFICATIONS_MODEL = new NotificationsModel;
return NOTIFICATIONS_MODEL;
}
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 &notification = 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;
case NotificationsModel::HasDefaultActionRole:
return notification.actions.contains("default");
default:
break;
}
return QVariant();
}
bool NotificationsModel::setData(const QModelIndex &index, const QVariant &value, int role)
{
// Notification &notification = 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::expired(uint id)
{
int row = rowOfNotification(id);
if (row > -1) {
onNotificationRemoved(id, NotificationServer::CloseReason::Expired);
}
}
void NotificationsModel::close(uint id)
{
if (rowOfNotification(id) > -1) {
NotificationServer::self()->closeNotification(id, NotificationServer::CloseReason::DismissedByUser);
}
}
void NotificationsModel::invokeDefaultAction(uint notificationId)
{
const int row = rowOfNotification(notificationId);
if (row == -1) {
return;
}
const Notification &notification = m_notifications.at(row);
if (!notification.actions.contains("default")) {
return;
}
NotificationServer::self()->InvokeAction(notificationId, "default");
}
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 &notification)
{
// Do Not Disturb Mode:
// Add directly to the historical model.
if (Settings::self()->doNotDisturb()) {
HistoryModel::self()->add(notification);
return;
}
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 &notification)
{
}
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 &notification = m_notifications[row];
// notification.setExpired(true);
notification.actions.clear();
emit dataChanged(idx, idx);
HistoryModel::self()->add(notification);
// 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();
}
}