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 #pragma once
8 
9 #include <QQmlParserStatus>
10 #include <QSortFilterProxyModel>
11 
12 #include <QScopedPointer>
13 
14 #include "notificationmanager_export.h"
15 
16 namespace NotificationManager
17 {
18 /**
19  * @brief A model with notifications and jobs
20  *
21  * This model contains application notifications as well as jobs
22  * and lets you apply fine-grained filter, sorting, and grouping rules.
23  *
24  * @author Kai Uwe Broulik <kde@privat.broulik.de>
25  **/
26 class NOTIFICATIONMANAGER_EXPORT Notifications : public QSortFilterProxyModel, public QQmlParserStatus
27 {
28     Q_OBJECT
29     Q_INTERFACES(QQmlParserStatus)
30 
31     /**
32      * The number of notifications the model should at most contain.
33      *
34      * Default is 0, which is no limit.
35      */
36     Q_PROPERTY(int limit READ limit WRITE setLimit NOTIFY limitChanged)
37 
38     /**
39      * Whether to show expired notifications.
40      *
41      * Expired notifications are those that timed out, i.e. ones that were not explicitly
42      * closed or acted upon by the user, nor revoked by the issuing application.
43      *
44      * An expired notification has its actions removed.
45      *
46      * Default is false.
47      */
48     Q_PROPERTY(bool showExpired READ showExpired WRITE setShowExpired NOTIFY showExpiredChanged)
49 
50     /**
51      * Whether to show dismissed notifications.
52      *
53      * Dismissed notifications are those that are temporarily hidden by the user.
54      * This can e.g. be a copy job that has its popup closed but still continues in the background.
55      *
56      * Default is false.
57      */
58     Q_PROPERTY(bool showDismissed READ showDismissed WRITE setShowDismissed NOTIFY showDismissedChanged)
59 
60     /**
61      * A list of desktop entries for which no notifications should be shown.
62      *
63      * If the same desktop entry is present in both blacklist and whitelist,
64      * the blacklist takes precedence, i.e. the notification is not shown.
65      */
66     Q_PROPERTY(QStringList blacklistedDesktopEntries READ blacklistedDesktopEntries WRITE setBlacklistedDesktopEntries NOTIFY blacklistedDesktopEntriesChanged)
67 
68     /**
69      * A list of notifyrc names for which no notifications should be shown.
70      *
71      * If the same notifyrc name is present in both blacklist and whitelist,
72      * the blacklist takes precedence, i.e. the notification is not shown.
73      */
74     Q_PROPERTY(QStringList blacklistedNotifyRcNames READ blacklistedNotifyRcNames WRITE setBlacklistedNotifyRcNames NOTIFY blacklistedNotifyRcNamesChanged)
75 
76     /**
77      * A list of desktop entries for which notifications should be shown.
78      *
79      * This bypasses any filtering for urgency.
80      *
81      * If the same desktop entry is present in both whitelist and blacklist,
82      * the blacklist takes precedence, i.e. the notification is not shown.
83      *
84      * Default is empty list, which means normal filtering is applied.
85      */
86     Q_PROPERTY(QStringList whitelistedDesktopEntries READ whitelistedDesktopEntries WRITE setWhitelistedDesktopEntries NOTIFY whitelistedDesktopEntriesChanged)
87 
88     /**
89      * A list of notifyrc names for which notifications should be shown.
90      *
91      * This bypasses any filtering for urgency.
92      *
93      * If the same notifyrc name is present in both whitelist and blacklist,
94      * the blacklist takes precedence, i.e. the notification is not shown.
95      *
96      * Default is empty list, which means normal filtering is applied.
97      */
98     Q_PROPERTY(QStringList whitelistedNotifyRcNames READ whitelistedNotifyRcNames WRITE setWhitelistedNotifyRcNames NOTIFY whitelistedNotifyRcNamesChanged)
99 
100     /**
101      * Whether to show notifications.
102      *
103      * Default is true.
104      */
105     Q_PROPERTY(bool showNotifications READ showNotifications WRITE setShowNotifications NOTIFY showNotificationsChanged)
106 
107     /**
108      * Whether to show application jobs.
109      *
110      * Default is false.
111      */
112     Q_PROPERTY(bool showJobs READ showJobs WRITE setShowJobs NOTIFY showJobsChanged)
113 
114     /**
115      * The notification urgency types the model should contain.
116      *
117      * Default is all urgencies: low, normal, critical.
118      */
119     Q_PROPERTY(Urgencies urgencies READ urgencies WRITE setUrgencies NOTIFY urgenciesChanged)
120 
121     /**
122      * The sort mode for notifications.
123      *
124      * Default is strictly by date created/updated.
125      */
126     Q_PROPERTY(SortMode sortMode READ sortMode WRITE setSortMode NOTIFY sortModeChanged)
127 
128     /**
129      * The sort order for notifications.
130      *
131      * This only affects the sort order by date. When @c sortMode is set to SortByTypeAndUrgency
132      * the order of notification groups (e.g. high - jobs - normal - low) is unaffected, and only
133      * notifications within the same group are either sorted ascending or descending by their
134      * creation/update date.
135      *
136      * Default is DescendingOrder, i.e. newest notifications come first.
137      *
138      * @since 5.19
139      */
140     Q_PROPERTY(Qt::SortOrder sortOrder READ sortOrder WRITE setSortOrder NOTIFY sortOrderChanged)
141 
142     /**
143      * The group mode for notifications.
144      *
145      * Default is ungrouped.
146      */
147     Q_PROPERTY(GroupMode groupMode READ groupMode WRITE setGroupMode NOTIFY groupModeChanged)
148 
149     /**
150      * How many notifications are shown in each group.
151      *
152      * You can expand a group by setting the IsGroupExpandedRole to true.
153      *
154      * Default is 0, which means no limit.
155      */
156     Q_PROPERTY(int groupLimit READ groupLimit WRITE setGroupLimit NOTIFY groupLimitChanged)
157 
158     /**
159      * Whether to automatically show notifications that are unread.
160      *
161      * This is any notification that was created or updated after the value of @c lastRead.
162      */
163     Q_PROPERTY(bool expandUnread READ expandUnread WRITE setExpandUnread NOTIFY expandUnreadChanged)
164 
165     /**
166      * The number of notifications in the model
167      */
168     Q_PROPERTY(int count READ count NOTIFY countChanged)
169 
170     /**
171      * The number of active, i.e. non-expired notifications
172      */
173     Q_PROPERTY(int activeNotificationsCount READ activeNotificationsCount NOTIFY activeNotificationsCountChanged)
174 
175     /**
176      * The number of inactive, i.e. non-expired notifications
177      */
178     Q_PROPERTY(int expiredNotificationsCount READ expiredNotificationsCount NOTIFY expiredNotificationsCountChanged)
179 
180     /**
181      * The time when the user last could read the notifications.
182      * This is typically reset whenever the list of notifications is opened and is used to determine
183      * the @c unreadNotificationsCount
184      */
185     Q_PROPERTY(QDateTime lastRead READ lastRead WRITE setLastRead RESET resetLastRead NOTIFY lastReadChanged)
186 
187     /**
188      * The number of notifications added since lastRead
189      *
190      * This can be used to show a "n unread notifications" label
191      */
192     Q_PROPERTY(int unreadNotificationsCount READ unreadNotificationsCount NOTIFY unreadNotificationsCountChanged)
193 
194     /**
195      * The number of active jobs
196      */
197     Q_PROPERTY(int activeJobsCount READ activeJobsCount NOTIFY activeJobsCountChanged)
198     /**
199      * The combined percentage of all jobs.
200      *
201      * This is the average of all percentages and could can be used to show
202      * a global progress bar.
203      */
204     Q_PROPERTY(int jobsPercentage READ jobsPercentage NOTIFY jobsPercentageChanged)
205 
206 public:
207     explicit Notifications(QObject *parent = nullptr);
208     ~Notifications() override;
209 
210     enum Roles {
211         IdRole = Qt::UserRole + 1, ///< A notification identifier. This can be uint notification ID or string application job source.
212         SummaryRole = Qt::DisplayRole, ///< The notification summary.
213         ImageRole = Qt::DecorationRole, ///< The notification main image, which is not the application icon. Only valid for pixmap icons.
214 
215         IsGroupRole = Qt::UserRole + 2, ///< Whether the item is a group
216         GroupChildrenCountRole, ///< The number of children in a group.
217         ExpandedGroupChildrenCountRole, ///< The number of children in a group that are expanded.
218         IsGroupExpandedRole, ///< Whether the group is expanded, this role is writable.
219 
220         IsInGroupRole, ///< Whether the notification is currently inside a group.
221         TypeRole, ///< The type of model entry, either NotificationType or JobType.
222         CreatedRole, ///< When the notification was first created.
223         UpdatedRole, ///< When the notification was last updated, invalid when it hasn't been updated.
224 
225         BodyRole, ///< The notification body text.
226         IconNameRole, ///< The notification main icon name, which is not the application icon. Only valid for icon names, if a URL supplied, it is loaded and
227                       ///< exposed as ImageRole instead.
228 
229         DesktopEntryRole, ///< The desktop entry (without .desktop suffix, e.g. org.kde.spectacle) of the application that sent the notification.
230         NotifyRcNameRole, ///< The notifyrc name (e.g. spectaclerc) of the application that sent the notification.
231 
232         ApplicationNameRole, ///< The user-visible name of the application (e.g. Spectacle)
233         ApplicationIconNameRole, ///< The icon name of the application
234         OriginNameRole, ///< The name of the device or account the notification originally came from, e.g. "My Phone" (in case of device sync) or
235                         ///< "foo@example.com" (in case of an email notification)
236 
237         // Jobs
238         JobStateRole, ///< The state of the job, either JobStateJopped, JobStateSuspended, or JobStateRunning.
239         PercentageRole, ///< The percentage of the job. Use @c jobsPercentage to get a global percentage for all jobs.
240         JobErrorRole, ///< The error id of the job, zero in case of no error.
241         SuspendableRole, ///< Whether the job can be suspended @sa suspendJob
242         KillableRole, ///< Whether the job can be killed/canceled @sa killJob
243         JobDetailsRole, ///< A pointer to a Job item itself containing more detailed information about the job
244 
245         ActionNamesRole, ///< The IDs of the actions, excluding the default and settings action, e.g. [action1, action2]
246         ActionLabelsRole, ///< The user-visible labels of the actions, excluding the default and settings action, e.g. ["Accept", "Reject"]
247         HasDefaultActionRole, ///< Whether the notification has a default action, which is one that is invoked when the popup itself is clicked
248         DefaultActionLabelRole, ///< The user-visible label of the default action, typically not shown as the popup itself becomes clickable
249 
250         UrlsRole, ///< A list of URLs associated with the notification, e.g. a path to a screenshot that was just taken or image received
251 
252         UrgencyRole, ///< The notification urgency, either LowUrgency, NormalUrgency, or CriticalUrgency. Jobs do not have an urgency.
253         TimeoutRole, ///< The timeout for the notification in milliseconds. 0 means the notification should not timeout, -1 means a sensible default should be
254                      ///< applied.
255 
256         ConfigurableRole, ///< Whether the notification can be configured because a desktopEntry or notifyRcName is known, or the notification has a setting
257                           ///< action. @sa configure
258         ConfigureActionLabelRole, ///< The user-visible label for the settings action
259         ClosableRole, ///< Whether the item can be closed. Notifications are always closable, jobs are only when in JobStateStopped.
260 
261         ExpiredRole, ///< The notification timed out and closed. Actions on it cannot be invoked anymore.
262         DismissedRole, ///< The notification got temporarily hidden by the user but could still be interacted with.
263         ReadRole, ///< Whether the notification got read by the user. If true, the notification isn't considered unread even if created after lastRead.
264                   ///< @since 5.17
265 
266         UserActionFeedbackRole, ///< Whether this notification is a response/confirmation to an explicit user action. @since 5.18
267 
268         HasReplyActionRole, ///< Whether the notification has a reply action. @since 5.18
269         ReplyActionLabelRole, ///< The user-visible label for the reply action. @since 5.18
270         ReplyPlaceholderTextRole, ///< A custom placeholder text for the reply action, e.g. "Reply to Max...". @since 5.18
271         ReplySubmitButtonTextRole, ///< A custom text for the reply submit button, e.g. "Submit Comment". @since 5.18
272         ReplySubmitButtonIconNameRole, ///< A custom icon name for the reply submit button. @since 5.18
273         CategoryRole, ///< The (optional) category of the notification. Notifications can optionally have a type indicator. Although neither client or nor
274                       ///< server must support this, some may choose to. Those servers implementing categories may use them to intelligently display the
275                       ///< notification in a certain way, or group notifications of similar types.  @since 5.21
276         ResidentRole, ///< Whether the notification should keep its actions even when they were invoked. @since 5.22
277         TransientRole, ///< Whether the notification is transient and should not be kept in history. @since 5.22
278     };
279     Q_ENUM(Roles)
280 
281     /**
282      * The type of model item.
283      */
284     enum Type {
285         NoType,
286         NotificationType, ///< This item represents a notification.
287         JobType, ///< This item represents an application job.
288     };
289     Q_ENUM(Type)
290 
291     /**
292      * The notification urgency.
293      *
294      * @note jobs do not have an urgency, yet still might be above normal urgency notifications.
295      */
296     enum Urgency {
297         // these don't match the spec's value
298         LowUrgency = 1 << 0, ///< The notification has low urgency, it is not important and may not be shown or added to a history.
299         NormalUrgency = 1 << 1, ///< The notification has normal urgency. This is also the default if no urgecny is supplied.
300         CriticalUrgency = 1 << 2,
301     };
302     Q_ENUM(Urgency)
303     Q_DECLARE_FLAGS(Urgencies, Urgency)
304     Q_FLAG(Urgencies)
305 
306     /**
307      * Which items should be cleared in a call to @c clear
308      */
309     enum ClearFlag {
310         ClearExpired = 1 << 1,
311         // TODO more
312     };
313     Q_ENUM(ClearFlag)
314     Q_DECLARE_FLAGS(ClearFlags, ClearFlag)
315     Q_FLAG(ClearFlags)
316 
317     /**
318      * The state an application job is in.
319      */
320     enum JobState {
321         JobStateStopped, ///< The job is stopped. It has either finished (error is 0) or failed (error is not 0)
322         JobStateRunning, ///< The job is currently running.
323         JobStateSuspended, ///< The job is currentl paused
324     };
325     Q_ENUM(JobState)
326 
327     /**
328      * The sort mode for the model.
329      */
330     enum SortMode {
331         SortByDate = 0, ///< Sort notifications strictly by the date they were updated or created.
332         // should this be flags? SortJobsFirst | SortByUrgency | ...?
333         SortByTypeAndUrgency, ///< Sort notifications taking into account their type and urgency. The order is (descending): Critical, jobs, Normal, Low.
334     };
335     Q_ENUM(SortMode)
336 
337     /**
338      * The group mode for the model.
339      */
340     enum GroupMode {
341         GroupDisabled = 0,
342         // GroupApplicationsTree, // TODO make actual tree
343         GroupApplicationsFlat,
344     };
345     Q_ENUM(GroupMode)
346 
347     int limit() const;
348     void setLimit(int limit);
349 
350     bool showExpired() const;
351     void setShowExpired(bool show);
352 
353     bool showDismissed() const;
354     void setShowDismissed(bool show);
355 
356     QStringList blacklistedDesktopEntries() const;
357     void setBlacklistedDesktopEntries(const QStringList &blacklist);
358 
359     QStringList blacklistedNotifyRcNames() const;
360     void setBlacklistedNotifyRcNames(const QStringList &blacklist);
361 
362     QStringList whitelistedDesktopEntries() const;
363     void setWhitelistedDesktopEntries(const QStringList &whitelist);
364 
365     QStringList whitelistedNotifyRcNames() const;
366     void setWhitelistedNotifyRcNames(const QStringList &whitelist);
367 
368     bool showNotifications() const;
369     void setShowNotifications(bool showNotifications);
370 
371     bool showJobs() const;
372     void setShowJobs(bool showJobs);
373 
374     Urgencies urgencies() const;
375     void setUrgencies(Urgencies urgencies);
376 
377     SortMode sortMode() const;
378     void setSortMode(SortMode sortMode);
379 
380     Qt::SortOrder sortOrder() const;
381     void setSortOrder(Qt::SortOrder sortOrder);
382 
383     GroupMode groupMode() const;
384     void setGroupMode(GroupMode groupMode);
385 
386     int groupLimit() const;
387     void setGroupLimit(int limit);
388 
389     bool expandUnread() const;
390     void setExpandUnread(bool expand);
391 
392     int count() const;
393 
394     int activeNotificationsCount() const;
395     int expiredNotificationsCount() const;
396 
397     QDateTime lastRead() const;
398     void setLastRead(const QDateTime &lastRead);
399     void resetLastRead();
400 
401     int unreadNotificationsCount() const;
402 
403     int activeJobsCount() const;
404     int jobsPercentage() const;
405 
406     /**
407      * Convert the given QModelIndex into a QPersistentModelIndex
408      */
409     Q_INVOKABLE QPersistentModelIndex makePersistentModelIndex(const QModelIndex &idx) const;
410 
411     /**
412      * @brief Expire a notification
413      *
414      * Closes the notification in response to its timeout running out.
415      *
416      * Call this if you have an implementation that handles the timeout itself
417      * by having called @c stopTimeout
418      *
419      * @sa stopTimeout
420      */
421     Q_INVOKABLE void expire(const QModelIndex &idx);
422     /**
423      * @brief Close a notification
424      *
425      * Closes the notification in response to the user explicitly closing it.
426      *
427      * When the model index belongs to a group, the entire group is closed.
428      */
429     Q_INVOKABLE void close(const QModelIndex &idx);
430     /**
431      * @brief Configure a notification
432      *
433      * This will invoke the settings action, if available, otherwise open the
434      * kcm_notifications KCM for configuring the respective application and event.
435      */
436     Q_INVOKABLE void configure(const QModelIndex &idx); // TODO pass ctx for transient handling
437     /**
438      * @brief Invoke the default notification action
439      *
440      * Invokes the action that should be triggered when clicking
441      * the notification bubble itself.
442      */
443     Q_INVOKABLE void invokeDefaultAction(const QModelIndex &idx);
444     /**
445      * @brief Invoke a notification action
446      *
447      * Invokes the action with the given actionId on the notification.
448      * For invoking the default action, i.e. the one that is triggered
449      * when clicking the notification bubble, use invokeDefaultAction
450      */
451     Q_INVOKABLE void invokeAction(const QModelIndex &idx, const QString &actionId);
452 
453     /**
454      * @brief Reply to a notification
455      *
456      * Replies to the given notification with the given text.
457      * @since 5.18
458      */
459     Q_INVOKABLE void reply(const QModelIndex &idx, const QString &text);
460 
461     /**
462      * @brief Start automatic timeout of notifications
463      *
464      * Call this if you no longer handle the timeout yourself.
465      *
466      * @sa stopTimeout
467      */
468     Q_INVOKABLE void startTimeout(const QModelIndex &idx);
469 
470     Q_INVOKABLE void startTimeout(uint notificationId);
471     /**
472      * @brief Stop the automatic timeout of notifications
473      *
474      * Call this if you have an implementation that handles the timeout itself
475      * taking into account e.g. whether the user is currently interacting with
476      * the notification to not close it under their mouse. Call @c expire
477      * once your custom timer has run out.
478      *
479      * @sa expire
480      */
481     Q_INVOKABLE void stopTimeout(const QModelIndex &idx);
482 
483     /**
484      * @brief Suspend a job
485      */
486     Q_INVOKABLE void suspendJob(const QModelIndex &idx);
487     /**
488      * @brief Resume a job
489      */
490     Q_INVOKABLE void resumeJob(const QModelIndex &idx);
491     /**
492      * @brief Kill a job
493      */
494     Q_INVOKABLE void killJob(const QModelIndex &idx);
495 
496     /**
497      * @brief Clear notifications
498      *
499      * Removes the notifications matching th ClearFlags from the model.
500      * This can be used for e.g. a "Clear History" action.
501      */
502     Q_INVOKABLE void clear(ClearFlags flags);
503 
504     /**
505      * Returns a model index pointing to the group of a notification.
506      */
507     Q_INVOKABLE QModelIndex groupIndex(const QModelIndex &idx) const;
508 
509     Q_INVOKABLE void collapseAllGroups();
510 
511     QVariant data(const QModelIndex &index, int role) const override;
512     bool setData(const QModelIndex &index, const QVariant &value, int role) override;
513     int rowCount(const QModelIndex &parent = QModelIndex()) const override;
514     QHash<int, QByteArray> roleNames() const override;
515 
516     bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override;
517     bool lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const override;
518 
519 Q_SIGNALS:
520     void limitChanged();
521     void showExpiredChanged();
522     void showDismissedChanged();
523     void blacklistedDesktopEntriesChanged();
524     void blacklistedNotifyRcNamesChanged();
525     void whitelistedDesktopEntriesChanged();
526     void whitelistedNotifyRcNamesChanged();
527     void showNotificationsChanged();
528     void showJobsChanged();
529     void urgenciesChanged();
530     void sortModeChanged();
531     void sortOrderChanged();
532     void groupModeChanged();
533     void groupLimitChanged();
534     void expandUnreadChanged();
535     void countChanged();
536     void activeNotificationsCountChanged();
537     void expiredNotificationsCountChanged();
538     void lastReadChanged();
539     void unreadNotificationsCountChanged();
540     void activeJobsCountChanged();
541     void jobsPercentageChanged();
542 
543 protected:
544     void classBegin() override;
545     void componentComplete() override;
546 
547 private:
548     class Private;
549     QScopedPointer<Private> d;
550 };
551 
552 } // namespace NotificationManager
553 
554 Q_DECLARE_OPERATORS_FOR_FLAGS(NotificationManager::Notifications::Urgencies)
555