Warning, file /pim/kalarm/src/messagenotification.cpp was not indexed or was modified since last indexation (in which case cross-reference links may be missing, inaccurate or erroneous).

0001 /*
0002  *  messagenotification.cpp  -  displays an alarm message in a system notification
0003  *  Program:  kalarm
0004  *  SPDX-FileCopyrightText: 2020-2023 David Jarvie <djarvie@kde.org>
0005  *
0006  *  SPDX-License-Identifier: GPL-2.0-or-later
0007  */
0008 
0009 #include "messagenotification.h"
0010 #include "messagedisplayhelper.h"
0011 
0012 #include "mainwindow.h"
0013 #include "resourcescalendar.h"
0014 #include "lib/file.h"
0015 #include "kalarm_debug.h"
0016 
0017 #include <KAboutData>
0018 #include <KLocalizedString>
0019 #include <KWindowSystem>
0020 #ifdef RESTORE_NOTIFICATIONS
0021 #include <KConfigGroup>
0022 #include <KConfigGui>
0023 #endif
0024 
0025 #include <QSessionManager>
0026 
0027 using namespace KAlarmCal;
0028 
0029 //clazy:excludeall=non-pod-global-static
0030 
0031 namespace
0032 {
0033 
0034 // Notification eventIds: these are the IDs contained in the '[Event/ID]'
0035 // entries in kalarm.notifyrc.
0036 const QString MessageId = QStringLiteral("Message");
0037 const QString BeepId    = QStringLiteral("MessageBeep");
0038 const QString SpeakId   = QStringLiteral("MessageSpeak");
0039 const QString ErrorId   = QStringLiteral("MessageError");
0040 
0041 const QString NL = QStringLiteral("\n");
0042 const QString SP = QStringLiteral(" ");
0043 
0044 inline QString getNotifyEventId(const KAEvent& event)
0045 {
0046     return event.beep() ? BeepId : event.speak() ? SpeakId : MessageId;
0047 }
0048 
0049 } // namespace
0050 
0051 
0052 /*=============================================================================
0053 * Helper class to save all message notifications' properties on session
0054 * shutdown, to enable them to be recreated on the next startup.
0055 *
0056 * NOTE: When a notification has closed, there is currently no way to know
0057 *       whether it has been closed by the user or has timed out. There is also
0058 *       no way to know when a notification in the notification history is
0059 *       closed by the user. So notifications are not restored on startup, since
0060 *       that might re-raise notifications which the user has already closed.
0061 *       If this changes in the future, notifications could be restored on
0062 *       startup, in the same way as alarm windows are restored.
0063 */
0064 class MNSessionManager : public QObject
0065 {
0066     Q_OBJECT
0067 public:
0068     MNSessionManager()
0069     {
0070 #ifdef RESTORE_NOTIFICATIONS
0071         connect(qApp, &QGuiApplication::saveStateRequest, this, &MNSessionManager::saveState);
0072 #endif
0073     }
0074     ~MNSessionManager() override {}
0075 
0076     static void create()
0077     {
0078         if (!mInstance)
0079             mInstance = new MNSessionManager;
0080     }
0081 
0082 private Q_SLOTS:
0083 #ifdef RESTORE_NOTIFICATIONS
0084     /******************************************************************************
0085     * Called by the session manager to request the application to save its state.
0086     */
0087     void saveState(QSessionManager& sm)
0088     {
0089         KConfigGui::setSessionConfig(sm.sessionId(), sm.sessionKey());
0090         KConfig* config = KConfigGui::sessionConfig();
0091         // Save each MessageNotification's data.
0092         int n = 1;
0093         for (MessageNotification* notif : std::as_const(MessageNotification::mNotificationList))
0094         {
0095             const QByteArray group = "Notification_" + QByteArray::number(++n);
0096             KConfigGroup cg(config, QLatin1String(group.constData()));
0097             notif->saveProperties(cg);
0098         }
0099         KConfigGroup cg(config, QStringLiteral("Number"));
0100         cg.writeEntry("NumberOfNotifications", MessageNotification::mNotificationList.count());
0101     }
0102 #endif
0103 
0104 private:
0105     static MNSessionManager* mInstance;
0106 };
0107 
0108 MNSessionManager* MNSessionManager::mInstance = nullptr;
0109 
0110 QList<MessageNotification*> MessageNotification::mNotificationList;
0111 
0112 /******************************************************************************
0113 * Restore MessageNotification instances saved at session shutdown.
0114 */
0115 void MessageNotification::sessionRestore()
0116 {
0117 #ifdef RESTORE_NOTIFICATIONS
0118     KConfig* config = KConfigGui::sessionConfig();
0119     if (config)
0120     {
0121         KConfigGroup cg(config, QStringLiteral("Number"));
0122         const int count = cg.readEntry("NumberOfNotifications", 0);
0123         for (int n = 1;  n <= count;  ++n)
0124         {
0125             const QByteArray group = "Notification_" + QByteArray::number(n);
0126             cg = KConfigGroup(config, group.constData());
0127             // Have to initialise the MessageNotification instance with its
0128             // eventId already known. So first create a helper, then read
0129             // its properties, and finally create the MessageNotification.
0130             MessageDisplayHelper* helper = new MessageDisplayHelper(nullptr);
0131             if (!helper->readPropertyValues(cg))
0132                 delete helper;
0133             else
0134             {
0135                 const QString notifyId = cg.readEntry("NotifyId");
0136                 new MessageNotification(notifyId, helper);
0137             }
0138         }
0139     }
0140 #endif
0141 }
0142 
0143 /******************************************************************************
0144 * Construct the message notification for the specified alarm.
0145 * Other alarms in the supplied event may have been updated by the caller, so
0146 * the whole event needs to be stored for updating the calendar file when it is
0147 * displayed.
0148 */
0149 MessageNotification::MessageNotification(const KAEvent& event, const KAAlarm& alarm, int flags)
0150     : KNotification(getNotifyEventId(event))
0151     , MessageDisplay(event, alarm, flags)
0152 {
0153     qCDebug(KALARM_LOG) << "MessageNotification():" << mEventId();
0154     MNSessionManager::create();
0155     if (!(flags & NoInitView))
0156         MessageNotification::setUpDisplay();    // avoid calling virtual method from constructor
0157 
0158     connect(this, &KNotification::closed, this, &MessageNotification::slotClosed);
0159     connect(mHelper, &MessageDisplayHelper::textsChanged, this, &MessageNotification::textsChanged);
0160     connect(mHelper, &MessageDisplayHelper::commandExited, this, &MessageNotification::commandCompleted);
0161 
0162     mNotificationList.append(this);
0163 }
0164 
0165 /******************************************************************************
0166 * Construct the message notification for a specified error message.
0167 * If 'dontShowAgain' is non-null, a "Don't show again" option is displayed. Note
0168 * that the option is specific to 'event'.
0169 */
0170 MessageNotification::MessageNotification(const KAEvent& event, const DateTime& alarmDateTime,
0171                        const QStringList& errmsgs, const QString& dontShowAgain)
0172     : KNotification(ErrorId)
0173     , MessageDisplay(event, alarmDateTime, errmsgs, dontShowAgain)
0174 {
0175     qCDebug(KALARM_LOG) << "MessageNotification(errmsg)";
0176     MNSessionManager::create();
0177     MessageNotification::setUpDisplay();    // avoid calling virtual method from constructor
0178 
0179     connect(this, &KNotification::closed, this, &MessageNotification::slotClosed);
0180     connect(mHelper, &MessageDisplayHelper::textsChanged, this, &MessageNotification::textsChanged);
0181 
0182     mNotificationList.append(this);
0183 }
0184 
0185 /******************************************************************************
0186 * Construct the message notification from the properties contained in the
0187 * supplied helper.
0188 * Ownership of the helper is taken by the new instance.
0189 */
0190 MessageNotification::MessageNotification(const QString& eventId, MessageDisplayHelper* helper)
0191     : KNotification(eventId)
0192     , MessageDisplay(helper)
0193 {
0194     qCDebug(KALARM_LOG) << "MessageNotification(helper):" << mEventId();
0195     MNSessionManager::create();
0196 
0197     connect(this, &KNotification::closed, this, &MessageNotification::slotClosed);
0198     connect(mHelper, &MessageDisplayHelper::textsChanged, this, &MessageNotification::textsChanged);
0199     connect(mHelper, &MessageDisplayHelper::commandExited, this, &MessageNotification::commandCompleted);
0200 
0201     mNotificationList.append(this);
0202     helper->processPropertyValues();
0203 }
0204 
0205 /******************************************************************************
0206 * Destructor. Perform any post-alarm actions before tidying up.
0207 */
0208 MessageNotification::~MessageNotification()
0209 {
0210     qCDebug(KALARM_LOG) << "~MessageNotification" << mEventId();
0211     close();
0212     mNotificationList.removeAll(this);
0213 }
0214 
0215 /******************************************************************************
0216 * Construct the message notification.
0217 */
0218 void MessageNotification::setUpDisplay()
0219 {
0220     mHelper->initTexts();
0221     MessageDisplayHelper::DisplayTexts texts = mHelper->texts();
0222 
0223     setNotificationTitle(texts.title);
0224 
0225     // Show the alarm date/time. Any reminder indication is shown in the
0226     // notification title.
0227     // Alarm date/time: display time zone if not local time zone.
0228     mTimeText = texts.time;
0229 
0230     mMessageText.clear();
0231     if (!mErrorWindow())
0232     {
0233         // It's a normal alarm message notification
0234         switch (mAction())
0235         {
0236             case KAEvent::SubAction::File:
0237                 // Display the file name
0238                 mMessageText = texts.fileName + NL;
0239 
0240                 if (mErrorMsgs().isEmpty())
0241                 {
0242                     // Display contents of file
0243                     switch (texts.fileType)
0244                     {
0245                         case File::Type::Image:
0246                             break;   // can't display an image
0247                         case File::Type::TextFormatted:
0248                         default:
0249                             mMessageText += texts.message;
0250                             break;
0251                     }
0252                 }
0253                 break;
0254 
0255             case KAEvent::SubAction::Message:
0256                 mMessageText = texts.message;
0257                 break;
0258 
0259             case KAEvent::SubAction::Command:
0260                 mMessageText = texts.message;
0261                 mCommandInhibit = true;
0262                 break;
0263 
0264             case KAEvent::SubAction::Email:
0265             default:
0266                 break;
0267         }
0268 
0269         if (!texts.remainingTime.isEmpty())
0270         {
0271             // Advance reminder: show remaining time until the actual alarm
0272             mRemainingText = texts.remainingTime;
0273         }
0274     }
0275     else
0276     {
0277         // It's an error message
0278         switch (mAction())
0279         {
0280             case KAEvent::SubAction::Email:
0281             {
0282                 // Display the email addresses and subject.
0283                 mMessageText = texts.errorEmail[0] + SP + texts.errorEmail[1] + NL
0284                              + texts.errorEmail[2] + SP + texts.errorEmail[3] + NL;
0285                 break;
0286             }
0287             case KAEvent::SubAction::Command:
0288             case KAEvent::SubAction::File:
0289             case KAEvent::SubAction::Message:
0290             default:
0291                 // Just display the error message strings
0292                 break;
0293         }
0294     }
0295 
0296     if (!mErrorMsgs().isEmpty())
0297     {
0298         setIconName(QStringLiteral("dialog-error"));
0299         mMessageText += mErrorMsgs().join(NL);
0300         mCommandInhibit = false;
0301     }
0302 
0303     setNotificationText();
0304 
0305     mEnableEdit = mShowEdit();
0306     if (!mNoDefer())
0307     {
0308         mEnableDefer = true;
0309         mHelper->setDeferralLimit(mEvent());  // ensure that button is disabled when alarm can't be deferred any more
0310     }
0311     setNotificationButtons();
0312 
0313     mInitialised = true;   // the notification's widgets have been created
0314 }
0315 
0316 /******************************************************************************
0317 * Return the number of message notifications.
0318 */
0319 int MessageNotification::notificationCount()
0320 {
0321     return mNotificationList.count();
0322 }
0323 
0324 /******************************************************************************
0325 * Returns the widget to act as parent for error messages, etc.
0326 */
0327 QWidget* MessageNotification::displayParent()
0328 {
0329     return MainWindow::mainMainWindow();
0330 }
0331 
0332 void MessageNotification::closeDisplay()
0333 {
0334     close();
0335 }
0336 
0337 /******************************************************************************
0338 * Display the notification.
0339 * Output any required audio notification, and reschedule or delete the event
0340 * from the calendar file.
0341 */
0342 void MessageNotification::showDisplay()
0343 {
0344     if (mInitialised  &&  mHelper->activateAutoClose())
0345     {
0346         if (!mCommandInhibit  &&  !mShown)
0347         {
0348             qCDebug(KALARM_LOG) << "MessageNotification::showDisplay: sendEvent";
0349             sendEvent();
0350             mShown = true;
0351             // Ensure that the screen wakes from sleep, in case the window manager
0352             // doesn't do this when the notification is displayed.
0353             mHelper->wakeScreen();
0354         }
0355         if (!mDisplayComplete  &&  !mErrorWindow()  &&  mAlarmType() != KAAlarm::Type::Invalid)
0356             mHelper->displayComplete(false);   // reschedule
0357         mDisplayComplete = true;
0358     }
0359 }
0360 
0361 void MessageNotification::raiseDisplay()
0362 {
0363 }
0364 
0365 /******************************************************************************
0366 * Raise the alarm notification, re-output any required audio notification, and
0367 * reschedule the alarm in the calendar file.
0368 */
0369 void MessageNotification::repeat(const KAAlarm& alarm)
0370 {
0371     if (!mInitialised)
0372         return;
0373     if (mEventId().isEmpty())
0374         return;
0375     KAEvent event = ResourcesCalendar::event(mEventId());
0376     if (event.isValid())
0377     {
0378         mAlarmType() = alarm.type();    // store new alarm type for use if it is later deferred
0379         if (mHelper->alarmShowing(event))
0380             ResourcesCalendar::updateEvent(event);
0381     }
0382 }
0383 
0384 bool MessageNotification::hasDefer() const
0385 {
0386     return mEnableDefer;
0387 }
0388 
0389 /******************************************************************************
0390 * Show the Defer button when it was previously hidden.
0391 */
0392 void MessageNotification::showDefer()
0393 {
0394     if (!mEnableDefer)
0395     {
0396         mNoDefer() = false;
0397         mEnableDefer = true;
0398         setNotificationButtons();
0399         mHelper->setDeferralLimit(mEvent());    // remove button when alarm can't be deferred any more
0400     }
0401 }
0402 
0403 /******************************************************************************
0404 * Convert a reminder notification into a normal alarm notification.
0405 */
0406 void MessageNotification::cancelReminder(const KAEvent& event, const KAAlarm& alarm)
0407 {
0408     if (mHelper->cancelReminder(event, alarm))
0409     {
0410         const MessageDisplayHelper::DisplayTexts& texts = mHelper->texts();
0411         setNotificationTitle(texts.title);
0412         mTimeText = texts.time;
0413         mRemainingText.clear();
0414         setNotificationText();
0415         showDefer();
0416     }
0417 }
0418 
0419 /******************************************************************************
0420 * Update and show the alarm's trigger time.
0421 */
0422 void MessageNotification::showDateTime(const KAEvent& event, const KAAlarm& alarm)
0423 {
0424     if (mHelper->updateDateTime(event, alarm))
0425     {
0426         mTimeText = mHelper->texts().time;
0427         setNotificationText();
0428     }
0429 }
0430 
0431 /******************************************************************************
0432 * Called when the texts to display have changed.
0433 */
0434 void MessageNotification::textsChanged(MessageDisplayHelper::DisplayTexts::TextIds ids, const QString& change)
0435 {
0436     const MessageDisplayHelper::DisplayTexts& texts = mHelper->texts();
0437 
0438     if (ids & MessageDisplayHelper::DisplayTexts::Title)
0439         setNotificationTitle(texts.title);
0440 
0441     bool textChanged = false;
0442     if (ids & MessageDisplayHelper::DisplayTexts::Time)
0443     {
0444         mTimeText = texts.time;
0445         textChanged = true;
0446     }
0447 
0448     if (ids & MessageDisplayHelper::DisplayTexts::RemainingTime)
0449     {
0450         mRemainingText = texts.remainingTime;
0451         textChanged = true;
0452     }
0453 
0454     if (ids & MessageDisplayHelper::DisplayTexts::MessageAppend)
0455     {
0456         // More output is available from the command which is providing the text
0457         // for this notification. Add the output, but don't show the notification
0458         // until all output has been received. This is a workaround for
0459         // notification texts not being reliably updated by setText().
0460         mMessageText += change;
0461         return;
0462     }
0463 
0464     if (textChanged)
0465         setNotificationText();
0466 }
0467 
0468 /******************************************************************************
0469 * Called when the command providing the alarm message text has exited.
0470 * Because setText() doesn't reliably update the text in the notification,
0471 * command output notifications are not displayed until all the text is
0472 * available to display.
0473 * 'success' is true if the command did not fail completely.
0474 */
0475 void MessageNotification::commandCompleted(bool success)
0476 {
0477     qCDebug(KALARM_LOG) << "MessageNotification::commandCompleted:" << success;
0478     if (!success)
0479     {
0480         // The command failed completely. KAlarmApp will output an error
0481         // message, so don't display the empty notification.
0482         deleteLater();
0483     }
0484     else
0485     {
0486         // The command may have produced some output, so display that, although
0487         // if an error occurred, KAlarmApp might display an error message as
0488         // well.
0489         setNotificationText();
0490         mCommandInhibit = false;
0491         showDisplay();
0492     }
0493 }
0494 
0495 /******************************************************************************
0496 * Set the notification's title.
0497 */
0498 void MessageNotification::setNotificationTitle(const QString& text)
0499 {
0500     setTitle(mErrorMsgs().isEmpty() ? QString() : text);
0501 }
0502 
0503 /******************************************************************************
0504 * Set the notification's text by combining the text portions.
0505 */
0506 void MessageNotification::setNotificationText()
0507 {
0508     setText(mMessageText + NL + mTimeText + NL + QStringLiteral("<i>") + mRemainingText + QStringLiteral("</i>"));
0509 }
0510 
0511 /******************************************************************************
0512 * Set the notification's action buttons.
0513 */
0514 void MessageNotification::setNotificationButtons()
0515 {
0516     if (mEnableEdit)
0517     {
0518         auto editAction = addAction(i18nc("@action:button", "Edit"));
0519         connect(editAction, &KNotificationAction::activated, this, [this] {
0520             if (mHelper->createEdit())
0521                 mHelper->executeEdit();
0522         });
0523     }
0524 
0525     if (mEnableDefer)
0526     {
0527         auto deferAction = addAction(i18nc("@action:button", "Defer"));
0528         connect(deferAction, &KNotificationAction::activated, this, [this] {
0529             DeferDlgData* data = createDeferDlg(this, true);
0530             executeDeferDlg(data);
0531         });
0532     }
0533 
0534     auto defaultAction = addDefaultAction(KAboutData::applicationData().displayName());
0535     connect(defaultAction, &KNotificationAction::activated, this, &MessageNotification::slotDefaultActivated);
0536 }
0537 
0538 bool MessageNotification::isDeferButtonEnabled() const
0539 {
0540     return mEnableDefer;
0541 }
0542 
0543 void MessageNotification::enableDeferButton(bool enable)
0544 {
0545     mEnableDefer = enable;
0546     setNotificationButtons();
0547 }
0548 
0549 void MessageNotification::enableEditButton(bool enable)
0550 {
0551     mEnableEdit = enable;
0552     setNotificationButtons();
0553 }
0554 
0555 /******************************************************************************
0556 * Save settings to the session managed config file, for restoration
0557 * when the program is restored.
0558 */
0559 void MessageNotification::saveProperties(KConfigGroup& config)
0560 {
0561     Q_UNUSED(config)
0562 #ifdef RESTORE_NOTIFICATIONS
0563     if (mDisplayComplete  &&  mHelper->saveProperties(config))
0564         config.writeEntry("NotifyId", eventId());
0565 #endif
0566 }
0567 
0568 /******************************************************************************
0569 * Called when the default action in the notification has been pressed, to show
0570 * the main window.
0571 */
0572 void MessageNotification::slotDefaultActivated()
0573 {
0574     KWindowSystem::setCurrentXdgActivationToken(xdgActivationToken());
0575     displayMainWindow();
0576 }
0577 
0578 /******************************************************************************
0579 * Called when the notification has closed, either by user action of by timeout.
0580 * Note that when a notification has timed out, it shows in the notification
0581 * history, but there is no way to know if the user closes it there.
0582 * Only quits the application if there is no system tray icon displayed.
0583 */
0584 void MessageNotification::slotClosed()
0585 {
0586     qCDebug(KALARM_LOG) << "MessageNotification::slotClosed";
0587     mHelper->closeEvent();
0588 }
0589 
0590 #include "messagenotification.moc"
0591 #include "moc_messagenotification.cpp"
0592 
0593 // vim: et sw=4: