Plasma-workspace

notifications.cpp
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 <QConcatenateTablesProxyModel>
10#include <QDebug>
11#include <QFile>
12#include <QMetaEnum>
13#include <canberra.h>
14#include <memory>
15
16#include <KConfig>
17#include <KConfigGroup>
18#include <KConfigWatcher>
19#include <KDescendantsProxyModel>
20#include <KLocalizedString>
21#include <KNotification>
22
23#include "limitedrowcountproxymodel_p.h"
24#include "notificationfilterproxymodel_p.h"
25#include "notificationgroupcollapsingproxymodel_p.h"
26#include "notificationgroupingproxymodel_p.h"
27#include "notificationsmodel.h"
28#include "notificationsortproxymodel_p.h"
29
30#include "jobsmodel.h"
31
32#include "settings.h"
33
34#include "notification.h"
35
36#include "utils_p.h"
37
38#include "debug.h"
39
40using namespace Qt::StringLiterals;
41using namespace NotificationManager;
42
43// This class is a workaround to https://bugreports.qt.io/browse/QTBUG-134210
44// and https://bugs.kde.org/show_bug.cgi?id=500749
45// if a model is added to an empty QConcatenateTablesProxyModel
46// it will report zero columns and non-zero rows, causing a data inconsistence
47// which causes an infinite recursion in NotificationFilterProxyModel::filterAcceptsRow
48// We fix the number of columns to always be 1
49// remove when the upstream bug is fixed
50class SingleColumnConcatenateTables : public QConcatenateTablesProxyModel
51{
52public:
54
55 int columnCount(const QModelIndex &parent = QModelIndex()) const override
56 {
57 Q_UNUSED(parent)
58 return 1;
59 }
60};
61
62class Q_DECL_HIDDEN Notifications::Private
63{
64public:
65 explicit Private(Notifications *q);
66 ~Private();
67
68 void initSourceModels();
69 void initProxyModels();
70
71 void updateCount();
72
73 bool showNotifications = true;
74 bool showJobs = false;
75
76 Notifications::GroupMode groupMode = Notifications::GroupDisabled;
77 int groupLimit = 0;
78 bool expandUnread = false;
79
80 int activeNotificationsCount = 0;
81 int expiredNotificationsCount = 0;
82
83 int unreadNotificationsCount = 0;
84
85 int activeJobsCount = 0;
86 int jobsPercentage = 0;
87
88 static bool isGroup(const QModelIndex &idx);
89 static uint notificationId(const QModelIndex &idx);
90 QModelIndex mapFromModel(const QModelIndex &idx) const;
91
92 // NOTE when you add or re-arrange models make sure to update mapFromModel()!
93 NotificationsModel::Ptr notificationsModel;
94 JobsModel::Ptr jobsModel;
95 std::shared_ptr<Settings> settings() const;
96
97 QConcatenateTablesProxyModel *notificationsAndJobsModel = nullptr;
98
99 NotificationFilterProxyModel *filterModel = nullptr;
100 NotificationSortProxyModel *sortModel = nullptr;
101 NotificationGroupingProxyModel *groupingModel = nullptr;
102 NotificationGroupCollapsingProxyModel *groupCollapsingModel = nullptr;
103 KDescendantsProxyModel *flattenModel = nullptr;
104 ca_context *canberraContext = nullptr;
105 LimitedRowCountProxyModel *limiterModel = nullptr;
106
107private:
108 Notifications *const q;
109};
110
111Notifications::Private::Private(Notifications *q)
112 : q(q)
113{
114}
115
116Notifications::Private::~Private()
117{
118}
119
120void Notifications::Private::initSourceModels()
121{
122 Q_ASSERT(notificationsAndJobsModel); // initProxyModels must be called before initSourceModels
123
124 if (showNotifications && !notificationsModel) {
125 notificationsModel = NotificationsModel::createNotificationsModel();
126 notificationsAndJobsModel->addSourceModel(notificationsModel.get());
127 connect(notificationsModel.get(), &NotificationsModel::lastReadChanged, q, [this] {
128 updateCount();
129 Q_EMIT q->lastReadChanged();
130 });
131 } else if (!showNotifications && notificationsModel) {
132 notificationsAndJobsModel->removeSourceModel(notificationsModel.get());
133 disconnect(notificationsModel.get(), nullptr, q, nullptr); // disconnect all
134 notificationsModel = nullptr;
135 }
136
137 if (showJobs && !jobsModel) {
138 jobsModel = JobsModel::createJobsModel();
139 notificationsAndJobsModel->addSourceModel(jobsModel.get());
140 jobsModel->init();
141 } else if (!showJobs && jobsModel) {
142 notificationsAndJobsModel->removeSourceModel(jobsModel.get());
143 jobsModel = nullptr;
144 }
145}
146
147void Notifications::Private::initProxyModels()
148{
149 /* The data flow is as follows:
150 * NOTE when you add or re-arrange models make sure to update mapFromModel()!
151 *
152 * NotificationsModel JobsModel
153 * \\ /
154 * \\ /
155 * QConcatenateTablesProxyModel
156 * |||
157 * |||
158 * NotificationFilterProxyModel
159 * (filters by urgency, whitelist, etc)
160 * |
161 * |
162 * NotificationSortProxyModel
163 * (sorts by urgency, date, etc)
164 * |
165 * --- BEGIN: Only when grouping is enabled ---
166 * |
167 * NotificationGroupingProxyModel
168 * (turns list into tree grouped by app)
169 * //\\
170 * //\\
171 * NotificationGroupCollapsingProxyModel
172 * (limits number of tree leaves for expand/collapse feature)
173 * /\
174 * /\
175 * KDescendantsProxyModel
176 * (flattens tree back into a list for consumption in ListView)
177 * |
178 * --- END: Only when grouping is enabled ---
179 * |
180 * LimitedRowCountProxyModel
181 * (limits the total number of items in the model)
182 * |
183 * |
184 * \o/ <- Happy user seeing their notifications
185 */
186
187 if (!notificationsAndJobsModel) {
188 notificationsAndJobsModel = new SingleColumnConcatenateTables(q);
189 }
190
191 if (!filterModel) {
192 filterModel = new NotificationFilterProxyModel();
193 connect(filterModel, &NotificationFilterProxyModel::urgenciesChanged, q, &Notifications::urgenciesChanged);
194 connect(filterModel, &NotificationFilterProxyModel::showExpiredChanged, q, &Notifications::showExpiredChanged);
195 connect(filterModel, &NotificationFilterProxyModel::showDismissedChanged, q, &Notifications::showDismissedChanged);
196 connect(filterModel, &NotificationFilterProxyModel::showAddedDuringInhibitionChanged, q, &Notifications::showAddedDuringInhibitionChanged);
197 connect(filterModel, &NotificationFilterProxyModel::blacklistedDesktopEntriesChanged, q, &Notifications::blacklistedDesktopEntriesChanged);
198 connect(filterModel, &NotificationFilterProxyModel::blacklistedNotifyRcNamesChanged, q, &Notifications::blacklistedNotifyRcNamesChanged);
199
200 filterModel->setSourceModel(notificationsAndJobsModel);
201
202 connect(filterModel, &QAbstractItemModel::rowsInserted, q, [this] {
203 updateCount();
204 });
205 connect(filterModel, &QAbstractItemModel::rowsRemoved, q, [this] {
206 updateCount();
207 });
208 connect(filterModel, &QAbstractItemModel::dataChanged, q, [this](const QModelIndex &topLeft, const QModelIndex &bottomRight, const QList<int> &roles) {
209 Q_UNUSED(topLeft);
210 Q_UNUSED(bottomRight);
213 updateCount();
214 }
215 });
216 }
217
218 if (!sortModel) {
219 sortModel = new NotificationSortProxyModel(q);
220 connect(sortModel, &NotificationSortProxyModel::sortModeChanged, q, &Notifications::sortModeChanged);
221 connect(sortModel, &NotificationSortProxyModel::sortOrderChanged, q, &Notifications::sortOrderChanged);
222 }
223
224 if (!limiterModel) {
225 limiterModel = new LimitedRowCountProxyModel(q);
226 connect(limiterModel, &LimitedRowCountProxyModel::limitChanged, q, &Notifications::limitChanged);
227 }
228
229 if (groupMode == GroupApplicationsFlat) {
230 if (!groupingModel) {
231 groupingModel = new NotificationGroupingProxyModel(q);
232 groupingModel->setSourceModel(filterModel);
233 }
234
235 if (!groupCollapsingModel) {
236 groupCollapsingModel = new NotificationGroupCollapsingProxyModel(q);
237 groupCollapsingModel->setLimit(groupLimit);
238 groupCollapsingModel->setExpandUnread(expandUnread);
239 groupCollapsingModel->setLastRead(q->lastRead());
240 groupCollapsingModel->setSourceModel(groupingModel);
241 }
242
243 sortModel->setSourceModel(groupCollapsingModel);
244
245 flattenModel = new KDescendantsProxyModel(q);
246 flattenModel->setSourceModel(sortModel);
247
248 limiterModel->setSourceModel(flattenModel);
249 } else {
250 sortModel->setSourceModel(filterModel);
251 limiterModel->setSourceModel(sortModel);
252 delete flattenModel;
253 flattenModel = nullptr;
254 delete groupingModel;
255 groupingModel = nullptr;
256 }
257
258 q->setSourceModel(limiterModel);
259}
260
261void Notifications::Private::updateCount()
262{
263 int active = 0;
264 int expired = 0;
265 int unread = 0;
266
267 int jobs = 0;
268 int totalPercentage = 0;
269
270 // We want to get the numbers after main filtering (urgencies, whitelists, etc)
271 // but before any limiting or group limiting, hence asking the filterModel for advice
272 // at which point notifications and jobs also have already been merged
273 for (int i = 0; i < filterModel->rowCount(); ++i) {
274 const QModelIndex idx = filterModel->index(i, 0);
275
277 ++expired;
278 } else {
279 ++active;
280 }
281
282 const bool read = idx.data(Notifications::ReadRole).toBool();
283 if (!active && !read) {
284 QDateTime date = idx.data(Notifications::UpdatedRole).toDateTime();
285 if (!date.isValid()) {
287 }
288
289 if (notificationsModel && date > notificationsModel->lastRead()) {
290 ++unread;
291 }
292 }
293
296 ++jobs;
297
298 totalPercentage += idx.data(Notifications::PercentageRole).toInt();
299 }
300 }
301 }
302
303 if (activeNotificationsCount != active) {
305 Q_EMIT q->activeNotificationsCountChanged();
306 }
307 if (expiredNotificationsCount != expired) {
309 Q_EMIT q->expiredNotificationsCountChanged();
310 }
311 if (unreadNotificationsCount != unread) {
313 Q_EMIT q->unreadNotificationsCountChanged();
314 }
315 if (activeJobsCount != jobs) {
316 activeJobsCount = jobs;
317 Q_EMIT q->activeJobsCountChanged();
318 }
319
320 const int percentage = (jobs > 0 ? totalPercentage / jobs : 0);
321 if (jobsPercentage != percentage) {
322 jobsPercentage = percentage;
323 Q_EMIT q->jobsPercentageChanged();
324 }
325
326 // TODO don't Q_EMIT in dataChanged
327 Q_EMIT q->countChanged();
328}
329
330bool Notifications::Private::isGroup(const QModelIndex &idx)
331{
333}
334
335uint Notifications::Private::notificationId(const QModelIndex &idx)
336{
337 return idx.data(Notifications::IdRole).toUInt();
338}
339
340QModelIndex Notifications::Private::mapFromModel(const QModelIndex &idx) const
341{
342 QModelIndex resolvedIdx = idx;
343
344 QAbstractItemModel *models[] = {
345 notificationsAndJobsModel,
347 sortModel,
348 groupingModel,
349 groupCollapsingModel,
350 flattenModel,
351 limiterModel,
352 };
353
354 // TODO can we do this with a generic loop like mapFromModel
355 while (resolvedIdx.isValid() && resolvedIdx.model() != q) {
356 const auto *idxModel = resolvedIdx.model();
357
358 // HACK try to find the model that uses the index' model as source
359 bool found = false;
360 for (QAbstractItemModel *model : models) {
361 if (!model) {
362 continue;
363 }
364
365 if (auto *proxyModel = qobject_cast<QAbstractProxyModel *>(model)) {
366 if (proxyModel->sourceModel() == idxModel) {
367 resolvedIdx = proxyModel->mapFromSource(resolvedIdx);
368 found = true;
369 break;
370 }
371 } else if (auto *concatenateModel = qobject_cast<QConcatenateTablesProxyModel *>(model)) {
372 if (idxModel == notificationsModel.get() || idxModel == jobsModel.get()) {
373 resolvedIdx = concatenateModel->mapFromSource(resolvedIdx);
374 found = true;
375 break;
376 }
377 }
378 }
379
380 if (!found) {
381 break;
382 }
383 }
384 return resolvedIdx;
385}
386
387std::shared_ptr<Settings> Notifications::Private::settings() const
388{
389 static std::weak_ptr<Settings> s_instance;
390 if (!s_instance.expired()) {
391 std::shared_ptr<Settings> ptr(new Settings());
392 s_instance = ptr;
393 return ptr;
394 }
395 return s_instance.lock();
396}
397
398Notifications::Notifications(QObject *parent)
400 , d(new Private(this))
401{
402 // The proxy models are always the same, just with different
403 // properties set whereas we want to avoid loading a source model
404 // e.g. notifications or jobs when we're not actually using them
405 d->initProxyModels();
406
407 // init source models when used from C++
409 this,
410 [this] {
411 d->initSourceModels();
412 },
414}
415
416Notifications::~Notifications() = default;
417
418void Notifications::classBegin()
419{
420}
421
422void Notifications::componentComplete()
423{
424 // init source models when used from QML
425 d->initSourceModels();
426}
427
428int Notifications::limit() const
429{
430 return d->limiterModel->limit();
431}
432
433void Notifications::setLimit(int limit)
434{
435 d->limiterModel->setLimit(limit);
436}
437
439{
440 return d->groupLimit;
441}
442
443void Notifications::setGroupLimit(int limit)
444{
445 if (d->groupLimit == limit) {
446 return;
447 }
448
449 d->groupLimit = limit;
450 if (d->groupCollapsingModel) {
451 d->groupCollapsingModel->setLimit(limit);
452 }
453 Q_EMIT groupLimitChanged();
454}
455
457{
458 return d->expandUnread;
459}
460
461void Notifications::setExpandUnread(bool expand)
462{
463 if (d->expandUnread == expand) {
464 return;
465 }
466
467 d->expandUnread = expand;
468 if (d->groupCollapsingModel) {
469 d->groupCollapsingModel->setExpandUnread(expand);
470 }
471 Q_EMIT expandUnreadChanged();
472}
473
474QWindow *Notifications::window() const
475{
476 return d->notificationsModel ? d->notificationsModel->window() : nullptr;
477}
478
479void Notifications::setWindow(QWindow *window)
480{
481 if (d->notificationsModel) {
482 d->notificationsModel->setWindow(window);
483 } else {
484 qCWarning(NOTIFICATIONMANAGER) << "Setting window before initialising the model" << this << window;
485 }
486}
487
489{
490 return d->filterModel->showExpired();
491}
492
493void Notifications::setShowExpired(bool show)
494{
495 d->filterModel->setShowExpired(show);
496}
497
499{
500 return d->filterModel->showDismissed();
501}
502
503void Notifications::setShowDismissed(bool show)
504{
505 d->filterModel->setShowDismissed(show);
506}
507
509{
510 return d->filterModel->showAddedDuringInhibition();
511}
512
513void Notifications::setShowAddedDuringInhibition(bool show)
514{
515 d->filterModel->setShowAddedDuringInhibition(show);
516}
517
519{
520 return d->filterModel->blacklistedDesktopEntries();
521}
522
523void Notifications::setBlacklistedDesktopEntries(const QStringList &blacklist)
524{
525 d->filterModel->setBlackListedDesktopEntries(blacklist);
526}
527
529{
530 return d->filterModel->blacklistedNotifyRcNames();
531}
532
533void Notifications::setBlacklistedNotifyRcNames(const QStringList &blacklist)
534{
535 d->filterModel->setBlacklistedNotifyRcNames(blacklist);
536}
537
539{
540 return d->filterModel->whitelistedDesktopEntries();
541}
542
543void Notifications::setWhitelistedDesktopEntries(const QStringList &whitelist)
544{
545 d->filterModel->setWhiteListedDesktopEntries(whitelist);
546}
547
549{
550 return d->filterModel->whitelistedNotifyRcNames();
551}
552
553void Notifications::setWhitelistedNotifyRcNames(const QStringList &whitelist)
554{
555 d->filterModel->setWhitelistedNotifyRcNames(whitelist);
556}
557
559{
560 return d->showNotifications;
561}
562
563void Notifications::setShowNotifications(bool show)
564{
565 if (d->showNotifications == show) {
566 return;
567 }
568
569 d->showNotifications = show;
570 d->initSourceModels();
571 Q_EMIT showNotificationsChanged();
572}
573
574bool Notifications::showJobs() const
575{
576 return d->showJobs;
577}
578
579void Notifications::setShowJobs(bool show)
580{
581 if (d->showJobs == show) {
582 return;
583 }
584
585 d->showJobs = show;
586 d->initSourceModels();
587 Q_EMIT showJobsChanged();
588}
589
590Notifications::Urgencies Notifications::urgencies() const
591{
592 return d->filterModel->urgencies();
593}
594
595void Notifications::setUrgencies(Urgencies urgencies)
596{
597 d->filterModel->setUrgencies(urgencies);
598}
599
601{
602 return d->sortModel->sortMode();
603}
604
605void Notifications::setSortMode(SortMode sortMode)
606{
607 d->sortModel->setSortMode(sortMode);
608}
609
611{
612 return d->sortModel->sortOrder();
613}
614
615void Notifications::setSortOrder(Qt::SortOrder sortOrder)
616{
617 d->sortModel->setSortOrder(sortOrder);
618}
619
621{
622 return d->groupMode;
623}
624
625void Notifications::setGroupMode(GroupMode groupMode)
626{
627 if (d->groupMode != groupMode) {
628 d->groupMode = groupMode;
629 d->initProxyModels();
630 Q_EMIT groupModeChanged();
631 }
632}
633
634int Notifications::count() const
635{
636 return rowCount(QModelIndex());
637}
638
640{
641 return d->activeNotificationsCount;
642}
643
645{
646 return d->expiredNotificationsCount;
647}
648
649QDateTime Notifications::lastRead() const
650{
651 if (d->notificationsModel) {
652 return d->notificationsModel->lastRead();
653 }
654 return QDateTime();
655}
656
657void Notifications::setLastRead(const QDateTime &lastRead)
658{
659 // TODO jobs could also be unread?
660 if (d->notificationsModel) {
661 d->notificationsModel->setLastRead(lastRead);
662 }
663 if (d->groupCollapsingModel) {
664 d->groupCollapsingModel->setLastRead(lastRead);
665 }
666}
667
668void Notifications::resetLastRead()
669{
670 setLastRead(QDateTime::currentDateTimeUtc());
671}
672
674{
675 return d->unreadNotificationsCount;
676}
677
679{
680 return d->activeJobsCount;
681}
682
684{
685 return d->jobsPercentage;
686}
687
692
694{
695 switch (static_cast<Notifications::Type>(idx.data(Notifications::TypeRole).toInt())) {
697 d->notificationsModel->expire(Private::notificationId(idx));
698 break;
700 d->jobsModel->expire(Utils::mapToModel(idx, d->jobsModel.get()));
701 break;
702 default:
703 Q_UNREACHABLE();
704 }
705}
706
708{
710 const QModelIndex groupIdx = Utils::mapToModel(idx, d->groupingModel);
711 if (!groupIdx.isValid()) {
712 qCWarning(NOTIFICATIONMANAGER) << "Failed to find group model index for this item";
713 return;
714 }
715
716 Q_ASSERT(groupIdx.model() == d->groupingModel);
717
718 const int childCount = d->groupingModel->rowCount(groupIdx);
719 for (int i = childCount - 1; i >= 0; --i) {
720 const QModelIndex childIdx = d->groupingModel->index(i, 0, groupIdx);
721 close(childIdx);
722 }
723 return;
724 }
725
727 return;
728 }
729
730 switch (static_cast<Notifications::Type>(idx.data(Notifications::TypeRole).toInt())) {
732 d->notificationsModel->close(Private::notificationId(idx));
733 break;
735 d->jobsModel->close(Utils::mapToModel(idx, d->jobsModel.get()));
736 break;
737 default:
738 Q_UNREACHABLE();
739 }
740}
741
743{
744 if (!d->notificationsModel) {
745 return;
746 }
747
748 // For groups just configure the application, not the individual event
749 if (Private::isGroup(idx)) {
750 const QString desktopEntry = idx.data(Notifications::DesktopEntryRole).toString();
751 const QString notifyRcName = idx.data(Notifications::NotifyRcNameRole).toString();
752
753 d->notificationsModel->configure(desktopEntry, notifyRcName, QString() /*eventId*/);
754 return;
755 }
756
757 d->notificationsModel->configure(Private::notificationId(idx));
758}
759
760void Notifications::invokeDefaultAction(const QModelIndex &idx, InvokeBehavior behavior)
761{
762 if (d->notificationsModel) {
763 d->notificationsModel->invokeDefaultAction(Private::notificationId(idx), behavior);
764 }
765}
766
767void Notifications::invokeAction(const QModelIndex &idx, const QString &actionId, InvokeBehavior behavior)
768{
769 if (d->notificationsModel) {
770 d->notificationsModel->invokeAction(Private::notificationId(idx), actionId, behavior);
771 }
772}
773
774void Notifications::reply(const QModelIndex &idx, const QString &text, InvokeBehavior behavior)
775{
776 if (d->notificationsModel) {
777 d->notificationsModel->reply(Private::notificationId(idx), text, behavior);
778 }
779}
780
782{
783 startTimeout(Private::notificationId(idx));
784}
785
786void Notifications::startTimeout(uint notificationId)
787{
788 if (d->notificationsModel) {
789 d->notificationsModel->startTimeout(notificationId);
790 }
791}
792
794{
795 if (d->notificationsModel) {
796 d->notificationsModel->stopTimeout(Private::notificationId(idx));
797 }
798}
799
801{
802 if (d->jobsModel) {
803 d->jobsModel->suspend(Utils::mapToModel(idx, d->jobsModel.get()));
804 }
805}
806
808{
809 if (d->jobsModel) {
810 d->jobsModel->resume(Utils::mapToModel(idx, d->jobsModel.get()));
811 }
812}
813
815{
816 if (d->jobsModel) {
817 d->jobsModel->kill(Utils::mapToModel(idx, d->jobsModel.get()));
818 }
819}
820
822{
823 if (d->notificationsModel) {
824 d->notificationsModel->clear(flags);
825 }
826 if (d->jobsModel) {
827 d->jobsModel->clear(flags);
828 }
829}
830
832{
834 return idx;
835 }
836
838 QModelIndex groupingIdx = Utils::mapToModel(idx, d->groupingModel);
839 return d->mapFromModel(groupingIdx.parent());
840 }
841
842 qCWarning(NOTIFICATIONMANAGER) << "Cannot get group index for item that isn't a group or inside one";
843 return QModelIndex();
844}
845
846void Notifications::collapseAllGroups()
847{
848 if (d->groupCollapsingModel) {
849 d->groupCollapsingModel->collapseAll();
850 }
851}
852
854{
855 int inhibited = 0;
856 for (int i = 0, count = d->notificationsAndJobsModel->rowCount(); i < count; ++i) {
857 const QModelIndex idx = d->notificationsAndJobsModel->index(i, 0);
859 ++inhibited;
860 }
861 }
862
863 if (!inhibited) {
864 return;
865 }
866
867 KNotification::event(u"inhibitionSummary"_s,
868 i18nc("@title", "Unread Notifications"),
869 i18nc("@info", "%1 notifications were received while Do Not Disturb was active.", QString::number(inhibited)),
870 u"preferences-desktop-notification-bell"_s,
872 u"libnotificationmanager"_s);
873}
874
875QVariant Notifications::data(const QModelIndex &index, int role) const
876{
878}
879
880bool Notifications::setData(const QModelIndex &index, const QVariant &value, int role)
881{
882 return QSortFilterProxyModel::setData(index, value, role);
883}
884
885bool Notifications::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const
886{
887 return QSortFilterProxyModel::filterAcceptsRow(source_row, source_parent);
888}
889
890bool Notifications::lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const
891{
892 return QSortFilterProxyModel::lessThan(source_left, source_right);
893}
894
895int Notifications::rowCount(const QModelIndex &parent) const
896{
898}
899
900QHash<int, QByteArray> Notifications::roleNames() const
901{
902 return Utils::roleNames();
903}
904
905void Notifications::playSoundHint(const QModelIndex &idx) const
906{
907 auto hints = data(idx, Notifications::HintsRole).toMap();
908 auto soundFilePath = hints.value(QStringLiteral("sound-file")).toString();
909 auto soundName = hints.value(QStringLiteral("sound-name")).toString();
910 auto isSuppressSound = hints.value(QStringLiteral("suppress-sound")).toBool();
911 auto desktopFile = data(idx, Notifications::DesktopEntryRole).toString();
912 auto appName = data(idx, Notifications::ApplicationNameRole).toString();
913
914 if (isSuppressSound || (soundName.isEmpty() && soundFilePath.isEmpty())) {
915 return;
916 }
917
918 if (!d->canberraContext) {
919 const int ret = ca_context_create(&d->canberraContext);
920 if (ret != CA_SUCCESS) {
921 qCWarning(NOTIFICATIONMANAGER)
922 << "Failed to initialize canberra context for audio notification:"
923 << ca_strerror(ret);
924 d->canberraContext = nullptr;
925 return;
926 }
927 }
928
929 ca_proplist *props = nullptr;
930 ca_proplist_create(&props);
931
932 const auto config = KSharedConfig::openConfig(QStringLiteral("kdeglobals"));
933 const KConfigGroup soundGroup = config->group(QStringLiteral("Sounds"));
934 const auto soundTheme =
935 soundGroup.readEntry("Theme", QStringLiteral("ocean"));
936
937 if (!soundName.isEmpty()) {
938 ca_proplist_sets(props, CA_PROP_EVENT_ID, soundName.toLatin1().constData());
939 }
940 if (!soundFilePath.isEmpty()) {
941 ca_proplist_sets(props, CA_PROP_MEDIA_FILENAME,
942 QFile::encodeName(soundFilePath).constData());
943 }
944 ca_proplist_sets(props, CA_PROP_APPLICATION_NAME,
946 ca_proplist_sets(props, CA_PROP_APPLICATION_ID,
947 desktopFile.toLatin1().constData());
948 ca_proplist_sets(props, CA_PROP_CANBERRA_XDG_THEME_NAME,
949 soundTheme.toLatin1().constData());
950 // We'll also want this cached for a time. volatile makes sure the cache is
951 // dropped after some time or when the cache is under pressure.
952 ca_proplist_sets(props, CA_PROP_CANBERRA_CACHE_CONTROL, "volatile");
953
954 const int ret =
955 ca_context_play_full(d->canberraContext, 0, props, nullptr, nullptr);
956
957 ca_proplist_destroy(props);
958
959 if (ret != CA_SUCCESS) {
960 qCWarning(NOTIFICATIONMANAGER) << "Failed to play sound" << soundName
961 << "with canberra:" << ca_strerror(ret);
962 return;
963 }
964}
965
966#include "moc_notifications.cpp"
QString readEntry(const char *key, const char *aDefault=nullptr) const
static KNotification * event(const QString &eventId, const QString &text=QString(), const QPixmap &pixmap=QPixmap(), const NotificationFlags &flags=CloseOnTimeout, const QString &componentName=QString())
static KSharedConfig::Ptr openConfig(const QString &fileName=QString(), OpenFlags mode=FullConfig, QStandardPaths::StandardLocation type=QStandardPaths::GenericConfigLocation)
A model with notifications and jobs.
Q_INVOKABLE void expire(const QModelIndex &idx)
Expire a notification.
QStringList whitelistedDesktopEntries
A list of desktop entries for which notifications should be shown.
GroupMode groupMode
The group mode for notifications.
Q_INVOKABLE void invokeAction(const QModelIndex &idx, const QString &actionId, InvokeBehavior=None)
Invoke a notification action.
QStringList blacklistedNotifyRcNames
A list of notifyrc names for which no notifications should be shown.
int groupLimit
How many notifications are shown in each group.
Q_INVOKABLE void resumeJob(const QModelIndex &idx)
Resume a job.
bool showNotifications
Whether to show notifications.
int unreadNotificationsCount
The number of notifications added since lastRead.
bool expandUnread
Whether to automatically show notifications that are unread.
Q_INVOKABLE void configure(const QModelIndex &idx)
Configure a notification.
bool showDismissed
Whether to show dismissed notifications.
Q_INVOKABLE void clear(ClearFlags flags)
Clear notifications.
int activeNotificationsCount
The number of active, i.e.
Q_INVOKABLE void invokeDefaultAction(const QModelIndex &idx, InvokeBehavior behavior=None)
Invoke the default notification action.
Q_INVOKABLE void stopTimeout(const QModelIndex &idx)
Stop the automatic timeout of notifications.
QWindow * window
The window that will render the notifications.
bool showExpired
Whether to show expired notifications.
Q_INVOKABLE QPersistentModelIndex makePersistentModelIndex(const QModelIndex &idx) const
Convert the given QModelIndex into a QPersistentModelIndex.
int jobsPercentage
The combined percentage of all jobs.
Urgencies urgencies
The notification urgency types the model should contain.
Q_INVOKABLE void showInhibitionSummary()
Shows a notification to report the number of unread inhibited notifications.
QStringList whitelistedNotifyRcNames
A list of notifyrc names for which notifications should be shown.
SortMode
The sort mode for the model.
@ JobType
This item represents an application job.
@ NotificationType
This item represents a notification.
bool showAddedDuringInhibition
Whether to show notifications added during inhibition.
Qt::SortOrder sortOrder
The sort order for notifications.
@ JobStateStopped
The job is stopped. It has either finished (error is 0) or failed (error is not 0)
int expiredNotificationsCount
The number of inactive, i.e.
bool showJobs
Whether to show application jobs.
QML_ELEMENTint limit
The number of notifications the model should at most contain.
GroupMode
The group mode for the model.
Q_INVOKABLE void reply(const QModelIndex &idx, const QString &text, InvokeBehavior behavior)
Reply to a notification.
int activeJobsCount
The number of active jobs.
int count
The number of notifications in the model.
Q_INVOKABLE void close(const QModelIndex &idx)
Close a notification.
@ ApplicationNameRole
The user-visible name of the application (e.g. Spectacle)
@ UpdatedRole
When the notification was last updated, invalid when it hasn't been updated.
@ NotifyRcNameRole
The notifyrc name (e.g. spectaclerc) of the application that sent the notification.
@ ReadRole
Whether the notification got read by the user.
@ IsInGroupRole
Whether the notification is currently inside a group.
@ JobStateRole
The state of the job, either JobStateJopped, JobStateSuspended, or JobStateRunning.
@ IdRole
A notification identifier. This can be uint notification ID or string application job source.
@ DesktopEntryRole
The desktop entry (without .desktop suffix, e.g. org.kde.spectacle) of the application that sent the ...
@ IsGroupRole
Whether the item is a group.
@ WasAddedDuringInhibitionRole
Whether the notification was added while inhibition was active.
@ ExpiredRole
The notification timed out and closed. Actions on it cannot be invoked anymore.
@ HintsRole
To provide extra data to a notification server that the server may be able to make use of.
@ CreatedRole
When the notification was first created.
@ ClosableRole
Whether the item can be closed. Notifications are always closable, jobs are only when in JobStateStop...
@ TypeRole
The type of model entry, either NotificationType or JobType.
@ PercentageRole
The percentage of the job. Use jobsPercentage to get a global percentage for all jobs.
QDateTime lastRead
The time when the user last could read the notifications.
Q_INVOKABLE void startTimeout(const QModelIndex &idx)
Start automatic timeout of notifications.
QStringList blacklistedDesktopEntries
A list of desktop entries for which no notifications should be shown.
Q_INVOKABLE QModelIndex groupIndex(const QModelIndex &idx) const
Returns a model index pointing to the group of a notification.
SortMode sortMode
The sort mode for notifications.
Q_INVOKABLE void killJob(const QModelIndex &idx)
Kill a job.
Q_INVOKABLE void suspendJob(const QModelIndex &idx)
Suspend a job.
QString i18nc(const char *context, const char *text, const TYPE &arg...)
const FMH::MODEL filterModel(const MODEL &model, const QVector< MODEL_KEY > &keys)
QVariant read(const QByteArray &data, int versionOverride=0)
QCA_EXPORT QString appName()
QAbstractItemModel(QObject *parent)
void dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QList< int > &roles)
virtual QModelIndex parent(const QModelIndex &index) const const=0
void rowsInserted(const QModelIndex &parent, int first, int last)
void rowsRemoved(const QModelIndex &parent, int first, int last)
const char * constData() const const
QConcatenateTablesProxyModel(QObject *parent)
QDateTime currentDateTimeUtc()
bool isValid() const const
QByteArray encodeName(const QString &fileName)
bool contains(const AT &value) const const
bool isEmpty() const const
bool invokeMethod(QObject *context, Functor &&function, FunctorReturnType *ret)
QVariant data(int role) const const
bool isValid() const const
const QAbstractItemModel * model() const const
QModelIndex parent() const const
QObject(QObject *parent)
Q_EMITQ_EMIT
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
bool disconnect(const QMetaObject::Connection &connection)
T qobject_cast(QObject *object)
QSortFilterProxyModel(QObject *parent)
virtual QVariant data(const QModelIndex &index, int role) const const override
virtual bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const const
virtual Qt::ItemFlags flags(const QModelIndex &index) const const override
virtual QModelIndex index(int row, int column, const QModelIndex &parent) const const override
virtual bool lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const const
virtual int rowCount(const QModelIndex &parent) const const override
virtual bool setData(const QModelIndex &index, const QVariant &value, int role) override
QString number(double n, char format, int precision)
QByteArray toLatin1() const const
QueuedConnection
SortOrder
bool toBool() const const
QDateTime toDateTime() const const
int toInt(bool *ok) const const
QString toString() const const
uint toUInt(bool *ok) const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2025 The KDE developers.
Generated on Fri Mar 28 2025 11:53:53 by doxygen 1.13.2 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.