1 /*
2     SPDX-FileCopyrightText: 2019 Kai Uwe Broulik <kde@privat.broulik.de>
3 
4     SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
5 */
6 
7 #include "notifications.h"
8 
9 #include <QDebug>
10 #include <QMetaEnum>
11 #include <QSharedPointer>
12 
13 #include <KConcatenateRowsProxyModel>
14 #include <KDescendantsProxyModel>
15 
16 #include "limitedrowcountproxymodel_p.h"
17 #include "notificationfilterproxymodel_p.h"
18 #include "notificationgroupcollapsingproxymodel_p.h"
19 #include "notificationgroupingproxymodel_p.h"
20 #include "notificationsmodel.h"
21 #include "notificationsortproxymodel_p.h"
22 
23 #include "jobsmodel.h"
24 
25 #include "settings.h"
26 
27 #include "notification.h"
28 
29 #include "utils_p.h"
30 
31 #include "debug.h"
32 
33 using namespace NotificationManager;
34 
35 class Q_DECL_HIDDEN Notifications::Private
36 {
37 public:
38     explicit Private(Notifications *q);
39     ~Private();
40 
41     void initSourceModels();
42     void initProxyModels();
43 
44     void updateCount();
45 
46     bool showNotifications = true;
47     bool showJobs = false;
48 
49     Notifications::GroupMode groupMode = Notifications::GroupDisabled;
50     int groupLimit = 0;
51     bool expandUnread = false;
52 
53     int activeNotificationsCount = 0;
54     int expiredNotificationsCount = 0;
55 
56     int unreadNotificationsCount = 0;
57 
58     int activeJobsCount = 0;
59     int jobsPercentage = 0;
60 
61     static bool isGroup(const QModelIndex &idx);
62     static uint notificationId(const QModelIndex &idx);
63     QModelIndex mapFromModel(const QModelIndex &idx) const;
64 
65     // NOTE when you add or re-arrange models make sure to update mapFromModel()!
66     NotificationsModel::Ptr notificationsModel;
67     JobsModel::Ptr jobsModel;
68     QSharedPointer<Settings> settings() const;
69 
70     KConcatenateRowsProxyModel *notificationsAndJobsModel = nullptr;
71 
72     NotificationFilterProxyModel *filterModel = nullptr;
73     NotificationSortProxyModel *sortModel = nullptr;
74     NotificationGroupingProxyModel *groupingModel = nullptr;
75     NotificationGroupCollapsingProxyModel *groupCollapsingModel = nullptr;
76     KDescendantsProxyModel *flattenModel = nullptr;
77 
78     LimitedRowCountProxyModel *limiterModel = nullptr;
79 
80 private:
81     Notifications *q;
82 };
83 
Private(Notifications * q)84 Notifications::Private::Private(Notifications *q)
85     : q(q)
86 {
87 }
88 
~Private()89 Notifications::Private::~Private()
90 {
91 }
92 
initSourceModels()93 void Notifications::Private::initSourceModels()
94 {
95     Q_ASSERT(notificationsAndJobsModel); // initProxyModels must be called before initSourceModels
96 
97     if (showNotifications && !notificationsModel) {
98         notificationsModel = NotificationsModel::createNotificationsModel();
99         connect(notificationsModel.data(), &NotificationsModel::lastReadChanged, q, [this] {
100             updateCount();
101             emit q->lastReadChanged();
102         });
103         notificationsAndJobsModel->addSourceModel(notificationsModel.data());
104     } else if (!showNotifications && notificationsModel) {
105         notificationsAndJobsModel->removeSourceModel(notificationsModel.data());
106         disconnect(notificationsModel.data(), nullptr, q, nullptr); // disconnect all
107         notificationsModel = nullptr;
108     }
109 
110     if (showJobs && !jobsModel) {
111         jobsModel = JobsModel::createJobsModel();
112         notificationsAndJobsModel->addSourceModel(jobsModel.data());
113         jobsModel->init();
114     } else if (!showJobs && jobsModel) {
115         notificationsAndJobsModel->removeSourceModel(jobsModel.data());
116         jobsModel = nullptr;
117     }
118 }
119 
initProxyModels()120 void Notifications::Private::initProxyModels()
121 {
122     /* The data flow is as follows:
123      * NOTE when you add or re-arrange models make sure to update mapFromModel()!
124      *
125      * NotificationsModel      JobsModel
126      *        \\                 /
127      *         \\               /
128      *     KConcatenateRowsProxyModel
129      *               |||
130      *               |||
131      *     NotificationFilterProxyModel
132      *     (filters by urgency, whitelist, etc)
133      *                |
134      *                |
135      *      NotificationSortProxyModel
136      *      (sorts by urgency, date, etc)
137      *                |
138      * --- BEGIN: Only when grouping is enabled ---
139      *                |
140      *    NotificationGroupingProxyModel
141      *    (turns list into tree grouped by app)
142      *               //\\
143      *               //\\
144      *   NotificationGroupCollapsingProxyModel
145      *   (limits number of tree leaves for expand/collapse feature)
146      *                /\
147      *                /\
148      *       KDescendantsProxyModel
149      *       (flattens tree back into a list for consumption in ListView)
150      *                |
151      * --- END: Only when grouping is enabled ---
152      *                |
153      *    LimitedRowCountProxyModel
154      *    (limits the total number of items in the model)
155      *                |
156      *                |
157      *               \o/ <- Happy user seeing their notifications
158      */
159 
160     if (!notificationsAndJobsModel) {
161         notificationsAndJobsModel = new KConcatenateRowsProxyModel(q);
162     }
163 
164     if (!filterModel) {
165         filterModel = new NotificationFilterProxyModel();
166         connect(filterModel, &NotificationFilterProxyModel::urgenciesChanged, q, &Notifications::urgenciesChanged);
167         connect(filterModel, &NotificationFilterProxyModel::showExpiredChanged, q, &Notifications::showExpiredChanged);
168         connect(filterModel, &NotificationFilterProxyModel::showDismissedChanged, q, &Notifications::showDismissedChanged);
169         connect(filterModel, &NotificationFilterProxyModel::blacklistedDesktopEntriesChanged, q, &Notifications::blacklistedDesktopEntriesChanged);
170         connect(filterModel, &NotificationFilterProxyModel::blacklistedNotifyRcNamesChanged, q, &Notifications::blacklistedNotifyRcNamesChanged);
171 
172         connect(filterModel, &QAbstractItemModel::rowsInserted, q, [this] {
173             updateCount();
174         });
175         connect(filterModel, &QAbstractItemModel::rowsRemoved, q, [this] {
176             updateCount();
177         });
178         connect(filterModel,
179                 &QAbstractItemModel::dataChanged,
180                 q,
181                 [this](const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector<int> &roles) {
182                     Q_UNUSED(topLeft);
183                     Q_UNUSED(bottomRight);
184                     if (roles.isEmpty() || roles.contains(Notifications::UpdatedRole) || roles.contains(Notifications::ExpiredRole)
185                         || roles.contains(Notifications::JobStateRole) || roles.contains(Notifications::PercentageRole)
186                         || roles.contains(Notifications::ReadRole)) {
187                         updateCount();
188                     }
189                 });
190 
191         filterModel->setSourceModel(notificationsAndJobsModel);
192     }
193 
194     if (!sortModel) {
195         sortModel = new NotificationSortProxyModel(q);
196         connect(sortModel, &NotificationSortProxyModel::sortModeChanged, q, &Notifications::sortModeChanged);
197         connect(sortModel, &NotificationSortProxyModel::sortOrderChanged, q, &Notifications::sortOrderChanged);
198     }
199 
200     if (!limiterModel) {
201         limiterModel = new LimitedRowCountProxyModel(q);
202         connect(limiterModel, &LimitedRowCountProxyModel::limitChanged, q, &Notifications::limitChanged);
203     }
204 
205     if (groupMode == GroupApplicationsFlat) {
206         if (!groupingModel) {
207             groupingModel = new NotificationGroupingProxyModel(q);
208             groupingModel->setSourceModel(filterModel);
209         }
210 
211         if (!groupCollapsingModel) {
212             groupCollapsingModel = new NotificationGroupCollapsingProxyModel(q);
213             groupCollapsingModel->setLimit(groupLimit);
214             groupCollapsingModel->setExpandUnread(expandUnread);
215             groupCollapsingModel->setLastRead(q->lastRead());
216             groupCollapsingModel->setSourceModel(groupingModel);
217         }
218 
219         sortModel->setSourceModel(groupCollapsingModel);
220 
221         flattenModel = new KDescendantsProxyModel(q);
222         flattenModel->setSourceModel(sortModel);
223 
224         limiterModel->setSourceModel(flattenModel);
225     } else {
226         sortModel->setSourceModel(filterModel);
227         limiterModel->setSourceModel(sortModel);
228         delete flattenModel;
229         flattenModel = nullptr;
230         delete groupingModel;
231         groupingModel = nullptr;
232     }
233 
234     q->setSourceModel(limiterModel);
235 }
236 
updateCount()237 void Notifications::Private::updateCount()
238 {
239     int active = 0;
240     int expired = 0;
241     int unread = 0;
242 
243     int jobs = 0;
244     int totalPercentage = 0;
245 
246     // We want to get the numbers after main filtering (urgencies, whitelists, etc)
247     // but before any limiting or group limiting, hence asking the filterModel for advice
248     // at which point notifications and jobs also have already been merged
249     for (int i = 0; i < filterModel->rowCount(); ++i) {
250         const QModelIndex idx = filterModel->index(i, 0);
251 
252         if (idx.data(Notifications::ExpiredRole).toBool()) {
253             ++expired;
254         } else {
255             ++active;
256         }
257 
258         const bool read = idx.data(Notifications::ReadRole).toBool();
259         if (!active && !read) {
260             QDateTime date = idx.data(Notifications::UpdatedRole).toDateTime();
261             if (!date.isValid()) {
262                 date = idx.data(Notifications::CreatedRole).toDateTime();
263             }
264 
265             if (notificationsModel && date > notificationsModel->lastRead()) {
266                 ++unread;
267             }
268         }
269 
270         if (idx.data(Notifications::TypeRole).toInt() == Notifications::JobType) {
271             if (idx.data(Notifications::JobStateRole).toInt() != Notifications::JobStateStopped) {
272                 ++jobs;
273 
274                 totalPercentage += idx.data(Notifications::PercentageRole).toInt();
275             }
276         }
277     }
278 
279     if (activeNotificationsCount != active) {
280         activeNotificationsCount = active;
281         emit q->activeNotificationsCountChanged();
282     }
283     if (expiredNotificationsCount != expired) {
284         expiredNotificationsCount = expired;
285         emit q->expiredNotificationsCountChanged();
286     }
287     if (unreadNotificationsCount != unread) {
288         unreadNotificationsCount = unread;
289         emit q->unreadNotificationsCountChanged();
290     }
291     if (activeJobsCount != jobs) {
292         activeJobsCount = jobs;
293         emit q->activeJobsCountChanged();
294     }
295 
296     const int percentage = (jobs > 0 ? totalPercentage / jobs : 0);
297     if (jobsPercentage != percentage) {
298         jobsPercentage = percentage;
299         emit q->jobsPercentageChanged();
300     }
301 
302     // TODO don't emit in dataChanged
303     emit q->countChanged();
304 }
305 
isGroup(const QModelIndex & idx)306 bool Notifications::Private::isGroup(const QModelIndex &idx)
307 {
308     return idx.data(Notifications::IsGroupRole).toBool();
309 }
310 
notificationId(const QModelIndex & idx)311 uint Notifications::Private::notificationId(const QModelIndex &idx)
312 {
313     return idx.data(Notifications::IdRole).toUInt();
314 }
315 
mapFromModel(const QModelIndex & idx) const316 QModelIndex Notifications::Private::mapFromModel(const QModelIndex &idx) const
317 {
318     QModelIndex resolvedIdx = idx;
319 
320     QAbstractItemModel *models[] = {
321         notificationsAndJobsModel,
322         filterModel,
323         sortModel,
324         groupingModel,
325         groupCollapsingModel,
326         flattenModel,
327         limiterModel,
328     };
329 
330     // TODO can we do this with a generic loop like mapFromModel
331     while (resolvedIdx.isValid() && resolvedIdx.model() != q) {
332         const auto *idxModel = resolvedIdx.model();
333 
334         // HACK try to find the model that uses the index' model as source
335         bool found = false;
336         for (QAbstractItemModel *model : models) {
337             if (!model) {
338                 continue;
339             }
340 
341             if (auto *proxyModel = qobject_cast<QAbstractProxyModel *>(model)) {
342                 if (proxyModel->sourceModel() == idxModel) {
343                     resolvedIdx = proxyModel->mapFromSource(resolvedIdx);
344                     found = true;
345                     break;
346                 }
347             } else if (auto *concatenateModel = qobject_cast<KConcatenateRowsProxyModel *>(model)) {
348                 // There's no "sourceModels()" on KConcatenateRowsProxyModel
349                 if (idxModel == notificationsModel.data() || idxModel == jobsModel.data()) {
350                     resolvedIdx = concatenateModel->mapFromSource(resolvedIdx);
351                     found = true;
352                     break;
353                 }
354             }
355         }
356 
357         if (!found) {
358             break;
359         }
360     }
361     return resolvedIdx;
362 }
363 
settings() const364 QSharedPointer<Settings> Notifications::Private::settings() const
365 {
366     static QWeakPointer<Settings> s_instance;
367     if (!s_instance) {
368         QSharedPointer<Settings> ptr(new Settings());
369         s_instance = ptr.toWeakRef();
370         return ptr;
371     }
372     return s_instance.toStrongRef();
373 }
374 
Notifications(QObject * parent)375 Notifications::Notifications(QObject *parent)
376     : QSortFilterProxyModel(parent)
377     , d(new Private(this))
378 {
379     // The proxy models are always the same, just with different
380     // properties set whereas we want to avoid loading a source model
381     // e.g. notifications or jobs when we're not actually using them
382     d->initProxyModels();
383 
384     // init source models when used from C++
385     QMetaObject::invokeMethod(
386         this,
387         [this] {
388             d->initSourceModels();
389         },
390         Qt::QueuedConnection);
391 }
392 
393 Notifications::~Notifications() = default;
394 
classBegin()395 void Notifications::classBegin()
396 {
397 }
398 
componentComplete()399 void Notifications::componentComplete()
400 {
401     // init source models when used from QML
402     d->initSourceModels();
403 }
404 
limit() const405 int Notifications::limit() const
406 {
407     return d->limiterModel->limit();
408 }
409 
setLimit(int limit)410 void Notifications::setLimit(int limit)
411 {
412     d->limiterModel->setLimit(limit);
413 }
414 
groupLimit() const415 int Notifications::groupLimit() const
416 {
417     return d->groupLimit;
418 }
419 
setGroupLimit(int limit)420 void Notifications::setGroupLimit(int limit)
421 {
422     if (d->groupLimit == limit) {
423         return;
424     }
425 
426     d->groupLimit = limit;
427     if (d->groupCollapsingModel) {
428         d->groupCollapsingModel->setLimit(limit);
429     }
430     emit groupLimitChanged();
431 }
432 
expandUnread() const433 bool Notifications::expandUnread() const
434 {
435     return d->expandUnread;
436 }
437 
setExpandUnread(bool expand)438 void Notifications::setExpandUnread(bool expand)
439 {
440     if (d->expandUnread == expand) {
441         return;
442     }
443 
444     d->expandUnread = expand;
445     if (d->groupCollapsingModel) {
446         d->groupCollapsingModel->setExpandUnread(expand);
447     }
448     emit expandUnreadChanged();
449 }
450 
showExpired() const451 bool Notifications::showExpired() const
452 {
453     return d->filterModel->showExpired();
454 }
455 
setShowExpired(bool show)456 void Notifications::setShowExpired(bool show)
457 {
458     d->filterModel->setShowExpired(show);
459 }
460 
showDismissed() const461 bool Notifications::showDismissed() const
462 {
463     return d->filterModel->showDismissed();
464 }
465 
setShowDismissed(bool show)466 void Notifications::setShowDismissed(bool show)
467 {
468     d->filterModel->setShowDismissed(show);
469 }
470 
blacklistedDesktopEntries() const471 QStringList Notifications::blacklistedDesktopEntries() const
472 {
473     return d->filterModel->blacklistedDesktopEntries();
474 }
475 
setBlacklistedDesktopEntries(const QStringList & blacklist)476 void Notifications::setBlacklistedDesktopEntries(const QStringList &blacklist)
477 {
478     d->filterModel->setBlackListedDesktopEntries(blacklist);
479 }
480 
blacklistedNotifyRcNames() const481 QStringList Notifications::blacklistedNotifyRcNames() const
482 {
483     return d->filterModel->blacklistedNotifyRcNames();
484 }
485 
setBlacklistedNotifyRcNames(const QStringList & blacklist)486 void Notifications::setBlacklistedNotifyRcNames(const QStringList &blacklist)
487 {
488     d->filterModel->setBlacklistedNotifyRcNames(blacklist);
489 }
490 
whitelistedDesktopEntries() const491 QStringList Notifications::whitelistedDesktopEntries() const
492 {
493     return d->filterModel->whitelistedDesktopEntries();
494 }
495 
setWhitelistedDesktopEntries(const QStringList & whitelist)496 void Notifications::setWhitelistedDesktopEntries(const QStringList &whitelist)
497 {
498     d->filterModel->setWhiteListedDesktopEntries(whitelist);
499 }
500 
whitelistedNotifyRcNames() const501 QStringList Notifications::whitelistedNotifyRcNames() const
502 {
503     return d->filterModel->whitelistedNotifyRcNames();
504 }
505 
setWhitelistedNotifyRcNames(const QStringList & whitelist)506 void Notifications::setWhitelistedNotifyRcNames(const QStringList &whitelist)
507 {
508     d->filterModel->setWhitelistedNotifyRcNames(whitelist);
509 }
510 
showNotifications() const511 bool Notifications::showNotifications() const
512 {
513     return d->showNotifications;
514 }
515 
setShowNotifications(bool show)516 void Notifications::setShowNotifications(bool show)
517 {
518     if (d->showNotifications == show) {
519         return;
520     }
521 
522     d->showNotifications = show;
523     d->initSourceModels();
524     emit showNotificationsChanged();
525 }
526 
showJobs() const527 bool Notifications::showJobs() const
528 {
529     return d->showJobs;
530 }
531 
setShowJobs(bool show)532 void Notifications::setShowJobs(bool show)
533 {
534     if (d->showJobs == show) {
535         return;
536     }
537 
538     d->showJobs = show;
539     d->initSourceModels();
540     emit showJobsChanged();
541 }
542 
urgencies() const543 Notifications::Urgencies Notifications::urgencies() const
544 {
545     return d->filterModel->urgencies();
546 }
547 
setUrgencies(Urgencies urgencies)548 void Notifications::setUrgencies(Urgencies urgencies)
549 {
550     d->filterModel->setUrgencies(urgencies);
551 }
552 
sortMode() const553 Notifications::SortMode Notifications::sortMode() const
554 {
555     return d->sortModel->sortMode();
556 }
557 
setSortMode(SortMode sortMode)558 void Notifications::setSortMode(SortMode sortMode)
559 {
560     d->sortModel->setSortMode(sortMode);
561 }
562 
sortOrder() const563 Qt::SortOrder Notifications::sortOrder() const
564 {
565     return d->sortModel->sortOrder();
566 }
567 
setSortOrder(Qt::SortOrder sortOrder)568 void Notifications::setSortOrder(Qt::SortOrder sortOrder)
569 {
570     d->sortModel->setSortOrder(sortOrder);
571 }
572 
groupMode() const573 Notifications::GroupMode Notifications::groupMode() const
574 {
575     return d->groupMode;
576 }
577 
setGroupMode(GroupMode groupMode)578 void Notifications::setGroupMode(GroupMode groupMode)
579 {
580     if (d->groupMode != groupMode) {
581         d->groupMode = groupMode;
582         d->initProxyModels();
583         emit groupModeChanged();
584     }
585 }
586 
count() const587 int Notifications::count() const
588 {
589     return rowCount(QModelIndex());
590 }
591 
activeNotificationsCount() const592 int Notifications::activeNotificationsCount() const
593 {
594     return d->activeNotificationsCount;
595 }
596 
expiredNotificationsCount() const597 int Notifications::expiredNotificationsCount() const
598 {
599     return d->expiredNotificationsCount;
600 }
601 
lastRead() const602 QDateTime Notifications::lastRead() const
603 {
604     if (d->notificationsModel) {
605         return d->notificationsModel->lastRead();
606     }
607     return QDateTime();
608 }
609 
setLastRead(const QDateTime & lastRead)610 void Notifications::setLastRead(const QDateTime &lastRead)
611 {
612     // TODO jobs could also be unread?
613     if (d->notificationsModel) {
614         d->notificationsModel->setLastRead(lastRead);
615     }
616     if (d->groupCollapsingModel) {
617         d->groupCollapsingModel->setLastRead(lastRead);
618     }
619 }
620 
resetLastRead()621 void Notifications::resetLastRead()
622 {
623     setLastRead(QDateTime::currentDateTimeUtc());
624 }
625 
unreadNotificationsCount() const626 int Notifications::unreadNotificationsCount() const
627 {
628     return d->unreadNotificationsCount;
629 }
630 
activeJobsCount() const631 int Notifications::activeJobsCount() const
632 {
633     return d->activeJobsCount;
634 }
635 
jobsPercentage() const636 int Notifications::jobsPercentage() const
637 {
638     return d->jobsPercentage;
639 }
640 
makePersistentModelIndex(const QModelIndex & idx) const641 QPersistentModelIndex Notifications::makePersistentModelIndex(const QModelIndex &idx) const
642 {
643     return QPersistentModelIndex(idx);
644 }
645 
expire(const QModelIndex & idx)646 void Notifications::expire(const QModelIndex &idx)
647 {
648     switch (static_cast<Notifications::Type>(idx.data(Notifications::TypeRole).toInt())) {
649     case Notifications::NotificationType:
650         d->notificationsModel->expire(Private::notificationId(idx));
651         break;
652     case Notifications::JobType:
653         d->jobsModel->expire(Utils::mapToModel(idx, d->jobsModel.data()));
654         break;
655     default:
656         Q_UNREACHABLE();
657     }
658 }
659 
close(const QModelIndex & idx)660 void Notifications::close(const QModelIndex &idx)
661 {
662     if (idx.data(Notifications::IsGroupRole).toBool()) {
663         const QModelIndex groupIdx = Utils::mapToModel(idx, d->groupingModel);
664         if (!groupIdx.isValid()) {
665             qCWarning(NOTIFICATIONMANAGER) << "Failed to find group model index for this item";
666             return;
667         }
668 
669         Q_ASSERT(groupIdx.model() == d->groupingModel);
670 
671         const int childCount = d->groupingModel->rowCount(groupIdx);
672         for (int i = childCount - 1; i >= 0; --i) {
673             const QModelIndex childIdx = d->groupingModel->index(i, 0, groupIdx);
674             close(childIdx);
675         }
676         return;
677     }
678 
679     if (!idx.data(Notifications::ClosableRole).toBool()) {
680         return;
681     }
682 
683     switch (static_cast<Notifications::Type>(idx.data(Notifications::TypeRole).toInt())) {
684     case Notifications::NotificationType:
685         d->notificationsModel->close(Private::notificationId(idx));
686         break;
687     case Notifications::JobType:
688         d->jobsModel->close(Utils::mapToModel(idx, d->jobsModel.data()));
689         break;
690     default:
691         Q_UNREACHABLE();
692     }
693 }
694 
configure(const QModelIndex & idx)695 void Notifications::configure(const QModelIndex &idx)
696 {
697     if (!d->notificationsModel) {
698         return;
699     }
700 
701     // For groups just configure the application, not the individual event
702     if (Private::isGroup(idx)) {
703         const QString desktopEntry = idx.data(Notifications::DesktopEntryRole).toString();
704         const QString notifyRcName = idx.data(Notifications::NotifyRcNameRole).toString();
705 
706         d->notificationsModel->configure(desktopEntry, notifyRcName, QString() /*eventId*/);
707         return;
708     }
709 
710     d->notificationsModel->configure(Private::notificationId(idx));
711 }
712 
invokeDefaultAction(const QModelIndex & idx)713 void Notifications::invokeDefaultAction(const QModelIndex &idx)
714 {
715     if (d->notificationsModel) {
716         d->notificationsModel->invokeDefaultAction(Private::notificationId(idx));
717     }
718 }
719 
invokeAction(const QModelIndex & idx,const QString & actionId)720 void Notifications::invokeAction(const QModelIndex &idx, const QString &actionId)
721 {
722     if (d->notificationsModel) {
723         d->notificationsModel->invokeAction(Private::notificationId(idx), actionId);
724     }
725 }
726 
reply(const QModelIndex & idx,const QString & text)727 void Notifications::reply(const QModelIndex &idx, const QString &text)
728 {
729     if (d->notificationsModel) {
730         d->notificationsModel->reply(Private::notificationId(idx), text);
731     }
732 }
733 
startTimeout(const QModelIndex & idx)734 void Notifications::startTimeout(const QModelIndex &idx)
735 {
736     startTimeout(Private::notificationId(idx));
737 }
738 
startTimeout(uint notificationId)739 void Notifications::startTimeout(uint notificationId)
740 {
741     if (d->notificationsModel) {
742         d->notificationsModel->startTimeout(notificationId);
743     }
744 }
745 
stopTimeout(const QModelIndex & idx)746 void Notifications::stopTimeout(const QModelIndex &idx)
747 {
748     if (d->notificationsModel) {
749         d->notificationsModel->stopTimeout(Private::notificationId(idx));
750     }
751 }
752 
suspendJob(const QModelIndex & idx)753 void Notifications::suspendJob(const QModelIndex &idx)
754 {
755     if (d->jobsModel) {
756         d->jobsModel->suspend(Utils::mapToModel(idx, d->jobsModel.data()));
757     }
758 }
759 
resumeJob(const QModelIndex & idx)760 void Notifications::resumeJob(const QModelIndex &idx)
761 {
762     if (d->jobsModel) {
763         d->jobsModel->resume(Utils::mapToModel(idx, d->jobsModel.data()));
764     }
765 }
766 
killJob(const QModelIndex & idx)767 void Notifications::killJob(const QModelIndex &idx)
768 {
769     if (d->jobsModel) {
770         d->jobsModel->kill(Utils::mapToModel(idx, d->jobsModel.data()));
771     }
772 }
773 
clear(ClearFlags flags)774 void Notifications::clear(ClearFlags flags)
775 {
776     if (d->notificationsModel) {
777         d->notificationsModel->clear(flags);
778     }
779     if (d->jobsModel) {
780         d->jobsModel->clear(flags);
781     }
782 }
783 
groupIndex(const QModelIndex & idx) const784 QModelIndex Notifications::groupIndex(const QModelIndex &idx) const
785 {
786     if (idx.data(Notifications::IsGroupRole).toBool()) {
787         return idx;
788     }
789 
790     if (idx.data(Notifications::IsInGroupRole).toBool()) {
791         QModelIndex groupingIdx = Utils::mapToModel(idx, d->groupingModel);
792         return d->mapFromModel(groupingIdx.parent());
793     }
794 
795     qCWarning(NOTIFICATIONMANAGER) << "Cannot get group index for item that isn't a group or inside one";
796     return QModelIndex();
797 }
798 
collapseAllGroups()799 void Notifications::collapseAllGroups()
800 {
801     if (d->groupCollapsingModel) {
802         d->groupCollapsingModel->collapseAll();
803     }
804 }
805 
data(const QModelIndex & index,int role) const806 QVariant Notifications::data(const QModelIndex &index, int role) const
807 {
808     return QSortFilterProxyModel::data(index, role);
809 }
810 
setData(const QModelIndex & index,const QVariant & value,int role)811 bool Notifications::setData(const QModelIndex &index, const QVariant &value, int role)
812 {
813     return QSortFilterProxyModel::setData(index, value, role);
814 }
815 
filterAcceptsRow(int source_row,const QModelIndex & source_parent) const816 bool Notifications::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const
817 {
818     return QSortFilterProxyModel::filterAcceptsRow(source_row, source_parent);
819 }
820 
lessThan(const QModelIndex & source_left,const QModelIndex & source_right) const821 bool Notifications::lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const
822 {
823     return QSortFilterProxyModel::lessThan(source_left, source_right);
824 }
825 
rowCount(const QModelIndex & parent) const826 int Notifications::rowCount(const QModelIndex &parent) const
827 {
828     return QSortFilterProxyModel::rowCount(parent);
829 }
830 
roleNames() const831 QHash<int, QByteArray> Notifications::roleNames() const
832 {
833     return Utils::roleNames();
834 }
835