File indexing completed on 2024-04-28 16:54:33
0001 /* 0002 SPDX-FileCopyrightText: 2018-2019 Kai Uwe Broulik <kde@privat.broulik.de> 0003 0004 SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL 0005 */ 0006 0007 #include "abstractnotificationsmodel.h" 0008 #include "abstractnotificationsmodel_p.h" 0009 #include "debug.h" 0010 0011 #include "utils_p.h" 0012 0013 #include "notification_p.h" 0014 0015 #include <QDebug> 0016 #include <QProcess> 0017 #include <QTextDocumentFragment> 0018 0019 #include <KLocalizedString> 0020 #include <KShell> 0021 0022 #include <algorithm> 0023 #include <chrono> 0024 #include <functional> 0025 0026 using namespace std::chrono_literals; 0027 0028 static const int s_notificationsLimit = 1000; 0029 0030 using namespace NotificationManager; 0031 0032 AbstractNotificationsModel::Private::Private(AbstractNotificationsModel *q) 0033 : q(q) 0034 , lastRead(QDateTime::currentDateTimeUtc()) 0035 { 0036 pendingRemovalTimer.setSingleShot(true); 0037 pendingRemovalTimer.setInterval(50ms); 0038 connect(&pendingRemovalTimer, &QTimer::timeout, q, [this, q] { 0039 QVector<int> rowsToBeRemoved; 0040 rowsToBeRemoved.reserve(pendingRemovals.count()); 0041 for (uint id : qAsConst(pendingRemovals)) { 0042 int row = q->rowOfNotification(id); // oh the complexity... 0043 if (row == -1) { 0044 continue; 0045 } 0046 rowsToBeRemoved.append(row); 0047 } 0048 0049 removeRows(rowsToBeRemoved); 0050 }); 0051 } 0052 0053 AbstractNotificationsModel::Private::~Private() 0054 { 0055 qDeleteAll(notificationTimeouts); 0056 notificationTimeouts.clear(); 0057 } 0058 0059 void AbstractNotificationsModel::Private::onNotificationAdded(const Notification ¬ification) 0060 { 0061 // Once we reach a certain insane number of notifications discard some old ones 0062 // as we keep pixmaps around etc 0063 if (notifications.count() >= s_notificationsLimit) { 0064 const int cleanupCount = s_notificationsLimit / 2; 0065 qCDebug(NOTIFICATIONMANAGER) << "Reached the notification limit of" << s_notificationsLimit << ", discarding the oldest" << cleanupCount 0066 << "notifications"; 0067 q->beginRemoveRows(QModelIndex(), 0, cleanupCount - 1); 0068 for (int i = 0; i < cleanupCount; ++i) { 0069 notifications.removeAt(0); 0070 // TODO close gracefully? 0071 } 0072 q->endRemoveRows(); 0073 } 0074 0075 setupNotificationTimeout(notification); 0076 0077 q->beginInsertRows(QModelIndex(), notifications.count(), notifications.count()); 0078 notifications.append(std::move(notification)); 0079 q->endInsertRows(); 0080 } 0081 0082 void AbstractNotificationsModel::Private::onNotificationReplaced(uint replacedId, const Notification ¬ification) 0083 { 0084 const int row = q->rowOfNotification(replacedId); 0085 0086 if (row == -1) { 0087 qCWarning(NOTIFICATIONMANAGER) << "Trying to replace notification with id" << replacedId 0088 << "which doesn't exist, creating a new one. This is an application bug!"; 0089 onNotificationAdded(notification); 0090 return; 0091 } 0092 0093 setupNotificationTimeout(notification); 0094 0095 Notification newNotification(notification); 0096 0097 const Notification &oldNotification = notifications.at(row); 0098 // As per spec a notification must be replaced atomically with no visual cues. 0099 // Transfer over properties that might cause this, such as unread showing the bell again, 0100 // or created() which should indicate the original date, whereas updated() is when it was last updated 0101 newNotification.setCreated(oldNotification.created()); 0102 newNotification.setExpired(oldNotification.expired()); 0103 newNotification.setDismissed(oldNotification.dismissed()); 0104 newNotification.setRead(oldNotification.read()); 0105 0106 notifications[row] = newNotification; 0107 const QModelIndex idx = q->index(row, 0); 0108 Q_EMIT q->dataChanged(idx, idx); 0109 } 0110 0111 void AbstractNotificationsModel::Private::onNotificationRemoved(uint removedId, Server::CloseReason reason) 0112 { 0113 const int row = q->rowOfNotification(removedId); 0114 if (row == -1) { 0115 return; 0116 } 0117 0118 q->stopTimeout(removedId); 0119 0120 // When a notification expired, keep it around in the history and mark it as such 0121 if (reason == Server::CloseReason::Expired) { 0122 const QModelIndex idx = q->index(row, 0); 0123 0124 Notification ¬ification = notifications[row]; 0125 notification.setExpired(true); 0126 0127 // Since the notification is "closed" it cannot have any actions 0128 // unless it is "resident" which we don't support 0129 notification.setActions(QStringList()); 0130 0131 // clang-format off 0132 Q_EMIT q->dataChanged(idx, idx, { 0133 Notifications::ExpiredRole, 0134 // TODO only Q_EMIT those if actually changed? 0135 Notifications::ActionNamesRole, 0136 Notifications::ActionLabelsRole, 0137 Notifications::HasDefaultActionRole, 0138 Notifications::DefaultActionLabelRole, 0139 Notifications::ConfigurableRole 0140 }); 0141 // clang-format on 0142 0143 return; 0144 } 0145 0146 // Otherwise if explicitly closed by either user or app, mark it for removal 0147 // some apps are notorious for closing a bunch of notifications at once 0148 // causing newer notifications to move up and have a dialogs created for them 0149 // just to then be discarded causing excess CPU usage 0150 if (!pendingRemovals.contains(removedId)) { 0151 pendingRemovals.append(removedId); 0152 } 0153 0154 if (!pendingRemovalTimer.isActive()) { 0155 pendingRemovalTimer.start(); 0156 } 0157 } 0158 0159 void AbstractNotificationsModel::Private::setupNotificationTimeout(const Notification ¬ification) 0160 { 0161 if (notification.timeout() == 0) { 0162 // In case it got replaced by a persistent notification 0163 q->stopTimeout(notification.id()); 0164 return; 0165 } 0166 0167 QTimer *timer = notificationTimeouts.value(notification.id()); 0168 if (!timer) { 0169 timer = new QTimer(); 0170 timer->setSingleShot(true); 0171 0172 connect(timer, &QTimer::timeout, q, [this, timer] { 0173 const uint id = timer->property("notificationId").toUInt(); 0174 q->expire(id); 0175 }); 0176 notificationTimeouts.insert(notification.id(), timer); 0177 } 0178 0179 timer->stop(); 0180 timer->setProperty("notificationId", notification.id()); 0181 timer->setInterval(60000 /*1min*/ + (notification.timeout() == -1 ? 120000 /*2min, max configurable default timeout*/ : notification.timeout())); 0182 timer->start(); 0183 } 0184 0185 void AbstractNotificationsModel::Private::removeRows(const QVector<int> &rows) 0186 { 0187 if (rows.isEmpty()) { 0188 return; 0189 } 0190 0191 QVector<int> rowsToBeRemoved(rows); 0192 std::sort(rowsToBeRemoved.begin(), rowsToBeRemoved.end()); 0193 0194 QVector<QPair<int, int>> clearQueue; 0195 0196 QPair<int, int> clearRange{rowsToBeRemoved.first(), rowsToBeRemoved.first()}; 0197 0198 for (int row : rowsToBeRemoved) { 0199 if (row > clearRange.second + 1) { 0200 clearQueue.append(clearRange); 0201 clearRange.first = row; 0202 } 0203 0204 clearRange.second = row; 0205 } 0206 0207 if (clearQueue.isEmpty() || clearQueue.last() != clearRange) { 0208 clearQueue.append(clearRange); 0209 } 0210 0211 int rowsRemoved = 0; 0212 0213 for (int i = clearQueue.count() - 1; i >= 0; --i) { 0214 const auto &range = clearQueue.at(i); 0215 0216 q->beginRemoveRows(QModelIndex(), range.first, range.second); 0217 for (int j = range.second; j >= range.first; --j) { 0218 notifications.removeAt(j); 0219 ++rowsRemoved; 0220 } 0221 q->endRemoveRows(); 0222 } 0223 0224 Q_ASSERT(rowsRemoved == rowsToBeRemoved.count()); 0225 0226 pendingRemovals.clear(); 0227 } 0228 0229 int AbstractNotificationsModel::rowOfNotification(uint id) const 0230 { 0231 auto it = std::find_if(d->notifications.constBegin(), d->notifications.constEnd(), [id](const Notification &item) { 0232 return item.id() == id; 0233 }); 0234 0235 if (it == d->notifications.constEnd()) { 0236 return -1; 0237 } 0238 0239 return std::distance(d->notifications.constBegin(), it); 0240 } 0241 0242 AbstractNotificationsModel::AbstractNotificationsModel() 0243 : QAbstractListModel(nullptr) 0244 , d(new Private(this)) 0245 { 0246 } 0247 0248 AbstractNotificationsModel::~AbstractNotificationsModel() = default; 0249 0250 QDateTime AbstractNotificationsModel::lastRead() const 0251 { 0252 return d->lastRead; 0253 } 0254 0255 void AbstractNotificationsModel::setLastRead(const QDateTime &lastRead) 0256 { 0257 if (d->lastRead != lastRead) { 0258 d->lastRead = lastRead; 0259 Q_EMIT lastReadChanged(); 0260 } 0261 } 0262 0263 QWindow *AbstractNotificationsModel::window() const 0264 { 0265 return d->window; 0266 } 0267 0268 void AbstractNotificationsModel::setWindow(QWindow *window) 0269 { 0270 if (d->window == window) { 0271 return; 0272 } 0273 if (d->window) { 0274 disconnect(d->window, &QObject::destroyed, this, nullptr); 0275 } 0276 d->window = window; 0277 if (d->window) { 0278 connect(d->window, &QObject::destroyed, this, [this] { 0279 setWindow(nullptr); 0280 }); 0281 } 0282 Q_EMIT windowChanged(window); 0283 } 0284 0285 QVariant AbstractNotificationsModel::data(const QModelIndex &index, int role) const 0286 { 0287 if (!checkIndex(index, QAbstractItemModel::CheckIndexOption::IndexIsValid)) { 0288 return QVariant(); 0289 } 0290 0291 const Notification ¬ification = d->notifications.at(index.row()); 0292 0293 switch (role) { 0294 case Notifications::IdRole: 0295 return notification.id(); 0296 case Notifications::TypeRole: 0297 return Notifications::NotificationType; 0298 0299 case Notifications::CreatedRole: 0300 if (notification.created().isValid()) { 0301 return notification.created(); 0302 } 0303 break; 0304 case Notifications::UpdatedRole: 0305 if (notification.updated().isValid()) { 0306 return notification.updated(); 0307 } 0308 break; 0309 case Notifications::SummaryRole: 0310 return notification.summary(); 0311 case Notifications::BodyRole: 0312 return notification.body(); 0313 case Qt::AccessibleDescriptionRole: 0314 return i18nc("@info %1 notification body %2 application name", 0315 "%1 from %2", 0316 QTextDocumentFragment::fromHtml(notification.body()).toPlainText(), 0317 notification.applicationName()); 0318 case Notifications::IconNameRole: 0319 if (notification.image().isNull()) { 0320 return notification.icon(); 0321 } 0322 break; 0323 case Notifications::ImageRole: 0324 if (!notification.image().isNull()) { 0325 return notification.image(); 0326 } 0327 break; 0328 case Notifications::DesktopEntryRole: 0329 return notification.desktopEntry(); 0330 case Notifications::NotifyRcNameRole: 0331 return notification.notifyRcName(); 0332 0333 case Notifications::ApplicationNameRole: 0334 return notification.applicationName(); 0335 case Notifications::ApplicationIconNameRole: 0336 return notification.applicationIconName(); 0337 case Notifications::OriginNameRole: 0338 return notification.originName(); 0339 0340 case Notifications::ActionNamesRole: 0341 return notification.actionNames(); 0342 case Notifications::ActionLabelsRole: 0343 return notification.actionLabels(); 0344 case Notifications::HasDefaultActionRole: 0345 return notification.hasDefaultAction(); 0346 case Notifications::DefaultActionLabelRole: 0347 return notification.defaultActionLabel(); 0348 0349 case Notifications::UrlsRole: 0350 return QVariant::fromValue(notification.urls()); 0351 0352 case Notifications::UrgencyRole: 0353 return static_cast<int>(notification.urgency()); 0354 case Notifications::UserActionFeedbackRole: 0355 return notification.userActionFeedback(); 0356 0357 case Notifications::TimeoutRole: 0358 return notification.timeout(); 0359 0360 case Notifications::ClosableRole: 0361 return true; 0362 case Notifications::ConfigurableRole: 0363 return notification.configurable(); 0364 case Notifications::ConfigureActionLabelRole: 0365 return notification.configureActionLabel(); 0366 0367 case Notifications::CategoryRole: 0368 return notification.category(); 0369 0370 case Notifications::ExpiredRole: 0371 return notification.expired(); 0372 case Notifications::ReadRole: 0373 return notification.read(); 0374 case Notifications::ResidentRole: 0375 return notification.resident(); 0376 case Notifications::TransientRole: 0377 return notification.transient(); 0378 0379 case Notifications::HasReplyActionRole: 0380 return notification.hasReplyAction(); 0381 case Notifications::ReplyActionLabelRole: 0382 return notification.replyActionLabel(); 0383 case Notifications::ReplyPlaceholderTextRole: 0384 return notification.replyPlaceholderText(); 0385 case Notifications::ReplySubmitButtonTextRole: 0386 return notification.replySubmitButtonText(); 0387 case Notifications::ReplySubmitButtonIconNameRole: 0388 return notification.replySubmitButtonIconName(); 0389 } 0390 0391 return QVariant(); 0392 } 0393 0394 bool AbstractNotificationsModel::setData(const QModelIndex &index, const QVariant &value, int role) 0395 { 0396 if (!checkIndex(index, QAbstractItemModel::CheckIndexOption::IndexIsValid)) { 0397 return false; 0398 } 0399 0400 Notification ¬ification = d->notifications[index.row()]; 0401 bool dirty = false; 0402 0403 switch (role) { 0404 case Notifications::ReadRole: 0405 if (value.toBool() != notification.read()) { 0406 notification.setRead(value.toBool()); 0407 dirty = true; 0408 } 0409 break; 0410 // Allows to mark a notification as expired without actually sending that out through expire() for persistency 0411 case Notifications::ExpiredRole: 0412 if (value.toBool() != notification.expired()) { 0413 notification.setExpired(value.toBool()); 0414 dirty = true; 0415 } 0416 break; 0417 } 0418 0419 if (dirty) { 0420 Q_EMIT dataChanged(index, index, {role}); 0421 } 0422 0423 return dirty; 0424 } 0425 0426 int AbstractNotificationsModel::rowCount(const QModelIndex &parent) const 0427 { 0428 if (parent.isValid()) { 0429 return 0; 0430 } 0431 0432 return d->notifications.count(); 0433 } 0434 0435 QHash<int, QByteArray> AbstractNotificationsModel::roleNames() const 0436 { 0437 return Utils::roleNames(); 0438 } 0439 0440 void AbstractNotificationsModel::startTimeout(uint notificationId) 0441 { 0442 const int row = rowOfNotification(notificationId); 0443 if (row == -1) { 0444 return; 0445 } 0446 0447 const Notification ¬ification = d->notifications.at(row); 0448 0449 if (!notification.timeout() || notification.expired()) { 0450 return; 0451 } 0452 0453 d->setupNotificationTimeout(notification); 0454 } 0455 0456 void AbstractNotificationsModel::stopTimeout(uint notificationId) 0457 { 0458 delete d->notificationTimeouts.take(notificationId); 0459 } 0460 0461 void AbstractNotificationsModel::clear(Notifications::ClearFlags flags) 0462 { 0463 if (d->notifications.isEmpty()) { 0464 return; 0465 } 0466 0467 QVector<int> rowsToRemove; 0468 0469 for (int i = 0; i < d->notifications.count(); ++i) { 0470 const Notification ¬ification = d->notifications.at(i); 0471 0472 if (flags.testFlag(Notifications::ClearExpired) && notification.expired()) { 0473 close(notification.id()); 0474 } 0475 } 0476 } 0477 0478 void AbstractNotificationsModel::onNotificationAdded(const Notification ¬ification) 0479 { 0480 d->onNotificationAdded(notification); 0481 } 0482 0483 void AbstractNotificationsModel::onNotificationReplaced(uint replacedId, const Notification ¬ification) 0484 { 0485 d->onNotificationReplaced(replacedId, notification); 0486 } 0487 0488 void AbstractNotificationsModel::onNotificationRemoved(uint notificationId, Server::CloseReason reason) 0489 { 0490 d->onNotificationRemoved(notificationId, reason); 0491 } 0492 0493 void AbstractNotificationsModel::setupNotificationTimeout(const Notification ¬ification) 0494 { 0495 d->setupNotificationTimeout(notification); 0496 } 0497 0498 const QVector<Notification> &AbstractNotificationsModel::notifications() 0499 { 0500 return d->notifications; 0501 }