7#include "notifications.h"
9#include <QConcatenateTablesProxyModel>
17#include <KConfigGroup>
18#include <KConfigWatcher>
19#include <KDescendantsProxyModel>
20#include <KLocalizedString>
21#include <KNotification>
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"
34#include "notification.h"
40using namespace Qt::StringLiterals;
41using namespace NotificationManager;
55 int columnCount(
const QModelIndex &
parent = QModelIndex())
const override
65 explicit Private(Notifications *q);
68 void initSourceModels();
69 void initProxyModels();
73 bool showNotifications =
true;
74 bool showJobs =
false;
78 bool expandUnread =
false;
80 int activeNotificationsCount = 0;
81 int expiredNotificationsCount = 0;
83 int unreadNotificationsCount = 0;
84 int dismissedResidentNotificationsCount = 0;
86 int activeJobsCount = 0;
87 int jobsPercentage = 0;
89 static bool isGroup(
const QModelIndex &idx);
90 static uint notificationId(
const QModelIndex &idx);
91 QModelIndex mapFromModel(
const QModelIndex &idx)
const;
94 NotificationsModel::Ptr notificationsModel;
95 JobsModel::Ptr jobsModel;
96 std::shared_ptr<Settings> settings()
const;
98 QConcatenateTablesProxyModel *notificationsAndJobsModel =
nullptr;
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;
109 Notifications *
const q;
117Notifications::Private::~Private()
121void Notifications::Private::initSourceModels()
123 Q_ASSERT(notificationsAndJobsModel);
126 notificationsModel = NotificationsModel::createNotificationsModel();
127 notificationsAndJobsModel->addSourceModel(notificationsModel.get());
128 connect(notificationsModel.get(), &NotificationsModel::lastReadChanged, q, [
this] {
130 Q_EMIT q->lastReadChanged();
133 notificationsAndJobsModel->removeSourceModel(notificationsModel.get());
134 disconnect(notificationsModel.get(),
nullptr, q,
nullptr);
135 notificationsModel =
nullptr;
139 jobsModel = JobsModel::createJobsModel();
140 notificationsAndJobsModel->addSourceModel(jobsModel.get());
142 }
else if (!
showJobs && jobsModel) {
143 notificationsAndJobsModel->removeSourceModel(jobsModel.get());
148void Notifications::Private::initProxyModels()
188 if (!notificationsAndJobsModel) {
189 notificationsAndJobsModel =
new SingleColumnConcatenateTables(q);
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);
201 filterModel->setSourceModel(notificationsAndJobsModel);
211 Q_UNUSED(bottomRight);
221 sortModel =
new NotificationSortProxyModel(q);
222 connect(sortModel, &NotificationSortProxyModel::sortModeChanged, q, &Notifications::sortModeChanged);
223 connect(sortModel, &NotificationSortProxyModel::sortOrderChanged, q, &Notifications::sortOrderChanged);
227 limiterModel =
new LimitedRowCountProxyModel(q);
228 connect(limiterModel, &LimitedRowCountProxyModel::limitChanged, q, &Notifications::limitChanged);
231 if (
groupMode == GroupApplicationsFlat) {
232 if (!groupingModel) {
233 groupingModel =
new NotificationGroupingProxyModel(q);
234 groupingModel->setSourceModel(filterModel);
237 if (!groupCollapsingModel) {
238 groupCollapsingModel =
new NotificationGroupCollapsingProxyModel(q);
241 groupCollapsingModel->setLastRead(q->lastRead());
242 groupCollapsingModel->setSourceModel(groupingModel);
245 sortModel->setSourceModel(groupCollapsingModel);
247 flattenModel =
new KDescendantsProxyModel(q);
248 flattenModel->setSourceModel(sortModel);
250 limiterModel->setSourceModel(flattenModel);
252 sortModel->setSourceModel(filterModel);
253 limiterModel->setSourceModel(sortModel);
255 flattenModel =
nullptr;
256 delete groupingModel;
257 groupingModel =
nullptr;
260 q->setSourceModel(limiterModel);
263void Notifications::Private::updateCount()
268 int dismissedResident = 0;
271 int totalPercentage = 0;
276 for (
int i = 0; i <
filterModel->rowCount(); ++i) {
287 if (!active && !read) {
293 if (notificationsModel && date > notificationsModel->lastRead()) {
313 Q_EMIT q->activeNotificationsCountChanged();
317 Q_EMIT q->expiredNotificationsCountChanged();
321 Q_EMIT q->unreadNotificationsCountChanged();
325 Q_EMIT q->dismissedResidentNotificationsCountChanged();
329 Q_EMIT q->activeJobsCountChanged();
332 const int percentage = (jobs > 0 ? totalPercentage / jobs : 0);
335 Q_EMIT q->jobsPercentageChanged();
342bool Notifications::Private::isGroup(
const QModelIndex &idx)
347uint Notifications::Private::notificationId(
const QModelIndex &idx)
352QModelIndex Notifications::Private::mapFromModel(
const QModelIndex &idx)
const
354 QModelIndex resolvedIdx = idx;
357 notificationsAndJobsModel,
361 groupCollapsingModel,
367 while (resolvedIdx.
isValid() && resolvedIdx.
model() != q) {
368 const auto *idxModel = resolvedIdx.
model();
378 if (proxyModel->sourceModel() == idxModel) {
379 resolvedIdx = proxyModel->mapFromSource(resolvedIdx);
384 if (idxModel == notificationsModel.get() || idxModel == jobsModel.get()) {
385 resolvedIdx = concatenateModel->mapFromSource(resolvedIdx);
399std::shared_ptr<Settings> Notifications::Private::settings()
const
401 static std::weak_ptr<Settings> s_instance;
402 if (!s_instance.expired()) {
403 std::shared_ptr<Settings> ptr(
new Settings());
407 return s_instance.lock();
412 , d(new Private(this))
417 d->initProxyModels();
423 d->initSourceModels();
428Notifications::~Notifications() =
default;
430void Notifications::classBegin()
434void Notifications::componentComplete()
437 d->initSourceModels();
442 return d->limiterModel->limit();
445void Notifications::setLimit(
int limit)
447 d->limiterModel->setLimit(
limit);
452 return d->groupLimit;
455void Notifications::setGroupLimit(
int limit)
457 if (d->groupLimit ==
limit) {
461 d->groupLimit =
limit;
462 if (d->groupCollapsingModel) {
463 d->groupCollapsingModel->setLimit(
limit);
465 Q_EMIT groupLimitChanged();
470 return d->expandUnread;
473void Notifications::setExpandUnread(
bool expand)
475 if (d->expandUnread == expand) {
479 d->expandUnread = expand;
480 if (d->groupCollapsingModel) {
481 d->groupCollapsingModel->setExpandUnread(expand);
483 Q_EMIT expandUnreadChanged();
488 return d->notificationsModel ? d->notificationsModel->window() :
nullptr;
491void Notifications::setWindow(QWindow *window)
493 if (d->notificationsModel) {
494 d->notificationsModel->setWindow(
window);
496 qCWarning(NOTIFICATIONMANAGER) <<
"Setting window before initialising the model" <<
this <<
window;
502 return d->filterModel->showExpired();
505void Notifications::setShowExpired(
bool show)
507 d->filterModel->setShowExpired(show);
512 return d->filterModel->showDismissed();
515void Notifications::setShowDismissed(
bool show)
517 d->filterModel->setShowDismissed(show);
522 return d->filterModel->showAddedDuringInhibition();
525void Notifications::setShowAddedDuringInhibition(
bool show)
527 d->filterModel->setShowAddedDuringInhibition(show);
532 return d->filterModel->blacklistedDesktopEntries();
535void Notifications::setBlacklistedDesktopEntries(
const QStringList &blacklist)
537 d->filterModel->setBlackListedDesktopEntries(blacklist);
542 return d->filterModel->blacklistedNotifyRcNames();
545void Notifications::setBlacklistedNotifyRcNames(
const QStringList &blacklist)
547 d->filterModel->setBlacklistedNotifyRcNames(blacklist);
552 return d->filterModel->whitelistedDesktopEntries();
555void Notifications::setWhitelistedDesktopEntries(
const QStringList &whitelist)
557 d->filterModel->setWhiteListedDesktopEntries(whitelist);
562 return d->filterModel->whitelistedNotifyRcNames();
565void Notifications::setWhitelistedNotifyRcNames(
const QStringList &whitelist)
567 d->filterModel->setWhitelistedNotifyRcNames(whitelist);
572 return d->showNotifications;
575void Notifications::setShowNotifications(
bool show)
577 if (d->showNotifications == show) {
581 d->showNotifications = show;
582 d->initSourceModels();
583 Q_EMIT showNotificationsChanged();
591void Notifications::setShowJobs(
bool show)
593 if (d->showJobs == show) {
598 d->initSourceModels();
604 return d->filterModel->urgencies();
607void Notifications::setUrgencies(Urgencies urgencies)
614 return d->sortModel->sortMode();
617void Notifications::setSortMode(SortMode sortMode)
619 d->sortModel->setSortMode(
sortMode);
624 return d->sortModel->sortOrder();
637void Notifications::setGroupMode(GroupMode groupMode)
641 d->initProxyModels();
642 Q_EMIT groupModeChanged();
648 return rowCount(QModelIndex());
653 return d->activeNotificationsCount;
658 return d->expiredNotificationsCount;
663 if (d->notificationsModel) {
664 return d->notificationsModel->lastRead();
669void Notifications::setLastRead(
const QDateTime &lastRead)
672 if (d->notificationsModel) {
673 d->notificationsModel->setLastRead(
lastRead);
675 if (d->groupCollapsingModel) {
676 d->groupCollapsingModel->setLastRead(
lastRead);
680void Notifications::resetLastRead()
687 return d->unreadNotificationsCount;
692 return d->dismissedResidentNotificationsCount;
697 return d->activeJobsCount;
702 return d->jobsPercentage;
714 d->notificationsModel->expire(Private::notificationId(idx));
717 d->jobsModel->expire(Utils::mapToModel(idx, d->jobsModel.get()));
727 const QModelIndex groupIdx = Utils::mapToModel(idx, d->groupingModel);
729 qCWarning(NOTIFICATIONMANAGER) <<
"Failed to find group model index for this item";
733 Q_ASSERT(groupIdx.
model() == d->groupingModel);
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);
749 d->notificationsModel->close(Private::notificationId(idx));
752 d->jobsModel->close(Utils::mapToModel(idx, d->jobsModel.get()));
761 if (!d->notificationsModel) {
766 if (Private::isGroup(idx)) {
770 d->notificationsModel->configure(desktopEntry, notifyRcName,
QString() );
774 d->notificationsModel->configure(Private::notificationId(idx));
779 if (d->notificationsModel) {
780 d->notificationsModel->invokeDefaultAction(Private::notificationId(idx), behavior);
786 if (d->notificationsModel) {
787 d->notificationsModel->invokeAction(Private::notificationId(idx), actionId, behavior);
793 if (d->notificationsModel) {
794 d->notificationsModel->reply(Private::notificationId(idx), text, behavior);
805 if (d->notificationsModel) {
806 d->notificationsModel->startTimeout(notificationId);
812 if (d->notificationsModel) {
813 d->notificationsModel->stopTimeout(Private::notificationId(idx));
820 d->jobsModel->suspend(Utils::mapToModel(idx, d->jobsModel.get()));
827 d->jobsModel->resume(Utils::mapToModel(idx, d->jobsModel.get()));
834 d->jobsModel->kill(Utils::mapToModel(idx, d->jobsModel.get()));
840 if (d->notificationsModel) {
841 d->notificationsModel->clear(
flags);
844 d->jobsModel->clear(
flags);
855 QModelIndex groupingIdx = Utils::mapToModel(idx, d->groupingModel);
856 return d->mapFromModel(groupingIdx.
parent());
859 qCWarning(NOTIFICATIONMANAGER) <<
"Cannot get group index for item that isn't a group or inside one";
863void Notifications::collapseAllGroups()
865 if (d->groupCollapsingModel) {
866 d->groupCollapsingModel->collapseAll();
873 for (
int i = 0,
count = d->notificationsAndJobsModel->rowCount(); i <
count; ++i) {
874 const QModelIndex idx = d->notificationsAndJobsModel->index(i, 0);
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);
902bool Notifications::filterAcceptsRow(
int source_row,
const QModelIndex &source_parent)
const
907bool Notifications::lessThan(
const QModelIndex &source_left,
const QModelIndex &source_right)
const
912int Notifications::rowCount(
const QModelIndex &parent)
const
917QHash<int, QByteArray> Notifications::roleNames()
const
919 return Utils::roleNames();
922void Notifications::playSoundHint(
const QModelIndex &idx)
const
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();
931 if (isSuppressSound || (soundName.isEmpty() && soundFilePath.isEmpty())) {
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:"
941 d->canberraContext =
nullptr;
946 ca_proplist *props =
nullptr;
947 ca_proplist_create(&props);
950 const KConfigGroup soundGroup = config->group(QStringLiteral(
"Sounds"));
951 const auto soundTheme =
952 soundGroup.
readEntry(
"Theme", QStringLiteral(
"ocean"));
954 if (!soundName.isEmpty()) {
955 ca_proplist_sets(props, CA_PROP_EVENT_ID, soundName.toLatin1().constData());
957 if (!soundFilePath.isEmpty()) {
958 ca_proplist_sets(props, CA_PROP_MEDIA_FILENAME,
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());
969 ca_proplist_sets(props, CA_PROP_CANBERRA_CACHE_CONTROL,
"volatile");
972 ca_context_play_full(d->canberraContext, 0, props,
nullptr,
nullptr);
974 ca_proplist_destroy(props);
976 if (ret != CA_SUCCESS) {
977 qCWarning(NOTIFICATIONMANAGER) <<
"Failed to play sound" << soundName
978 <<
"with canberra:" << ca_strerror(ret);
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.
Type
The type of model item.
@ 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
QVariant data(int role) const const
bool isValid() const const
const QAbstractItemModel * model() const const
QModelIndex parent() const const
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
bool toBool() const const
QDateTime toDateTime() const const
int toInt(bool *ok) const const
QString toString() const const
uint toUInt(bool *ok) const const