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;
85 int activeJobsCount = 0;
86 int jobsPercentage = 0;
88 static bool isGroup(
const QModelIndex &idx);
89 static uint notificationId(
const QModelIndex &idx);
90 QModelIndex mapFromModel(
const QModelIndex &idx)
const;
93 NotificationsModel::Ptr notificationsModel;
94 JobsModel::Ptr jobsModel;
95 std::shared_ptr<Settings> settings()
const;
97 QConcatenateTablesProxyModel *notificationsAndJobsModel =
nullptr;
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;
108 Notifications *
const q;
116Notifications::Private::~Private()
120void Notifications::Private::initSourceModels()
122 Q_ASSERT(notificationsAndJobsModel);
125 notificationsModel = NotificationsModel::createNotificationsModel();
126 notificationsAndJobsModel->addSourceModel(notificationsModel.get());
127 connect(notificationsModel.get(), &NotificationsModel::lastReadChanged, q, [
this] {
129 Q_EMIT q->lastReadChanged();
132 notificationsAndJobsModel->removeSourceModel(notificationsModel.get());
133 disconnect(notificationsModel.get(),
nullptr, q,
nullptr);
134 notificationsModel =
nullptr;
138 jobsModel = JobsModel::createJobsModel();
139 notificationsAndJobsModel->addSourceModel(jobsModel.get());
141 }
else if (!
showJobs && jobsModel) {
142 notificationsAndJobsModel->removeSourceModel(jobsModel.get());
147void Notifications::Private::initProxyModels()
187 if (!notificationsAndJobsModel) {
188 notificationsAndJobsModel =
new SingleColumnConcatenateTables(q);
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);
200 filterModel->setSourceModel(notificationsAndJobsModel);
210 Q_UNUSED(bottomRight);
219 sortModel =
new NotificationSortProxyModel(q);
220 connect(sortModel, &NotificationSortProxyModel::sortModeChanged, q, &Notifications::sortModeChanged);
221 connect(sortModel, &NotificationSortProxyModel::sortOrderChanged, q, &Notifications::sortOrderChanged);
225 limiterModel =
new LimitedRowCountProxyModel(q);
226 connect(limiterModel, &LimitedRowCountProxyModel::limitChanged, q, &Notifications::limitChanged);
229 if (
groupMode == GroupApplicationsFlat) {
230 if (!groupingModel) {
231 groupingModel =
new NotificationGroupingProxyModel(q);
232 groupingModel->setSourceModel(filterModel);
235 if (!groupCollapsingModel) {
236 groupCollapsingModel =
new NotificationGroupCollapsingProxyModel(q);
239 groupCollapsingModel->setLastRead(q->lastRead());
240 groupCollapsingModel->setSourceModel(groupingModel);
243 sortModel->setSourceModel(groupCollapsingModel);
245 flattenModel =
new KDescendantsProxyModel(q);
246 flattenModel->setSourceModel(sortModel);
248 limiterModel->setSourceModel(flattenModel);
250 sortModel->setSourceModel(filterModel);
251 limiterModel->setSourceModel(sortModel);
253 flattenModel =
nullptr;
254 delete groupingModel;
255 groupingModel =
nullptr;
258 q->setSourceModel(limiterModel);
261void Notifications::Private::updateCount()
268 int totalPercentage = 0;
273 for (
int i = 0; i <
filterModel->rowCount(); ++i) {
283 if (!active && !read) {
289 if (notificationsModel && date > notificationsModel->lastRead()) {
305 Q_EMIT q->activeNotificationsCountChanged();
309 Q_EMIT q->expiredNotificationsCountChanged();
313 Q_EMIT q->unreadNotificationsCountChanged();
317 Q_EMIT q->activeJobsCountChanged();
320 const int percentage = (jobs > 0 ? totalPercentage / jobs : 0);
323 Q_EMIT q->jobsPercentageChanged();
330bool Notifications::Private::isGroup(
const QModelIndex &idx)
335uint Notifications::Private::notificationId(
const QModelIndex &idx)
340QModelIndex Notifications::Private::mapFromModel(
const QModelIndex &idx)
const
342 QModelIndex resolvedIdx = idx;
345 notificationsAndJobsModel,
349 groupCollapsingModel,
355 while (resolvedIdx.
isValid() && resolvedIdx.
model() != q) {
356 const auto *idxModel = resolvedIdx.
model();
366 if (proxyModel->sourceModel() == idxModel) {
367 resolvedIdx = proxyModel->mapFromSource(resolvedIdx);
372 if (idxModel == notificationsModel.get() || idxModel == jobsModel.get()) {
373 resolvedIdx = concatenateModel->mapFromSource(resolvedIdx);
387std::shared_ptr<Settings> Notifications::Private::settings()
const
389 static std::weak_ptr<Settings> s_instance;
390 if (!s_instance.expired()) {
391 std::shared_ptr<Settings> ptr(
new Settings());
395 return s_instance.lock();
400 , d(new Private(this))
405 d->initProxyModels();
411 d->initSourceModels();
416Notifications::~Notifications() =
default;
418void Notifications::classBegin()
422void Notifications::componentComplete()
425 d->initSourceModels();
430 return d->limiterModel->limit();
433void Notifications::setLimit(
int limit)
435 d->limiterModel->setLimit(
limit);
440 return d->groupLimit;
443void Notifications::setGroupLimit(
int limit)
445 if (d->groupLimit ==
limit) {
449 d->groupLimit =
limit;
450 if (d->groupCollapsingModel) {
451 d->groupCollapsingModel->setLimit(
limit);
453 Q_EMIT groupLimitChanged();
458 return d->expandUnread;
461void Notifications::setExpandUnread(
bool expand)
463 if (d->expandUnread == expand) {
467 d->expandUnread = expand;
468 if (d->groupCollapsingModel) {
469 d->groupCollapsingModel->setExpandUnread(expand);
471 Q_EMIT expandUnreadChanged();
476 return d->notificationsModel ? d->notificationsModel->window() :
nullptr;
479void Notifications::setWindow(QWindow *window)
481 if (d->notificationsModel) {
482 d->notificationsModel->setWindow(
window);
484 qCWarning(NOTIFICATIONMANAGER) <<
"Setting window before initialising the model" <<
this <<
window;
490 return d->filterModel->showExpired();
493void Notifications::setShowExpired(
bool show)
495 d->filterModel->setShowExpired(show);
500 return d->filterModel->showDismissed();
503void Notifications::setShowDismissed(
bool show)
505 d->filterModel->setShowDismissed(show);
510 return d->filterModel->showAddedDuringInhibition();
513void Notifications::setShowAddedDuringInhibition(
bool show)
515 d->filterModel->setShowAddedDuringInhibition(show);
520 return d->filterModel->blacklistedDesktopEntries();
523void Notifications::setBlacklistedDesktopEntries(
const QStringList &blacklist)
525 d->filterModel->setBlackListedDesktopEntries(blacklist);
530 return d->filterModel->blacklistedNotifyRcNames();
533void Notifications::setBlacklistedNotifyRcNames(
const QStringList &blacklist)
535 d->filterModel->setBlacklistedNotifyRcNames(blacklist);
540 return d->filterModel->whitelistedDesktopEntries();
543void Notifications::setWhitelistedDesktopEntries(
const QStringList &whitelist)
545 d->filterModel->setWhiteListedDesktopEntries(whitelist);
550 return d->filterModel->whitelistedNotifyRcNames();
553void Notifications::setWhitelistedNotifyRcNames(
const QStringList &whitelist)
555 d->filterModel->setWhitelistedNotifyRcNames(whitelist);
560 return d->showNotifications;
563void Notifications::setShowNotifications(
bool show)
565 if (d->showNotifications == show) {
569 d->showNotifications = show;
570 d->initSourceModels();
571 Q_EMIT showNotificationsChanged();
579void Notifications::setShowJobs(
bool show)
581 if (d->showJobs == show) {
586 d->initSourceModels();
592 return d->filterModel->urgencies();
595void Notifications::setUrgencies(Urgencies urgencies)
602 return d->sortModel->sortMode();
605void Notifications::setSortMode(SortMode sortMode)
607 d->sortModel->setSortMode(
sortMode);
612 return d->sortModel->sortOrder();
625void Notifications::setGroupMode(GroupMode groupMode)
629 d->initProxyModels();
630 Q_EMIT groupModeChanged();
636 return rowCount(QModelIndex());
641 return d->activeNotificationsCount;
646 return d->expiredNotificationsCount;
651 if (d->notificationsModel) {
652 return d->notificationsModel->lastRead();
657void Notifications::setLastRead(
const QDateTime &lastRead)
660 if (d->notificationsModel) {
661 d->notificationsModel->setLastRead(
lastRead);
663 if (d->groupCollapsingModel) {
664 d->groupCollapsingModel->setLastRead(
lastRead);
668void Notifications::resetLastRead()
675 return d->unreadNotificationsCount;
680 return d->activeJobsCount;
685 return d->jobsPercentage;
697 d->notificationsModel->expire(Private::notificationId(idx));
700 d->jobsModel->expire(Utils::mapToModel(idx, d->jobsModel.get()));
710 const QModelIndex groupIdx = Utils::mapToModel(idx, d->groupingModel);
712 qCWarning(NOTIFICATIONMANAGER) <<
"Failed to find group model index for this item";
716 Q_ASSERT(groupIdx.
model() == d->groupingModel);
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);
732 d->notificationsModel->close(Private::notificationId(idx));
735 d->jobsModel->close(Utils::mapToModel(idx, d->jobsModel.get()));
744 if (!d->notificationsModel) {
749 if (Private::isGroup(idx)) {
753 d->notificationsModel->configure(desktopEntry, notifyRcName,
QString() );
757 d->notificationsModel->configure(Private::notificationId(idx));
762 if (d->notificationsModel) {
763 d->notificationsModel->invokeDefaultAction(Private::notificationId(idx), behavior);
769 if (d->notificationsModel) {
770 d->notificationsModel->invokeAction(Private::notificationId(idx), actionId, behavior);
776 if (d->notificationsModel) {
777 d->notificationsModel->reply(Private::notificationId(idx), text, behavior);
788 if (d->notificationsModel) {
789 d->notificationsModel->startTimeout(notificationId);
795 if (d->notificationsModel) {
796 d->notificationsModel->stopTimeout(Private::notificationId(idx));
803 d->jobsModel->suspend(Utils::mapToModel(idx, d->jobsModel.get()));
810 d->jobsModel->resume(Utils::mapToModel(idx, d->jobsModel.get()));
817 d->jobsModel->kill(Utils::mapToModel(idx, d->jobsModel.get()));
823 if (d->notificationsModel) {
824 d->notificationsModel->clear(
flags);
827 d->jobsModel->clear(
flags);
838 QModelIndex groupingIdx = Utils::mapToModel(idx, d->groupingModel);
839 return d->mapFromModel(groupingIdx.
parent());
842 qCWarning(NOTIFICATIONMANAGER) <<
"Cannot get group index for item that isn't a group or inside one";
846void Notifications::collapseAllGroups()
848 if (d->groupCollapsingModel) {
849 d->groupCollapsingModel->collapseAll();
856 for (
int i = 0,
count = d->notificationsAndJobsModel->rowCount(); i <
count; ++i) {
857 const QModelIndex idx = d->notificationsAndJobsModel->index(i, 0);
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);
885bool Notifications::filterAcceptsRow(
int source_row,
const QModelIndex &source_parent)
const
890bool Notifications::lessThan(
const QModelIndex &source_left,
const QModelIndex &source_right)
const
895int Notifications::rowCount(
const QModelIndex &parent)
const
900QHash<int, QByteArray> Notifications::roleNames()
const
902 return Utils::roleNames();
905void Notifications::playSoundHint(
const QModelIndex &idx)
const
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();
914 if (isSuppressSound || (soundName.isEmpty() && soundFilePath.isEmpty())) {
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:"
924 d->canberraContext =
nullptr;
929 ca_proplist *props =
nullptr;
930 ca_proplist_create(&props);
933 const KConfigGroup soundGroup = config->group(QStringLiteral(
"Sounds"));
934 const auto soundTheme =
935 soundGroup.
readEntry(
"Theme", QStringLiteral(
"ocean"));
937 if (!soundName.isEmpty()) {
938 ca_proplist_sets(props, CA_PROP_EVENT_ID, soundName.toLatin1().constData());
940 if (!soundFilePath.isEmpty()) {
941 ca_proplist_sets(props, CA_PROP_MEDIA_FILENAME,
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());
952 ca_proplist_sets(props, CA_PROP_CANBERRA_CACHE_CONTROL,
"volatile");
955 ca_context_play_full(d->canberraContext, 0, props,
nullptr,
nullptr);
957 ca_proplist_destroy(props);
959 if (ret != CA_SUCCESS) {
960 qCWarning(NOTIFICATIONMANAGER) <<
"Failed to play sound" << soundName
961 <<
"with canberra:" << ca_strerror(ret);
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.
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 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
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