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 int dismissedResidentNotificationsCount = 0;
85
86 int activeJobsCount = 0;
87 int jobsPercentage = 0;
88
89 static bool isGroup(const QModelIndex &idx);
90 static uint notificationId(const QModelIndex &idx);
91 QModelIndex mapFromModel(const QModelIndex &idx) const;
92
93 // NOTE when you add or re-arrange models make sure to update mapFromModel()!
94 NotificationsModel::Ptr notificationsModel;
95 JobsModel::Ptr jobsModel;
96 std::shared_ptr<Settings> settings() const;
97
98 QConcatenateTablesProxyModel *notificationsAndJobsModel = nullptr;
99
100 NotificationFilterProxyModel *filterModel = nullptr;
101 NotificationSortProxyModel *sortModel = nullptr;
102 NotificationGroupingProxyModel *groupingModel = nullptr;
103 NotificationGroupCollapsingProxyModel *groupCollapsingModel = nullptr;
104 KDescendantsProxyModel *flattenModel = nullptr;
105 ca_context *canberraContext = nullptr;
106 LimitedRowCountProxyModel *limiterModel = nullptr;
107
108private:
109 Notifications *const q;
110};
111
112Notifications::Private::Private(Notifications *q)
113 : q(q)
114{
115}
116
117Notifications::Private::~Private()
118{
119}
120
121void Notifications::Private::initSourceModels()
122{
123 Q_ASSERT(notificationsAndJobsModel); // initProxyModels must be called before initSourceModels
124
125 if (showNotifications && !notificationsModel) {
126 notificationsModel = NotificationsModel::createNotificationsModel();
127 notificationsAndJobsModel->addSourceModel(notificationsModel.get());
128 connect(notificationsModel.get(), &NotificationsModel::lastReadChanged, q, [this] {
129 updateCount();
130 Q_EMIT q->lastReadChanged();
131 });
132 } else if (!showNotifications && notificationsModel) {
133 notificationsAndJobsModel->removeSourceModel(notificationsModel.get());
134 disconnect(notificationsModel.get(), nullptr, q, nullptr); // disconnect all
135 notificationsModel = nullptr;
136 }
137
138 if (showJobs && !jobsModel) {
139 jobsModel = JobsModel::createJobsModel();
140 notificationsAndJobsModel->addSourceModel(jobsModel.get());
141 jobsModel->init();
142 } else if (!showJobs && jobsModel) {
143 notificationsAndJobsModel->removeSourceModel(jobsModel.get());
144 jobsModel = nullptr;
145 }
146}
147
148void Notifications::Private::initProxyModels()
149{
150 /* The data flow is as follows:
151 * NOTE when you add or re-arrange models make sure to update mapFromModel()!
152 *
153 * NotificationsModel JobsModel
154 * \\ /
155 * \\ /
156 * QConcatenateTablesProxyModel
157 * |||
158 * |||
159 * NotificationFilterProxyModel
160 * (filters by urgency, whitelist, etc)
161 * |
162 * |
163 * NotificationSortProxyModel
164 * (sorts by urgency, date, etc)
165 * |
166 * --- BEGIN: Only when grouping is enabled ---
167 * |
168 * NotificationGroupingProxyModel
169 * (turns list into tree grouped by app)
170 * //\\
171 * //\\
172 * NotificationGroupCollapsingProxyModel
173 * (limits number of tree leaves for expand/collapse feature)
174 * /\
175 * /\
176 * KDescendantsProxyModel
177 * (flattens tree back into a list for consumption in ListView)
178 * |
179 * --- END: Only when grouping is enabled ---
180 * |
181 * LimitedRowCountProxyModel
182 * (limits the total number of items in the model)
183 * |
184 * |
185 * \o/ <- Happy user seeing their notifications
186 */
187
188 if (!notificationsAndJobsModel) {
189 notificationsAndJobsModel = new SingleColumnConcatenateTables(q);
190 }
191
192 if (!filterModel) {
193 filterModel = new NotificationFilterProxyModel();
194 connect(filterModel, &NotificationFilterProxyModel::urgenciesChanged, q, &Notifications::urgenciesChanged);
195 connect(filterModel, &NotificationFilterProxyModel::showExpiredChanged, q, &Notifications::showExpiredChanged);
196 connect(filterModel, &NotificationFilterProxyModel::showDismissedChanged, q, &Notifications::showDismissedChanged);
197 connect(filterModel, &NotificationFilterProxyModel::showAddedDuringInhibitionChanged, q, &Notifications::showAddedDuringInhibitionChanged);
198 connect(filterModel, &NotificationFilterProxyModel::blacklistedDesktopEntriesChanged, q, &Notifications::blacklistedDesktopEntriesChanged);
199 connect(filterModel, &NotificationFilterProxyModel::blacklistedNotifyRcNamesChanged, q, &Notifications::blacklistedNotifyRcNamesChanged);
200
201 filterModel->setSourceModel(notificationsAndJobsModel);
202
203 connect(filterModel, &QAbstractItemModel::rowsInserted, q, [this] {
204 updateCount();
205 });
206 connect(filterModel, &QAbstractItemModel::rowsRemoved, q, [this] {
207 updateCount();
208 });
209 connect(filterModel, &QAbstractItemModel::dataChanged, q, [this](const QModelIndex &topLeft, const QModelIndex &bottomRight, const QList<int> &roles) {
210 Q_UNUSED(topLeft);
211 Q_UNUSED(bottomRight);
215 updateCount();
216 }
217 });
218 }
219
220 if (!sortModel) {
221 sortModel = new NotificationSortProxyModel(q);
222 connect(sortModel, &NotificationSortProxyModel::sortModeChanged, q, &Notifications::sortModeChanged);
223 connect(sortModel, &NotificationSortProxyModel::sortOrderChanged, q, &Notifications::sortOrderChanged);
224 }
225
226 if (!limiterModel) {
227 limiterModel = new LimitedRowCountProxyModel(q);
228 connect(limiterModel, &LimitedRowCountProxyModel::limitChanged, q, &Notifications::limitChanged);
229 }
230
231 if (groupMode == GroupApplicationsFlat) {
232 if (!groupingModel) {
233 groupingModel = new NotificationGroupingProxyModel(q);
234 groupingModel->setSourceModel(filterModel);
235 }
236
237 if (!groupCollapsingModel) {
238 groupCollapsingModel = new NotificationGroupCollapsingProxyModel(q);
239 groupCollapsingModel->setLimit(groupLimit);
240 groupCollapsingModel->setExpandUnread(expandUnread);
241 groupCollapsingModel->setLastRead(q->lastRead());
242 groupCollapsingModel->setSourceModel(groupingModel);
243 }
244
245 sortModel->setSourceModel(groupCollapsingModel);
246
247 flattenModel = new KDescendantsProxyModel(q);
248 flattenModel->setSourceModel(sortModel);
249
250 limiterModel->setSourceModel(flattenModel);
251 } else {
252 sortModel->setSourceModel(filterModel);
253 limiterModel->setSourceModel(sortModel);
254 delete flattenModel;
255 flattenModel = nullptr;
256 delete groupingModel;
257 groupingModel = nullptr;
258 }
259
260 q->setSourceModel(limiterModel);
261}
262
263void Notifications::Private::updateCount()
264{
265 int active = 0;
266 int expired = 0;
267 int unread = 0;
268 int dismissedResident = 0;
269
270 int jobs = 0;
271 int totalPercentage = 0;
272
273 // We want to get the numbers after main filtering (urgencies, whitelists, etc)
274 // but before any limiting or group limiting, hence asking the filterModel for advice
275 // at which point notifications and jobs also have already been merged
276 for (int i = 0; i < filterModel->rowCount(); ++i) {
277 const QModelIndex idx = filterModel->index(i, 0);
278
279 const bool isExpired = idx.data(Notifications::ExpiredRole).toBool();
281 ++expired;
282 } else {
283 ++active;
284 }
285
286 const bool read = idx.data(Notifications::ReadRole).toBool();
287 if (!active && !read) {
288 QDateTime date = idx.data(Notifications::UpdatedRole).toDateTime();
289 if (!date.isValid()) {
291 }
292
293 if (notificationsModel && date > notificationsModel->lastRead()) {
294 ++unread;
295 }
296 }
297
299 ++dismissedResident;
300 }
301
304 ++jobs;
305
306 totalPercentage += idx.data(Notifications::PercentageRole).toInt();
307 }
308 }
309 }
310
311 if (activeNotificationsCount != active) {
313 Q_EMIT q->activeNotificationsCountChanged();
314 }
315 if (expiredNotificationsCount != expired) {
317 Q_EMIT q->expiredNotificationsCountChanged();
318 }
319 if (unreadNotificationsCount != unread) {
321 Q_EMIT q->unreadNotificationsCountChanged();
322 }
323 if (dismissedResidentNotificationsCount != dismissedResident) {
324 dismissedResidentNotificationsCount = dismissedResident;
325 Q_EMIT q->dismissedResidentNotificationsCountChanged();
326 }
327 if (activeJobsCount != jobs) {
328 activeJobsCount = jobs;
329 Q_EMIT q->activeJobsCountChanged();
330 }
331
332 const int percentage = (jobs > 0 ? totalPercentage / jobs : 0);
333 if (jobsPercentage != percentage) {
334 jobsPercentage = percentage;
335 Q_EMIT q->jobsPercentageChanged();
336 }
337
338 // TODO don't Q_EMIT in dataChanged
339 Q_EMIT q->countChanged();
340}
341
342bool Notifications::Private::isGroup(const QModelIndex &idx)
343{
345}
346
347uint Notifications::Private::notificationId(const QModelIndex &idx)
348{
349 return idx.data(Notifications::IdRole).toUInt();
350}
351
352QModelIndex Notifications::Private::mapFromModel(const QModelIndex &idx) const
353{
354 QModelIndex resolvedIdx = idx;
355
356 QAbstractItemModel *models[] = {
357 notificationsAndJobsModel,
359 sortModel,
360 groupingModel,
361 groupCollapsingModel,
362 flattenModel,
363 limiterModel,
364 };
365
366 // TODO can we do this with a generic loop like mapFromModel
367 while (resolvedIdx.isValid() && resolvedIdx.model() != q) {
368 const auto *idxModel = resolvedIdx.model();
369
370 // HACK try to find the model that uses the index' model as source
371 bool found = false;
372 for (QAbstractItemModel *model : models) {
373 if (!model) {
374 continue;
375 }
376
377 if (auto *proxyModel = qobject_cast<QAbstractProxyModel *>(model)) {
378 if (proxyModel->sourceModel() == idxModel) {
379 resolvedIdx = proxyModel->mapFromSource(resolvedIdx);
380 found = true;
381 break;
382 }
383 } else if (auto *concatenateModel = qobject_cast<QConcatenateTablesProxyModel *>(model)) {
384 if (idxModel == notificationsModel.get() || idxModel == jobsModel.get()) {
385 resolvedIdx = concatenateModel->mapFromSource(resolvedIdx);
386 found = true;
387 break;
388 }
389 }
390 }
391
392 if (!found) {
393 break;
394 }
395 }
396 return resolvedIdx;
397}
398
399std::shared_ptr<Settings> Notifications::Private::settings() const
400{
401 static std::weak_ptr<Settings> s_instance;
402 if (!s_instance.expired()) {
403 std::shared_ptr<Settings> ptr(new Settings());
404 s_instance = ptr;
405 return ptr;
406 }
407 return s_instance.lock();
408}
409
410Notifications::Notifications(QObject *parent)
412 , d(new Private(this))
413{
414 // The proxy models are always the same, just with different
415 // properties set whereas we want to avoid loading a source model
416 // e.g. notifications or jobs when we're not actually using them
417 d->initProxyModels();
418
419 // init source models when used from C++
421 this,
422 [this] {
423 d->initSourceModels();
424 },
426}
427
428Notifications::~Notifications() = default;
429
430void Notifications::classBegin()
431{
432}
433
434void Notifications::componentComplete()
435{
436 // init source models when used from QML
437 d->initSourceModels();
438}
439
440int Notifications::limit() const
441{
442 return d->limiterModel->limit();
443}
444
445void Notifications::setLimit(int limit)
446{
447 d->limiterModel->setLimit(limit);
448}
449
451{
452 return d->groupLimit;
453}
454
455void Notifications::setGroupLimit(int limit)
456{
457 if (d->groupLimit == limit) {
458 return;
459 }
460
461 d->groupLimit = limit;
462 if (d->groupCollapsingModel) {
463 d->groupCollapsingModel->setLimit(limit);
464 }
465 Q_EMIT groupLimitChanged();
466}
467
469{
470 return d->expandUnread;
471}
472
473void Notifications::setExpandUnread(bool expand)
474{
475 if (d->expandUnread == expand) {
476 return;
477 }
478
479 d->expandUnread = expand;
480 if (d->groupCollapsingModel) {
481 d->groupCollapsingModel->setExpandUnread(expand);
482 }
483 Q_EMIT expandUnreadChanged();
484}
485
486QWindow *Notifications::window() const
487{
488 return d->notificationsModel ? d->notificationsModel->window() : nullptr;
489}
490
491void Notifications::setWindow(QWindow *window)
492{
493 if (d->notificationsModel) {
494 d->notificationsModel->setWindow(window);
495 } else {
496 qCWarning(NOTIFICATIONMANAGER) << "Setting window before initialising the model" << this << window;
497 }
498}
499
501{
502 return d->filterModel->showExpired();
503}
504
505void Notifications::setShowExpired(bool show)
506{
507 d->filterModel->setShowExpired(show);
508}
509
511{
512 return d->filterModel->showDismissed();
513}
514
515void Notifications::setShowDismissed(bool show)
516{
517 d->filterModel->setShowDismissed(show);
518}
519
521{
522 return d->filterModel->showAddedDuringInhibition();
523}
524
525void Notifications::setShowAddedDuringInhibition(bool show)
526{
527 d->filterModel->setShowAddedDuringInhibition(show);
528}
529
531{
532 return d->filterModel->blacklistedDesktopEntries();
533}
534
535void Notifications::setBlacklistedDesktopEntries(const QStringList &blacklist)
536{
537 d->filterModel->setBlackListedDesktopEntries(blacklist);
538}
539
541{
542 return d->filterModel->blacklistedNotifyRcNames();
543}
544
545void Notifications::setBlacklistedNotifyRcNames(const QStringList &blacklist)
546{
547 d->filterModel->setBlacklistedNotifyRcNames(blacklist);
548}
549
551{
552 return d->filterModel->whitelistedDesktopEntries();
553}
554
555void Notifications::setWhitelistedDesktopEntries(const QStringList &whitelist)
556{
557 d->filterModel->setWhiteListedDesktopEntries(whitelist);
558}
559
561{
562 return d->filterModel->whitelistedNotifyRcNames();
563}
564
565void Notifications::setWhitelistedNotifyRcNames(const QStringList &whitelist)
566{
567 d->filterModel->setWhitelistedNotifyRcNames(whitelist);
568}
569
571{
572 return d->showNotifications;
573}
574
575void Notifications::setShowNotifications(bool show)
576{
577 if (d->showNotifications == show) {
578 return;
579 }
580
581 d->showNotifications = show;
582 d->initSourceModels();
583 Q_EMIT showNotificationsChanged();
584}
585
586bool Notifications::showJobs() const
587{
588 return d->showJobs;
589}
590
591void Notifications::setShowJobs(bool show)
592{
593 if (d->showJobs == show) {
594 return;
595 }
596
597 d->showJobs = show;
598 d->initSourceModels();
599 Q_EMIT showJobsChanged();
600}
601
602Notifications::Urgencies Notifications::urgencies() const
603{
604 return d->filterModel->urgencies();
605}
606
607void Notifications::setUrgencies(Urgencies urgencies)
608{
609 d->filterModel->setUrgencies(urgencies);
610}
611
613{
614 return d->sortModel->sortMode();
615}
616
617void Notifications::setSortMode(SortMode sortMode)
618{
619 d->sortModel->setSortMode(sortMode);
620}
621
623{
624 return d->sortModel->sortOrder();
625}
626
627void Notifications::setSortOrder(Qt::SortOrder sortOrder)
628{
629 d->sortModel->setSortOrder(sortOrder);
630}
631
633{
634 return d->groupMode;
635}
636
637void Notifications::setGroupMode(GroupMode groupMode)
638{
639 if (d->groupMode != groupMode) {
640 d->groupMode = groupMode;
641 d->initProxyModels();
642 Q_EMIT groupModeChanged();
643 }
644}
645
646int Notifications::count() const
647{
648 return rowCount(QModelIndex());
649}
650
652{
653 return d->activeNotificationsCount;
654}
655
657{
658 return d->expiredNotificationsCount;
659}
660
661QDateTime Notifications::lastRead() const
662{
663 if (d->notificationsModel) {
664 return d->notificationsModel->lastRead();
665 }
666 return QDateTime();
667}
668
669void Notifications::setLastRead(const QDateTime &lastRead)
670{
671 // TODO jobs could also be unread?
672 if (d->notificationsModel) {
673 d->notificationsModel->setLastRead(lastRead);
674 }
675 if (d->groupCollapsingModel) {
676 d->groupCollapsingModel->setLastRead(lastRead);
677 }
678}
679
680void Notifications::resetLastRead()
681{
682 setLastRead(QDateTime::currentDateTimeUtc());
683}
684
686{
687 return d->unreadNotificationsCount;
688}
689
691{
692 return d->dismissedResidentNotificationsCount;
693}
694
696{
697 return d->activeJobsCount;
698}
699
701{
702 return d->jobsPercentage;
703}
704
709
711{
712 switch (static_cast<Notifications::Type>(idx.data(Notifications::TypeRole).toInt())) {
714 d->notificationsModel->expire(Private::notificationId(idx));
715 break;
717 d->jobsModel->expire(Utils::mapToModel(idx, d->jobsModel.get()));
718 break;
719 default:
720 Q_UNREACHABLE();
721 }
722}
723
725{
727 const QModelIndex groupIdx = Utils::mapToModel(idx, d->groupingModel);
728 if (!groupIdx.isValid()) {
729 qCWarning(NOTIFICATIONMANAGER) << "Failed to find group model index for this item";
730 return;
731 }
732
733 Q_ASSERT(groupIdx.model() == d->groupingModel);
734
735 const int childCount = d->groupingModel->rowCount(groupIdx);
736 for (int i = childCount - 1; i >= 0; --i) {
737 const QModelIndex childIdx = d->groupingModel->index(i, 0, groupIdx);
738 close(childIdx);
739 }
740 return;
741 }
742
744 return;
745 }
746
747 switch (static_cast<Notifications::Type>(idx.data(Notifications::TypeRole).toInt())) {
749 d->notificationsModel->close(Private::notificationId(idx));
750 break;
752 d->jobsModel->close(Utils::mapToModel(idx, d->jobsModel.get()));
753 break;
754 default:
755 Q_UNREACHABLE();
756 }
757}
758
760{
761 if (!d->notificationsModel) {
762 return;
763 }
764
765 // For groups just configure the application, not the individual event
766 if (Private::isGroup(idx)) {
767 const QString desktopEntry = idx.data(Notifications::DesktopEntryRole).toString();
768 const QString notifyRcName = idx.data(Notifications::NotifyRcNameRole).toString();
769
770 d->notificationsModel->configure(desktopEntry, notifyRcName, QString() /*eventId*/);
771 return;
772 }
773
774 d->notificationsModel->configure(Private::notificationId(idx));
775}
776
777void Notifications::invokeDefaultAction(const QModelIndex &idx, InvokeBehavior behavior)
778{
779 if (d->notificationsModel) {
780 d->notificationsModel->invokeDefaultAction(Private::notificationId(idx), behavior);
781 }
782}
783
784void Notifications::invokeAction(const QModelIndex &idx, const QString &actionId, InvokeBehavior behavior)
785{
786 if (d->notificationsModel) {
787 d->notificationsModel->invokeAction(Private::notificationId(idx), actionId, behavior);
788 }
789}
790
791void Notifications::reply(const QModelIndex &idx, const QString &text, InvokeBehavior behavior)
792{
793 if (d->notificationsModel) {
794 d->notificationsModel->reply(Private::notificationId(idx), text, behavior);
795 }
796}
797
799{
800 startTimeout(Private::notificationId(idx));
801}
802
803void Notifications::startTimeout(uint notificationId)
804{
805 if (d->notificationsModel) {
806 d->notificationsModel->startTimeout(notificationId);
807 }
808}
809
811{
812 if (d->notificationsModel) {
813 d->notificationsModel->stopTimeout(Private::notificationId(idx));
814 }
815}
816
818{
819 if (d->jobsModel) {
820 d->jobsModel->suspend(Utils::mapToModel(idx, d->jobsModel.get()));
821 }
822}
823
825{
826 if (d->jobsModel) {
827 d->jobsModel->resume(Utils::mapToModel(idx, d->jobsModel.get()));
828 }
829}
830
832{
833 if (d->jobsModel) {
834 d->jobsModel->kill(Utils::mapToModel(idx, d->jobsModel.get()));
835 }
836}
837
839{
840 if (d->notificationsModel) {
841 d->notificationsModel->clear(flags);
842 }
843 if (d->jobsModel) {
844 d->jobsModel->clear(flags);
845 }
846}
847
849{
851 return idx;
852 }
853
855 QModelIndex groupingIdx = Utils::mapToModel(idx, d->groupingModel);
856 return d->mapFromModel(groupingIdx.parent());
857 }
858
859 qCWarning(NOTIFICATIONMANAGER) << "Cannot get group index for item that isn't a group or inside one";
860 return QModelIndex();
861}
862
863void Notifications::collapseAllGroups()
864{
865 if (d->groupCollapsingModel) {
866 d->groupCollapsingModel->collapseAll();
867 }
868}
869
871{
872 int inhibited = 0;
873 for (int i = 0, count = d->notificationsAndJobsModel->rowCount(); i < count; ++i) {
874 const QModelIndex idx = d->notificationsAndJobsModel->index(i, 0);
876 ++inhibited;
877 }
878 }
879
880 if (!inhibited) {
881 return;
882 }
883
884 KNotification::event(u"inhibitionSummary"_s,
885 i18nc("@title", "Unread Notifications"),
886 i18nc("@info", "%1 notifications were received while Do Not Disturb was active.", QString::number(inhibited)),
887 u"preferences-desktop-notification-bell"_s,
889 u"libnotificationmanager"_s);
890}
891
892QVariant Notifications::data(const QModelIndex &index, int role) const
893{
895}
896
897bool Notifications::setData(const QModelIndex &index, const QVariant &value, int role)
898{
899 return QSortFilterProxyModel::setData(index, value, role);
900}
901
902bool Notifications::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const
903{
904 return QSortFilterProxyModel::filterAcceptsRow(source_row, source_parent);
905}
906
907bool Notifications::lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const
908{
909 return QSortFilterProxyModel::lessThan(source_left, source_right);
910}
911
912int Notifications::rowCount(const QModelIndex &parent) const
913{
915}
916
917QHash<int, QByteArray> Notifications::roleNames() const
918{
919 return Utils::roleNames();
920}
921
922void Notifications::playSoundHint(const QModelIndex &idx) const
923{
924 auto hints = data(idx, Notifications::HintsRole).toMap();
925 auto soundFilePath = hints.value(QStringLiteral("sound-file")).toString();
926 auto soundName = hints.value(QStringLiteral("sound-name")).toString();
927 auto isSuppressSound = hints.value(QStringLiteral("suppress-sound")).toBool();
928 auto desktopFile = data(idx, Notifications::DesktopEntryRole).toString();
929 auto appName = data(idx, Notifications::ApplicationNameRole).toString();
930
931 if (isSuppressSound || (soundName.isEmpty() && soundFilePath.isEmpty())) {
932 return;
933 }
934
935 if (!d->canberraContext) {
936 const int ret = ca_context_create(&d->canberraContext);
937 if (ret != CA_SUCCESS) {
938 qCWarning(NOTIFICATIONMANAGER)
939 << "Failed to initialize canberra context for audio notification:"
940 << ca_strerror(ret);
941 d->canberraContext = nullptr;
942 return;
943 }
944 }
945
946 ca_proplist *props = nullptr;
947 ca_proplist_create(&props);
948
949 const auto config = KSharedConfig::openConfig(QStringLiteral("kdeglobals"));
950 const KConfigGroup soundGroup = config->group(QStringLiteral("Sounds"));
951 const auto soundTheme =
952 soundGroup.readEntry("Theme", QStringLiteral("ocean"));
953
954 if (!soundName.isEmpty()) {
955 ca_proplist_sets(props, CA_PROP_EVENT_ID, soundName.toLatin1().constData());
956 }
957 if (!soundFilePath.isEmpty()) {
958 ca_proplist_sets(props, CA_PROP_MEDIA_FILENAME,
959 QFile::encodeName(soundFilePath).constData());
960 }
961 ca_proplist_sets(props, CA_PROP_APPLICATION_NAME,
963 ca_proplist_sets(props, CA_PROP_APPLICATION_ID,
964 desktopFile.toLatin1().constData());
965 ca_proplist_sets(props, CA_PROP_CANBERRA_XDG_THEME_NAME,
966 soundTheme.toLatin1().constData());
967 // We'll also want this cached for a time. volatile makes sure the cache is
968 // dropped after some time or when the cache is under pressure.
969 ca_proplist_sets(props, CA_PROP_CANBERRA_CACHE_CONTROL, "volatile");
970
971 const int ret =
972 ca_context_play_full(d->canberraContext, 0, props, nullptr, nullptr);
973
974 ca_proplist_destroy(props);
975
976 if (ret != CA_SUCCESS) {
977 qCWarning(NOTIFICATIONMANAGER) << "Failed to play sound" << soundName
978 << "with canberra:" << ca_strerror(ret);
979 return;
980 }
981}
982
983#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 dismissedResidentNotificationsCount
The number of resident notifications that have been dismissed.
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.
@ ResidentRole
Whether the notification should keep its actions even when they were invoked.
@ DismissedRole
The notification got temporarily hidden by the user but could still be interacted with.
@ 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 Apr 11 2025 11:52:44 by doxygen 1.13.2 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.