1 /*
2 * messagenotification.cpp - displays an alarm message in a system notification
3 * Program: kalarm
4 * SPDX-FileCopyrightText: 2020-2022 David Jarvie <djarvie@kde.org>
5 *
6 * SPDX-License-Identifier: GPL-2.0-or-later
7 */
8
9 #include "messagenotification.h"
10 #include "messagedisplayhelper.h"
11
12 #include "kalarmapp.h"
13 #include "mainwindow.h"
14 #include "preferences.h"
15 #include "resourcescalendar.h"
16 #include "lib/file.h"
17 #include "kalarm_debug.h"
18
19 #include <KAboutData>
20 #include <KLocalizedString>
21 #include <KIdleTime>
22 #ifdef RESTORE_NOTIFICATIONS
23 #include <KConfigGroup>
24 #include <KConfigGui>
25 #endif
26
27 #include <QSessionManager>
28
29 using namespace KAlarmCal;
30
31 //clazy:excludeall=non-pod-global-static
32
33 namespace
34 {
35
36 // Notification eventIds: these are the IDs contained in the '[Event/ID]'
37 // entries in kalarm.notifyrc.
38 const QString MessageId = QStringLiteral("Message");
39 const QString BeepId = QStringLiteral("MessageBeep");
40 const QString SpeakId = QStringLiteral("MessageSpeak");
41 const QString ErrorId = QStringLiteral("MessageError");
42
43 // Flags for the notification
44 const KNotification::NotificationFlags NFLAGS = KNotification::RaiseWidgetOnActivation;
45
46 const QString NL = QStringLiteral("\n");
47 const QString SP = QStringLiteral(" ");
48
getNotifyEventId(const KAEvent & event)49 inline QString getNotifyEventId(const KAEvent& event)
50 {
51 return event.beep() ? BeepId : event.speak() ? SpeakId : MessageId;
52 }
53
54 } // namespace
55
56
57 /*=============================================================================
58 * Helper class to save all message notifications' properties on session
59 * shutdown, to enable them to be recreated on the next startup.
60 *
61 * NOTE: When a notification has closed, there is currently no way to know
62 * whether it has been closed by the user or has timed out. There is also
63 * no way to know when a notification in the notification history is
64 * closed by the user. So notifications are not restored on startup, since
65 * that might re-raise notifications which the user has already closed.
66 * If this changes in the future, notifications could be restored on
67 * startup, in the same way as alarm windows are restored.
68 */
69 class MNSessionManager : public QObject
70 {
71 Q_OBJECT
72 public:
MNSessionManager()73 MNSessionManager()
74 {
75 #ifdef RESTORE_NOTIFICATIONS
76 connect(qApp, &QGuiApplication::saveStateRequest, this, &MNSessionManager::saveState);
77 #endif
78 }
~MNSessionManager()79 ~MNSessionManager() override {}
80
create()81 static void create()
82 {
83 if (!mInstance)
84 mInstance = new MNSessionManager;
85 }
86
87 private Q_SLOTS:
88 #ifdef RESTORE_NOTIFICATIONS
89 /******************************************************************************
90 * Called by the session manager to request the application to save its state.
91 */
saveState(QSessionManager & sm)92 void saveState(QSessionManager& sm)
93 {
94 KConfigGui::setSessionConfig(sm.sessionId(), sm.sessionKey());
95 KConfig* config = KConfigGui::sessionConfig();
96 // Save each MessageNotification's data.
97 int n = 1;
98 for (MessageNotification* notif : std::as_const(MessageNotification::mNotificationList))
99 {
100 const QByteArray group = "Notification_" + QByteArray::number(++n);
101 KConfigGroup cg(config, group.constData());
102 notif->saveProperties(cg);
103 }
104 KConfigGroup cg(config, "Number");
105 cg.writeEntry("NumberOfNotifications", MessageNotification::mNotificationList.count());
106 }
107 #endif
108
109 private:
110 static MNSessionManager* mInstance;
111 };
112
113 MNSessionManager* MNSessionManager::mInstance = nullptr;
114
115
116 QVector<MessageNotification*> MessageNotification::mNotificationList;
117
118 /******************************************************************************
119 * Restore MessageNotification instances saved at session shutdown.
120 */
sessionRestore()121 void MessageNotification::sessionRestore()
122 {
123 #ifdef RESTORE_NOTIFICATIONS
124 KConfig* config = KConfigGui::sessionConfig();
125 if (config)
126 {
127 KConfigGroup cg(config, "Number");
128 const int count = cg.readEntry("NumberOfNotifications", 0);
129 for (int n = 1; n <= count; ++n)
130 {
131 const QByteArray group = "Notification_" + QByteArray::number(n);
132 cg = KConfigGroup(config, group.constData());
133 // Have to initialise the MessageNotification instance with its
134 // eventId already known. So first create a helper, then read
135 // its properties, and finally create the MessageNotification.
136 MessageDisplayHelper* helper = new MessageDisplayHelper(nullptr);
137 if (!helper->readPropertyValues(cg))
138 delete helper;
139 else
140 {
141 const QString notifyId = cg.readEntry("NotifyId");
142 new MessageNotification(notifyId, helper);
143 }
144 }
145 }
146 #endif
147 }
148
149 /******************************************************************************
150 * Construct the message notification for the specified alarm.
151 * Other alarms in the supplied event may have been updated by the caller, so
152 * the whole event needs to be stored for updating the calendar file when it is
153 * displayed.
154 */
MessageNotification(const KAEvent & event,const KAAlarm & alarm,int flags)155 MessageNotification::MessageNotification(const KAEvent& event, const KAAlarm& alarm, int flags)
156 : KNotification(getNotifyEventId(event), NFLAGS)
157 , MessageDisplay(event, alarm, flags)
158 {
159 qCDebug(KALARM_LOG) << "MessageNotification():" << mEventId();
160 MNSessionManager::create();
161 setWidget(MainWindow::mainMainWindow());
162 if (!(flags & NoInitView))
163 MessageNotification::setUpDisplay(); // avoid calling virtual method from constructor
164
165 connect(this, qOverload<unsigned int>(&KNotification::activated), this, &MessageNotification::buttonActivated);
166 connect(this, &KNotification::closed, this, &MessageNotification::slotClosed);
167 connect(mHelper, &MessageDisplayHelper::textsChanged, this, &MessageNotification::textsChanged);
168 connect(mHelper, &MessageDisplayHelper::commandExited, this, &MessageNotification::commandCompleted);
169
170 mNotificationList.append(this);
171 }
172
173 /******************************************************************************
174 * Construct the message notification for a specified error message.
175 * If 'dontShowAgain' is non-null, a "Don't show again" option is displayed. Note
176 * that the option is specific to 'event'.
177 */
MessageNotification(const KAEvent & event,const DateTime & alarmDateTime,const QStringList & errmsgs,const QString & dontShowAgain)178 MessageNotification::MessageNotification(const KAEvent& event, const DateTime& alarmDateTime,
179 const QStringList& errmsgs, const QString& dontShowAgain)
180 : KNotification(ErrorId, NFLAGS)
181 , MessageDisplay(event, alarmDateTime, errmsgs, dontShowAgain)
182 {
183 qCDebug(KALARM_LOG) << "MessageNotification(errmsg)";
184 MNSessionManager::create();
185 setWidget(MainWindow::mainMainWindow());
186 MessageNotification::setUpDisplay(); // avoid calling virtual method from constructor
187
188 connect(this, qOverload<unsigned int>(&KNotification::activated), this, &MessageNotification::buttonActivated);
189 connect(this, &KNotification::closed, this, &MessageNotification::slotClosed);
190 connect(mHelper, &MessageDisplayHelper::textsChanged, this, &MessageNotification::textsChanged);
191
192 mNotificationList.append(this);
193 }
194
195 /******************************************************************************
196 * Construct the message notification from the properties contained in the
197 * supplied helper.
198 * Ownership of the helper is taken by the new instance.
199 */
MessageNotification(const QString & eventId,MessageDisplayHelper * helper)200 MessageNotification::MessageNotification(const QString& eventId, MessageDisplayHelper* helper)
201 : KNotification(eventId, NFLAGS)
202 , MessageDisplay(helper)
203 {
204 qCDebug(KALARM_LOG) << "MessageNotification(helper):" << mEventId();
205 MNSessionManager::create();
206 setWidget(MainWindow::mainMainWindow());
207
208 connect(this, qOverload<unsigned int>(&KNotification::activated), this, &MessageNotification::buttonActivated);
209 connect(this, &KNotification::closed, this, &MessageNotification::slotClosed);
210 connect(mHelper, &MessageDisplayHelper::textsChanged, this, &MessageNotification::textsChanged);
211 connect(mHelper, &MessageDisplayHelper::commandExited, this, &MessageNotification::commandCompleted);
212
213 mNotificationList.append(this);
214 helper->processPropertyValues();
215 }
216
217 /******************************************************************************
218 * Destructor. Perform any post-alarm actions before tidying up.
219 */
~MessageNotification()220 MessageNotification::~MessageNotification()
221 {
222 qCDebug(KALARM_LOG) << "~MessageNotification" << mEventId();
223 close();
224 mNotificationList.removeAll(this);
225 }
226
227 /******************************************************************************
228 * Construct the message notification.
229 */
setUpDisplay()230 void MessageNotification::setUpDisplay()
231 {
232 mHelper->initTexts();
233 MessageDisplayHelper::DisplayTexts texts = mHelper->texts();
234
235 setNotificationTitle(texts.title);
236
237 // Show the alarm date/time. Any reminder indication is shown in the
238 // notification title.
239 // Alarm date/time: display time zone if not local time zone.
240 mTimeText = texts.time;
241
242 mMessageText.clear();
243 if (!mErrorWindow())
244 {
245 // It's a normal alarm message notification
246 switch (mAction())
247 {
248 case KAEvent::FILE:
249 // Display the file name
250 mMessageText = texts.fileName + NL;
251
252 if (mErrorMsgs().isEmpty())
253 {
254 // Display contents of file
255 switch (texts.fileType)
256 {
257 case File::Image:
258 break; // can't display an image
259 case File::TextFormatted:
260 default:
261 mMessageText += texts.message;
262 break;
263 }
264 }
265 break;
266
267 case KAEvent::MESSAGE:
268 mMessageText = texts.message;
269 break;
270
271 case KAEvent::COMMAND:
272 mMessageText = texts.message;
273 mCommandInhibit = true;
274 break;
275
276 case KAEvent::EMAIL:
277 default:
278 break;
279 }
280
281 if (!texts.remainingTime.isEmpty())
282 {
283 // Advance reminder: show remaining time until the actual alarm
284 mRemainingText = texts.remainingTime;
285 }
286 }
287 else
288 {
289 // It's an error message
290 switch (mAction())
291 {
292 case KAEvent::EMAIL:
293 {
294 // Display the email addresses and subject.
295 mMessageText = texts.errorEmail[0] + SP + texts.errorEmail[1] + NL
296 + texts.errorEmail[2] + SP + texts.errorEmail[3] + NL;
297 break;
298 }
299 case KAEvent::COMMAND:
300 case KAEvent::FILE:
301 case KAEvent::MESSAGE:
302 default:
303 // Just display the error message strings
304 break;
305 }
306 }
307
308 if (!mErrorMsgs().isEmpty())
309 {
310 setIconName(QStringLiteral("dialog-error"));
311 mMessageText += mErrorMsgs().join(NL);
312 mCommandInhibit = false;
313 }
314
315 setNotificationText();
316
317 mEnableEdit = mShowEdit();
318 if (!mNoDefer())
319 {
320 mEnableDefer = true;
321 mHelper->setDeferralLimit(mEvent()); // ensure that button is disabled when alarm can't be deferred any more
322 }
323 setNotificationButtons();
324
325 mInitialised = true; // the notification's widgets have been created
326 }
327
328 /******************************************************************************
329 * Return the number of message notifications.
330 */
notificationCount()331 int MessageNotification::notificationCount()
332 {
333 return mNotificationList.count();
334 }
335
336 /******************************************************************************
337 * Returns the widget to act as parent for error messages, etc.
338 */
displayParent()339 QWidget* MessageNotification::displayParent()
340 {
341 return widget();
342 }
343
closeDisplay()344 void MessageNotification::closeDisplay()
345 {
346 close();
347 }
348
349 /******************************************************************************
350 * Display the notification.
351 * Output any required audio notification, and reschedule or delete the event
352 * from the calendar file.
353 */
showDisplay()354 void MessageNotification::showDisplay()
355 {
356 if (mInitialised && mHelper->activateAutoClose())
357 {
358 if (!mCommandInhibit && !mShown)
359 {
360 qCDebug(KALARM_LOG) << "MessageNotification::showDisplay: sendEvent";
361 sendEvent();
362 mShown = true;
363 // Ensure that the screen wakes from sleep, in case the window manager
364 // doesn't do this when the notification is displayed.
365 KIdleTime::instance()->simulateUserActivity();
366 }
367 if (!mDisplayComplete && !mErrorWindow() && mAlarmType() != KAAlarm::INVALID_ALARM)
368 mHelper->displayComplete(false); // reschedule
369 mDisplayComplete = true;
370 }
371 }
372
raiseDisplay()373 void MessageNotification::raiseDisplay()
374 {
375 }
376
377 /******************************************************************************
378 * Raise the alarm notification, re-output any required audio notification, and
379 * reschedule the alarm in the calendar file.
380 */
repeat(const KAAlarm & alarm)381 void MessageNotification::repeat(const KAAlarm& alarm)
382 {
383 if (!mInitialised)
384 return;
385 if (mEventId().isEmpty())
386 return;
387 KAEvent event = ResourcesCalendar::event(mEventId());
388 if (event.isValid())
389 {
390 mAlarmType() = alarm.type(); // store new alarm type for use if it is later deferred
391 if (mHelper->alarmShowing(event))
392 ResourcesCalendar::updateEvent(event);
393 }
394 }
395
hasDefer() const396 bool MessageNotification::hasDefer() const
397 {
398 return mEnableDefer;
399 }
400
401 /******************************************************************************
402 * Show the Defer button when it was previously hidden.
403 */
showDefer()404 void MessageNotification::showDefer()
405 {
406 if (!mEnableDefer)
407 {
408 mNoDefer() = false;
409 mEnableDefer = true;
410 setNotificationButtons();
411 mHelper->setDeferralLimit(mEvent()); // remove button when alarm can't be deferred any more
412 }
413 }
414
415 /******************************************************************************
416 * Convert a reminder notification into a normal alarm notification.
417 */
cancelReminder(const KAEvent & event,const KAAlarm & alarm)418 void MessageNotification::cancelReminder(const KAEvent& event, const KAAlarm& alarm)
419 {
420 if (mHelper->cancelReminder(event, alarm))
421 {
422 const MessageDisplayHelper::DisplayTexts& texts = mHelper->texts();
423 setNotificationTitle(texts.title);
424 mTimeText = texts.time;
425 mRemainingText.clear();
426 setNotificationText();
427 showDefer();
428 }
429 }
430
431 /******************************************************************************
432 * Update and show the alarm's trigger time.
433 */
showDateTime(const KAEvent & event,const KAAlarm & alarm)434 void MessageNotification::showDateTime(const KAEvent& event, const KAAlarm& alarm)
435 {
436 if (mHelper->updateDateTime(event, alarm))
437 {
438 mTimeText = mHelper->texts().time;
439 setNotificationText();
440 }
441 }
442
443 /******************************************************************************
444 * Called when the texts to display have changed.
445 */
textsChanged(MessageDisplayHelper::DisplayTexts::TextIds ids,const QString & change)446 void MessageNotification::textsChanged(MessageDisplayHelper::DisplayTexts::TextIds ids, const QString& change)
447 {
448 const MessageDisplayHelper::DisplayTexts& texts = mHelper->texts();
449
450 if (ids & MessageDisplayHelper::DisplayTexts::Title)
451 setNotificationTitle(texts.title);
452
453 bool textChanged = false;
454 if (ids & MessageDisplayHelper::DisplayTexts::Time)
455 {
456 mTimeText = texts.time;
457 textChanged = true;
458 }
459
460 if (ids & MessageDisplayHelper::DisplayTexts::RemainingTime)
461 {
462 mRemainingText = texts.remainingTime;
463 textChanged = true;
464 }
465
466 if (ids & MessageDisplayHelper::DisplayTexts::MessageAppend)
467 {
468 // More output is available from the command which is providing the text
469 // for this notification. Add the output, but don't show the notification
470 // until all output has been received. This is a workaround for
471 // notification texts not being reliably updated by setText().
472 mMessageText += change;
473 return;
474 }
475
476 if (textChanged)
477 setNotificationText();
478
479 // Update the notification. Note that this does nothing if no changes have occurred.
480 update();
481 }
482
483 /******************************************************************************
484 * Called when the command providing the alarm message text has exited.
485 * Because setText() doesn't reliably update the text in the notification,
486 * command output notifications are not displayed until all the text is
487 * available to display.
488 * 'success' is true if the command did not fail completely.
489 */
commandCompleted(bool success)490 void MessageNotification::commandCompleted(bool success)
491 {
492 qCDebug(KALARM_LOG) << "MessageNotification::commandCompleted:" << success;
493 if (!success)
494 {
495 // The command failed completely. KAlarmApp will output an error
496 // message, so don't display the empty notification.
497 deleteLater();
498 }
499 else
500 {
501 // The command may have produced some output, so display that, although
502 // if an error occurred, KAlarmApp might display an error message as
503 // well.
504 setNotificationText();
505 mCommandInhibit = false;
506 showDisplay();
507 }
508 }
509
510 /******************************************************************************
511 * Set the notification's title.
512 */
setNotificationTitle(const QString & text)513 void MessageNotification::setNotificationTitle(const QString& text)
514 {
515 setTitle(mErrorMsgs().isEmpty() ? QString() : text);
516 }
517
518 /******************************************************************************
519 * Set the notification's text by combining the text portions.
520 */
setNotificationText()521 void MessageNotification::setNotificationText()
522 {
523 setText(mMessageText + NL + mTimeText + NL + QStringLiteral("<i>") + mRemainingText + QStringLiteral("</i>"));
524 update();
525 }
526
527 /******************************************************************************
528 * Set the notification's action buttons.
529 */
setNotificationButtons()530 void MessageNotification::setNotificationButtons()
531 {
532 mEditButtonIndex = -1;
533 mDeferButtonIndex = -1;
534 QStringList buttons;
535 if (mEnableEdit)
536 {
537 mEditButtonIndex = 0;
538 buttons += i18nc("@action:button", "Edit");
539 }
540 if (mEnableDefer)
541 {
542 mDeferButtonIndex = buttons.count();
543 buttons += i18nc("@action:button", "Defer");
544 }
545 setActions(buttons);
546 setDefaultAction(KAboutData::applicationData().displayName());
547 }
548
isDeferButtonEnabled() const549 bool MessageNotification::isDeferButtonEnabled() const
550 {
551 return mEnableDefer;
552 }
553
enableDeferButton(bool enable)554 void MessageNotification::enableDeferButton(bool enable)
555 {
556 mEnableDefer = enable;
557 setNotificationButtons();
558 }
559
enableEditButton(bool enable)560 void MessageNotification::enableEditButton(bool enable)
561 {
562 mEnableEdit = enable;
563 setNotificationButtons();
564 }
565
566 /******************************************************************************
567 * Save settings to the session managed config file, for restoration
568 * when the program is restored.
569 */
saveProperties(KConfigGroup & config)570 void MessageNotification::saveProperties(KConfigGroup& config)
571 {
572 Q_UNUSED(config)
573 #ifdef RESTORE_NOTIFICATIONS
574 if (mDisplayComplete && mHelper->saveProperties(config))
575 config.writeEntry("NotifyId", eventId());
576 #endif
577 }
578
579 /******************************************************************************
580 * Called when a button in the notification has been pressed.
581 * Button indexes start at 1.
582 */
buttonActivated(unsigned int index)583 void MessageNotification::buttonActivated(unsigned int index)
584 {
585 int i = static_cast<int>(index);
586 if (i == 0)
587 {
588 displayMainWindow();
589 }
590 else if (i == mEditButtonIndex + 1)
591 {
592 if (mHelper->createEdit())
593 mHelper->executeEdit();
594 }
595 else if (i == mDeferButtonIndex + 1)
596 {
597 DeferDlgData* data = createDeferDlg(this, true);
598 executeDeferDlg(data);
599 }
600 }
601
602 /******************************************************************************
603 * Called when the notification has closed, either by user action of by timeout.
604 * Note that when a notification has timed out, it shows in the notification
605 * history, but there is no way to know if the user closes it there.
606 * Only quits the application if there is no system tray icon displayed.
607 */
slotClosed()608 void MessageNotification::slotClosed()
609 {
610 qCDebug(KALARM_LOG) << "MessageNotification::slotClosed";
611 mHelper->closeEvent();
612 }
613
614 #include "messagenotification.moc"
615
616 // vim: et sw=4:
617