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

0001 /*
0002  *  kalarmapp.cpp  -  the KAlarm application object
0003  *  Program:  kalarm
0004  *  SPDX-FileCopyrightText: 2001-2024 David Jarvie <djarvie@kde.org>
0005  *
0006  *  SPDX-License-Identifier: GPL-2.0-or-later
0007  */
0008 
0009 #include "kalarmapp.h"
0010 
0011 #include "kalarm.h"
0012 #include "commandoptions.h"
0013 #include "dbushandler.h"
0014 #include "displaycalendar.h"
0015 #include "editdlgtypes.h"
0016 #include "functions.h"
0017 #include "kamail.h"
0018 #include "mainwindow.h"
0019 #include "messagenotification.h"
0020 #include "messagewindow.h"
0021 #include "pluginmanager.h"
0022 #include "resourcescalendar.h"
0023 #include "startdaytimer.h"
0024 #include "traywindow.h"
0025 #include "resources/datamodel.h"
0026 #include "resources/resources.h"
0027 #include "lib/desktop.h"
0028 #include "lib/messagebox.h"
0029 #include "notifications_interface.h" // DBUS-generated
0030 #include "dbusproperties.h"          // DBUS-generated
0031 #include "kalarmcalendar/datetime.h"
0032 #include "kalarmcalendar/karecurrence.h"
0033 #include "kalarm_debug.h"
0034 
0035 #include <KLocalizedString>
0036 #include <KConfig>
0037 #include <KConfigGui>
0038 #include <KAboutData>
0039 #include <KSharedConfig>
0040 #include <KStandardGuiItem>
0041 #include <KWindowSystem>
0042 #include <netwm.h>
0043 #include <KShell>
0044 
0045 #include <QObject>
0046 #include <QTimer>
0047 #include <QFile>
0048 #include <QTextStream>
0049 #include <QTemporaryFile>
0050 #include <QStandardPaths>
0051 #include <QSystemTrayIcon>
0052 #include <QCommandLineParser>
0053 
0054 #include <stdlib.h>
0055 #include <ctype.h>
0056 #include <iostream>
0057 #include <climits>
0058 
0059 namespace
0060 {
0061 const int RESOURCES_TIMEOUT = 30;   // timeout (seconds) for resources to be populated
0062 
0063 const char FDO_NOTIFICATIONS_SERVICE[] = "org.freedesktop.Notifications";
0064 const char FDO_NOTIFICATIONS_PATH[]    = "/org/freedesktop/Notifications";
0065 
0066 const QLatin1String GENERAL_GROUP("General");
0067 
0068 /******************************************************************************
0069 * Find the maximum number of seconds late which a late-cancel alarm is allowed
0070 * to be. This is calculated as the late cancel interval, plus a few seconds
0071 * leeway to cater for any timing irregularities.
0072 */
0073 inline int maxLateness(int lateCancel)
0074 {
0075     static const int LATENESS_LEEWAY = 5;
0076     int lc = (lateCancel >= 1) ? (lateCancel - 1)*60 : 0;
0077     return LATENESS_LEEWAY + lc;
0078 }
0079 
0080 QWidget* mainWidget()
0081 {
0082     return MainWindow::mainMainWindow();
0083 }
0084 }
0085 
0086 
0087 KAlarmApp*  KAlarmApp::mInstance  = nullptr;
0088 int         KAlarmApp::mActiveCount = 0;
0089 int         KAlarmApp::mFatalError  = 0;
0090 QString     KAlarmApp::mFatalMessage;
0091 
0092 
0093 /******************************************************************************
0094 * Construct the application.
0095 */
0096 KAlarmApp::KAlarmApp(int& argc, char** argv)
0097     : QApplication(argc, argv)
0098     , mDBusHandler(new DBusHandler())
0099 {
0100     qCDebug(KALARM_LOG) << "KAlarmApp:";
0101 }
0102 
0103 /******************************************************************************
0104 */
0105 KAlarmApp::~KAlarmApp()
0106 {
0107     while (!mCommandProcesses.isEmpty())
0108     {
0109         ProcData* pd = mCommandProcesses.at(0);
0110         mCommandProcesses.pop_front();
0111         delete pd;
0112     }
0113     ResourcesCalendar::terminate();
0114     DisplayCalendar::terminate();
0115     DataModel::terminate();
0116     delete mDBusHandler;
0117 }
0118 
0119 /******************************************************************************
0120 * Return the one and only KAlarmApp instance.
0121 * If it doesn't already exist, it is created first.
0122 */
0123 KAlarmApp* KAlarmApp::create(int& argc, char** argv)
0124 {
0125     if (!mInstance)
0126     {
0127         mInstance = new KAlarmApp(argc, argv);
0128 
0129         if (mFatalError)
0130             mInstance->quitFatal();
0131     }
0132     return mInstance;
0133 }
0134 
0135 /******************************************************************************
0136 * Perform initialisations which may require KAboutData to have been set up.
0137 */
0138 void KAlarmApp::initialise()
0139 {
0140 #ifndef NDEBUG
0141     KAlarm::setTestModeConditions();
0142 #endif
0143 
0144     setQuitOnLastWindowClosed(false);
0145     Preferences::self();    // read KAlarm configuration
0146     if (!Preferences::noAutoStart())
0147     {
0148         // Strip out any "OnlyShowIn=KDE" list from kalarm.autostart.desktop
0149         Preferences::setNoAutoStart(false);
0150         // Enable kalarm.autostart.desktop to start KAlarm
0151         Preferences::setAutoStart(true);
0152         Preferences::self()->save();
0153     }
0154     Preferences::connect(&Preferences::startOfDayChanged, this, &KAlarmApp::changeStartOfDay);
0155     Preferences::connect(&Preferences::workTimeChanged, this, &KAlarmApp::slotWorkTimeChanged);
0156     Preferences::connect(&Preferences::holidaysChanged, this, &KAlarmApp::slotHolidaysChanged);
0157     Preferences::connect(&Preferences::feb29TypeChanged, this, &KAlarmApp::slotFeb29TypeChanged);
0158     Preferences::connect(&Preferences::showInSystemTrayChanged, this, &KAlarmApp::slotShowInSystemTrayChanged);
0159     Preferences::connect(&Preferences::archivedKeepDaysChanged, this, &KAlarmApp::setArchivePurgeDays);
0160     Preferences::connect(&Preferences::messageFontChanged, this, &KAlarmApp::slotMessageFontChanged);
0161     slotFeb29TypeChanged(Preferences::defaultFeb29Type());
0162 
0163     KAEvent::setStartOfDay(Preferences::startOfDay());
0164     KAEvent::setWorkTime(Preferences::workDays(), Preferences::workDayStart(), Preferences::workDayEnd(), Preferences::timeSpec());
0165     KAEvent::setHolidays(Preferences::holidays());
0166     KAEvent::setDefaultFont(Preferences::messageFont());
0167 
0168     // Load plugins
0169     if (PluginManager::instance()->akonadiPlugin())
0170         qCDebug(KALARM_LOG) << "KAlarmApp: Akonadi plugin found";
0171     else
0172         qCDebug(KALARM_LOG) << "KAlarmApp: Akonadi dependent features not available (Akonadi plugin not found)";
0173 
0174     // Check if KOrganizer is installed
0175     const QString korg = QStringLiteral("korganizer");
0176     mKOrganizerEnabled = !QStandardPaths::findExecutable(korg).isEmpty();
0177     if (!mKOrganizerEnabled) { qCDebug(KALARM_LOG) << "KAlarmApp: KOrganizer options disabled (KOrganizer not found)"; }
0178     // Check if the window manager can't handle keyboard focus transfer between windows
0179     mWindowFocusBroken = (Desktop::currentIdentity() == Desktop::Type::Unity);
0180     if (mWindowFocusBroken) { qCDebug(KALARM_LOG) << "KAlarmApp: Window keyboard focus broken"; }
0181 
0182     Resources* resources = Resources::instance();
0183     connect(resources, &Resources::resourceAdded,
0184                  this, &KAlarmApp::slotResourceAdded);
0185     connect(resources, &Resources::resourcePopulated,
0186                  this, &KAlarmApp::slotResourcePopulated);
0187     connect(resources, &Resources::resourcePopulated,
0188                  this, &KAlarmApp::purgeNewArchivedDefault);
0189     connect(resources, &Resources::resourcesCreated,
0190                  this, &KAlarmApp::slotResourcesCreated);
0191     connect(resources, &Resources::resourcesPopulated,
0192                  this, &KAlarmApp::processQueue);
0193 
0194     initialiseTimerResources();   // initialise calendars and alarm timer
0195 
0196     KConfigGroup config(KSharedConfig::openConfig(), GENERAL_GROUP);
0197     mNoSystemTray        = config.readEntry("NoSystemTray", false);
0198     mOldShowInSystemTray = wantShowInSystemTray();
0199     DateTime::setStartOfDay(Preferences::startOfDay());
0200     mPrefsArchivedColour = Preferences::archivedColour();
0201 
0202     // Get notified when the Freedesktop notifications properties have changed.
0203     QDBusConnection conn = QDBusConnection::sessionBus();
0204     if (conn.interface()->isServiceRegistered(QString::fromLatin1(FDO_NOTIFICATIONS_SERVICE)))
0205     {
0206         OrgFreedesktopDBusPropertiesInterface* piface = new OrgFreedesktopDBusPropertiesInterface(
0207                 QString::fromLatin1(FDO_NOTIFICATIONS_SERVICE),
0208                 QString::fromLatin1(FDO_NOTIFICATIONS_PATH),
0209                 conn, this);
0210         connect(piface, &OrgFreedesktopDBusPropertiesInterface::PropertiesChanged,
0211                 this, &KAlarmApp::slotFDOPropertiesChanged);
0212         OrgFreedesktopNotificationsInterface niface(
0213                 QString::fromLatin1(FDO_NOTIFICATIONS_SERVICE),
0214                 QString::fromLatin1(FDO_NOTIFICATIONS_PATH),
0215                 conn);
0216         mNotificationsInhibited = niface.inhibited();
0217     }
0218 }
0219 
0220 /******************************************************************************
0221 * Initialise or reinitialise things which are tidied up/closed by quitIf().
0222 * Reinitialisation can be necessary if session restoration finds nothing to
0223 * restore and starts quitting the application, but KAlarm then starts up again
0224 * before the application has exited.
0225 * Reply = true if calendars were initialised successfully,
0226 *         false if they were already initialised, or if initialisation failed.
0227 */
0228 bool KAlarmApp::initialiseTimerResources()
0229 {
0230     if (!mAlarmTimer)
0231     {
0232         mAlarmTimer = new QTimer(this);
0233         mAlarmTimer->setSingleShot(true);
0234         connect(mAlarmTimer, &QTimer::timeout, this, &KAlarmApp::checkNextDueAlarm);
0235     }
0236     if (!ResourcesCalendar::instance())
0237     {
0238         qCDebug(KALARM_LOG) << "KAlarmApp::initialise: initialising calendars";
0239         Desktop::setMainWindowFunc(&mainWidget);
0240         // First, initialise calendar resources, which need to be ready to
0241         // receive signals when resources initialise.
0242         ResourcesCalendar::initialise(KALARM_NAME, KALARM_VERSION);
0243         connect(ResourcesCalendar::instance(), &ResourcesCalendar::earliestAlarmChanged, this, &KAlarmApp::checkNextDueAlarm);
0244         connect(ResourcesCalendar::instance(), &ResourcesCalendar::atLoginEventAdded, this, &KAlarmApp::atLoginEventAdded);
0245         DisplayCalendar::initialise();
0246         // Finally, initialise the resources which generate signals as they initialise.
0247         DataModel::initialise();
0248         return true;
0249     }
0250     return false;
0251 }
0252 
0253 /******************************************************************************
0254 * Restore the saved session if required.
0255 */
0256 bool KAlarmApp::restoreSession()
0257 {
0258     if (!isSessionRestored())
0259         return false;
0260     if (mFatalError)
0261     {
0262         quitFatal();
0263         return false;
0264     }
0265 
0266     // Process is being restored by session management.
0267     qCDebug(KALARM_LOG) << "KAlarmApp::restoreSession: Restoring";
0268     ++mActiveCount;
0269     // Create the session config object now.
0270     // This is necessary since if initCheck() below causes calendars to be updated,
0271     // the session config created after that points to an invalid file, resulting
0272     // in no windows being restored followed by a later crash.
0273     KConfigGui::sessionConfig();
0274 
0275     // When KAlarm is session restored, automatically set start-at-login to true.
0276     Preferences::self()->load();
0277     Preferences::setAutoStart(true);
0278     Preferences::setNoAutoStart(false);
0279     Preferences::setAskAutoStart(true);  // cancel any start-at-login prompt suppression
0280     Preferences::self()->save();
0281 
0282     if (!initCheck(true))     // open the calendar file (needed for main windows), don't process queue yet
0283     {
0284         --mActiveCount;
0285         quitIf(1, true);    // error opening the main calendar - quit
0286         return false;
0287     }
0288     MainWindow* trayParent = nullptr;
0289     for (int i = 1;  KMainWindow::canBeRestored(i);  ++i)
0290     {
0291         const QString type = KMainWindow::classNameOfToplevel(i);
0292         if (type == QLatin1String("MainWindow"))
0293         {
0294             MainWindow* win = MainWindow::create(true);
0295             win->restore(i, false);
0296             if (win->isHiddenTrayParent())
0297                 trayParent = win;
0298             else
0299                 win->show();
0300         }
0301         else if (type == QLatin1String("MessageWindow"))
0302         {
0303             auto win = new MessageWindow;
0304             win->restore(i, false);
0305             if (win->isValid())
0306             {
0307                 if (Resources::allCreated()  &&  !mNotificationsInhibited)
0308                     win->display();
0309                 else
0310                     mRestoredWindows += win;
0311             }
0312             else
0313                 delete win;
0314         }
0315     }
0316 
0317     MessageNotification::sessionRestore();
0318 
0319     // 1) There should have been a main window to restore, but even if not, there
0320     //    must always be one, so create one if necessary but don't show it.
0321     // 2) Try to display the system tray icon if it is configured to be shown
0322     if (trayParent  ||  wantShowInSystemTray())
0323     {
0324         if (!MainWindow::count())
0325         {
0326             qCWarning(KALARM_LOG) << "KAlarmApp::restoreSession: no main window to restore!?";
0327             trayParent = MainWindow::create();
0328         }
0329         displayTrayIcon(true, trayParent);
0330         // Occasionally for no obvious reason, the main main window is
0331         // shown when it should be hidden, so hide it just to be sure.
0332         if (trayParent)
0333             trayParent->hide();
0334     }
0335     else
0336         createOnlyMainWindow();
0337 
0338     --mActiveCount;
0339     if (quitIf(0))          // quit if no windows are open
0340         return false;       // quitIf() can sometimes return, despite calling exit()
0341 
0342     startProcessQueue();    // start processing the execution queue
0343     return true;
0344 }
0345 
0346 /******************************************************************************
0347 * If resources have been created and notifications are not inhibited,
0348 * show message windows restored at startup which are waiting to be displayed,
0349 * and redisplay alarms showing when the program crashed or was killed.
0350 */
0351 void KAlarmApp::showRestoredWindows()
0352 {
0353     if (!mNotificationsInhibited  &&  Resources::allCreated())
0354     {
0355         if (!mRestoredWindows.isEmpty())
0356         {
0357             // Display message windows restored at startup.
0358             for (MessageWindow* win : std::as_const(mRestoredWindows))
0359                 win->display();
0360             mRestoredWindows.clear();
0361         }
0362         if (mRedisplayAlarms)
0363         {
0364             // Display alarms which were showing when the program crashed or was killed.
0365             mRedisplayAlarms = false;
0366             MessageDisplay::redisplayAlarms();
0367         }
0368     }
0369 }
0370 
0371 /******************************************************************************
0372 * Called to start a new instance of the unique QApplication.
0373 * Reply: exit code (>= 0), or -1 to continue execution.
0374 *        If exit code >= 0, 'outputText' holds text to output before terminating.
0375 */
0376 int KAlarmApp::activateInstance(const QStringList& args, const QString& workingDirectory, QString* outputText)
0377 {
0378     Q_UNUSED(workingDirectory)
0379     qCDebug(KALARM_LOG) << "KAlarmApp::activateInstance" << args;
0380     if (outputText)
0381         outputText->clear();
0382     if (mFatalError)
0383     {
0384         Q_EMIT setExitValue(1);
0385         quitFatal();
0386         return 1;
0387     }
0388 
0389     // The D-Bus call to activate a subsequent instance of KAlarm may not supply
0390     // any arguments, but we need one.
0391     if (!args.isEmpty()  &&  mActivateArg0.isEmpty())
0392         mActivateArg0 = args[0];
0393     QStringList fixedArgs(args);
0394     if (args.isEmpty()  &&  !mActivateArg0.isEmpty())
0395         fixedArgs << mActivateArg0;
0396 
0397     // Parse and interpret command line arguments.
0398     QCommandLineParser parser;
0399     KAboutData::applicationData().setupCommandLine(&parser);
0400     parser.setApplicationDescription(QApplication::applicationDisplayName());
0401     auto options = new CommandOptions;
0402     const QStringList nonexecArgs = options->setOptions(&parser, fixedArgs);
0403     options->parse();
0404     KAboutData::applicationData().processCommandLine(&parser);
0405 
0406     ++mActiveCount;
0407     int exitCode = 0;               // default = success
0408     static bool firstInstance = true;
0409     bool dontRedisplay = false;
0410     if (!firstInstance  ||  !isSessionRestored())
0411     {
0412         options->process();
0413 #ifndef NDEBUG
0414         if (options->simulationTime().isValid())
0415             KAlarm::setSimulatedSystemTime(options->simulationTime());
0416 #endif
0417         CommandOptions::Command command = options->command();
0418         if (options->disableAll())
0419             setAlarmsEnabled(false);   // disable alarm monitoring
0420 
0421         // Handle options which exit with a terminal message, before
0422         // making the application a unique application, since a
0423         // unique application won't output to the terminal if another
0424         // instance is already running.
0425         switch (command)
0426         {
0427             case CommandOptions::CMD_ERROR:
0428                 Q_EMIT setExitValue(1);
0429                 if (outputText)
0430                 {
0431                     // Instance was activated from main().
0432                     *outputText = options->outputText();
0433                     delete options;
0434                     return 1;
0435                 }
0436                 // Instance was activated by DBus.
0437                 std::cerr << qPrintable(options->outputText()) << std::endl;;
0438                 mReadOnly = true;   // don't need write access to calendars
0439                 exitCode = 1;
0440                 break;
0441             default:
0442                 break;
0443         }
0444 
0445         switch (command)
0446         {
0447             case CommandOptions::TRIGGER_EVENT:
0448             case CommandOptions::CANCEL_EVENT:
0449             {
0450                 // Display or delete the event with the specified event ID
0451                 auto action = static_cast<QueuedAction>(int((command == CommandOptions::TRIGGER_EVENT) ? QueuedAction::Trigger : QueuedAction::Cancel)
0452                                                       | int(QueuedAction::Exit));
0453                 // Open the calendar, don't start processing execution queue yet,
0454                 // and wait for the calendar resources to be populated.
0455                 if (!initCheck(true))
0456                     exitCode = 1;
0457                 else
0458                 {
0459                     mCommandOption = options->commandName();
0460                     // Get the resource ID string and event UID. Note that if
0461                     // resources have not been created yet, the numeric
0462                     // resource ID can't yet be looked up.
0463                     if (options->resourceId().isEmpty())
0464                         action = static_cast<QueuedAction>((int)action | int(QueuedAction::FindId));
0465                     mActionQueue.enqueue(ActionQEntry(action, EventId(options->eventId()), options->resourceId()));
0466                     startProcessQueue(true);      // start processing the execution queue
0467                     dontRedisplay = true;
0468                 }
0469                 break;
0470             }
0471             case CommandOptions::LIST:
0472                 // Output a list of scheduled alarms to stdout.
0473                 // Open the calendar, don't start processing execution queue yet,
0474                 // and wait for all calendar resources to be populated.
0475                 mReadOnly = true;   // don't need write access to calendars
0476                 if (firstInstance)
0477                     mAlarmsEnabled = false;   // prevent alarms being processed if no other instance is running
0478                 if (!initCheck(true))
0479                     exitCode = 1;
0480                 else
0481                 {
0482                     const auto action = static_cast<QueuedAction>(int(QueuedAction::List) | int(QueuedAction::Exit));
0483                     mActionQueue.enqueue(ActionQEntry(action, EventId()));
0484                     startProcessQueue(true);      // start processing the execution queue
0485                     dontRedisplay = true;
0486                 }
0487                 break;
0488 
0489             case CommandOptions::EDIT:
0490                 // Edit a specified existing alarm.
0491                 // Open the calendar and wait for the calendar resources to be populated.
0492                 if (!initCheck(false))
0493                     exitCode = 1;
0494                 else
0495                 {
0496                     mCommandOption = options->commandName();
0497                     if (firstInstance)
0498                         mEditingCmdLineAlarm = 0x10;   // want to redisplay alarms if successful
0499                     // Get the resource ID string and event UID. Note that if
0500                     // resources have not been created yet, the numeric
0501                     // resource ID can't yet be looked up.
0502                     mActionQueue.enqueue(ActionQEntry(QueuedAction::Edit, EventId(options->eventId()), options->resourceId()));
0503                     startProcessQueue(true);      // start processing the execution queue
0504                     dontRedisplay = true;
0505                 }
0506                 break;
0507 
0508             case CommandOptions::EDIT_NEW:
0509             {
0510                 // Edit a new alarm, and optionally preset selected values
0511                 if (!initCheck())
0512                     exitCode = 1;
0513                 else
0514                 {
0515                     EditAlarmDlg* editDlg = EditAlarmDlg::create(false, options->editType(), MainWindow::mainMainWindow());
0516                     if (!editDlg)
0517                     {
0518                         exitCode = 1;
0519                         break;
0520                     }
0521                     editDlg->setName(options->name());
0522                     if (options->alarmTime().isValid())
0523                         editDlg->setTime(options->alarmTime());
0524                     if (options->recurrence())
0525                         editDlg->setRecurrence(*options->recurrence(), options->subRepeatInterval(), options->subRepeatCount());
0526                     else if (options->flags() & KAEvent::REPEAT_AT_LOGIN)
0527                         editDlg->setRepeatAtLogin();
0528                     editDlg->setAction(options->editAction(), AlarmText(options->text()));
0529                     if (options->lateCancel())
0530                         editDlg->setLateCancel(options->lateCancel());
0531                     if (options->flags() & KAEvent::WAKE_SUSPEND)
0532                         editDlg->setWakeFromSuspend(true);
0533                     if (options->flags() & KAEvent::COPY_KORGANIZER)
0534                         editDlg->setShowInKOrganizer(true);
0535                     switch (options->editType())
0536                     {
0537                         case EditAlarmDlg::DISPLAY:
0538                         {
0539                             // EditAlarmDlg::create() always returns EditDisplayAlarmDlg for type = DISPLAY
0540                             auto dlg = qobject_cast<EditDisplayAlarmDlg*>(editDlg);
0541                             if (options->fgColour().isValid())
0542                                 dlg->setFgColour(options->fgColour());
0543                             if (options->bgColour().isValid())
0544                                 dlg->setBgColour(options->bgColour());
0545                             if (!options->audioFile().isEmpty()
0546                             ||  options->flags() & (KAEvent::BEEP | KAEvent::SPEAK))
0547                             {
0548                                 const KAEvent::Flags flags = options->flags();
0549                                 const Preferences::SoundType type = (flags & KAEvent::BEEP) ? Preferences::Sound_Beep
0550                                                                   : (flags & KAEvent::SPEAK) ? Preferences::Sound_Speak
0551                                                                   : Preferences::Sound_File;
0552                                 dlg->setAudio(type, options->audioFile(), options->audioVolume(), ((flags & KAEvent::REPEAT_SOUND) ? 0 : -1));
0553                             }
0554                             if (options->reminderMinutes())
0555                                 dlg->setReminder(options->reminderMinutes(), (options->flags() & KAEvent::REMINDER_ONCE));
0556                             if (options->flags() & KAEvent::NOTIFY)
0557                                 dlg->setNotify(true);
0558                             if (options->flags() & KAEvent::CONFIRM_ACK)
0559                                 dlg->setConfirmAck(true);
0560                             if (options->flags() & KAEvent::AUTO_CLOSE)
0561                                 dlg->setAutoClose(true);
0562                             break;
0563                         }
0564                         case EditAlarmDlg::COMMAND:
0565                             break;
0566                         case EditAlarmDlg::EMAIL:
0567                         {
0568                             // EditAlarmDlg::create() always returns EditEmailAlarmDlg for type = EMAIL
0569                             auto dlg = qobject_cast<EditEmailAlarmDlg*>(editDlg);
0570                             if (options->fromID()
0571                             ||  !options->addressees().isEmpty()
0572                             ||  !options->subject().isEmpty()
0573                             ||  !options->attachments().isEmpty())
0574                                 dlg->setEmailFields(options->fromID(), options->addressees(), options->subject(), options->attachments());
0575                             if (options->flags() & KAEvent::EMAIL_BCC)
0576                                 dlg->setBcc(true);
0577                             break;
0578                         }
0579                         case EditAlarmDlg::AUDIO:
0580                         {
0581                             // EditAlarmDlg::create() always returns EditAudioAlarmDlg for type = AUDIO
0582                             auto dlg = qobject_cast<EditAudioAlarmDlg*>(editDlg);
0583                             if (!options->audioFile().isEmpty()  ||  options->audioVolume() >= 0)
0584                                 dlg->setAudio(options->audioFile(), options->audioVolume());
0585                             break;
0586                         }
0587                         case EditAlarmDlg::NO_TYPE:
0588                             break;
0589                     }
0590 
0591                     // Execute the edit dialogue. Note that if no other instance of KAlarm is
0592                     // running, this new instance will not exit after the dialogue is closed.
0593                     // This is deliberate, since exiting would mean that KAlarm wouldn't
0594                     // trigger the new alarm.
0595                     KAlarm::execNewAlarmDlg(editDlg);
0596 
0597                     createOnlyMainWindow();   // prevent the application from quitting
0598                 }
0599                 break;
0600             }
0601             case CommandOptions::EDIT_NEW_PRESET:
0602                 // Edit a new alarm, preset with a template
0603                 if (!initCheck())
0604                     exitCode = 1;
0605                 else
0606                 {
0607                     // Execute the edit dialogue. Note that if no other instance of KAlarm is
0608                     // running, this new instance will not exit after the dialogue is closed.
0609                     // This is deliberate, since exiting would mean that KAlarm wouldn't
0610                     // trigger the new alarm.
0611                     KAlarm::editNewAlarm(options->name());
0612 
0613                     createOnlyMainWindow();   // prevent the application from quitting
0614                 }
0615                 break;
0616 
0617             case CommandOptions::NEW:
0618                 // Display a message or file, execute a command, or send an email
0619                 setResourcesTimeout();   // set timeout for resource initialisation
0620                 if (!initCheck())
0621                     exitCode = 1;
0622                 else
0623                 {
0624                     QueuedAction flags = QueuedAction::CmdLine;
0625                     if (!MainWindow::count())
0626                         flags = static_cast<QueuedAction>(int(flags) + int(QueuedAction::ErrorExit));
0627                     if (!scheduleEvent(flags,
0628                                        options->editAction(), options->name(), options->text(),
0629                                        options->alarmTime(), options->lateCancel(), options->flags(),
0630                                        options->bgColour(), options->fgColour(), QFont(),
0631                                        options->audioFile(), options->audioVolume(),
0632                                        options->reminderMinutes(), (options->recurrence() ? *options->recurrence() : KARecurrence()),
0633                                        options->subRepeatInterval(), options->subRepeatCount(),
0634                                        options->fromID(), options->addressees(),
0635                                        options->subject(), options->attachments()))
0636                         exitCode = 1;
0637                     else
0638                         createOnlyMainWindow();   // prevent the application from quitting
0639                 }
0640                 break;
0641 
0642             case CommandOptions::TRAY:
0643                 // Display only the system tray icon
0644                 if (Preferences::showInSystemTray()  &&  QSystemTrayIcon::isSystemTrayAvailable())
0645                 {
0646                     if (!initCheck())   // open the calendar, start processing execution queue
0647                         exitCode = 1;
0648                     else
0649                     {
0650                         if (!displayTrayIcon(true))
0651                             exitCode = 1;
0652                     }
0653                     break;
0654                 }
0655                 [[fallthrough]];   // fall through to NONE
0656             case CommandOptions::NONE:
0657                 // No arguments - run interactively & display the main window
0658 #ifndef NDEBUG
0659                 if (options->simulationTime().isValid()  &&  !firstInstance)
0660                     break;   // simulating time: don't open main window if already running
0661 #endif
0662                 if (!initCheck())
0663                     exitCode = 1;
0664                 else
0665                 {
0666                     if (mTrayWindow  &&  mTrayWindow->assocMainWindow()  &&  !mTrayWindow->assocMainWindow()->isVisible())
0667                         mTrayWindow->showAssocMainWindow();
0668                     else
0669                     {
0670                         MainWindow* win = MainWindow::create();
0671                         if (command == CommandOptions::TRAY)
0672                             win->setWindowState(win->windowState() | Qt::WindowMinimized);
0673                         win->show();
0674                     }
0675                 }
0676                 break;
0677             default:
0678                 break;
0679         }
0680     }
0681     if (options != CommandOptions::firstInstance())
0682         delete options;
0683 
0684     // If this is the first time through, redisplay any alarm message windows
0685     // from last time.
0686     if (firstInstance  &&  !dontRedisplay  &&  !exitCode)
0687     {
0688         /* First time through, so redisplay alarm message windows from last time.
0689          * But it is possible for session restoration in some circumstances to
0690          * not create any windows, in which case the alarm calendars will have
0691          * been deleted - if so, don't try to do anything. (This has been known
0692          * to happen under the Xfce desktop.)
0693          */
0694         if (ResourcesCalendar::instance())
0695         {
0696             mRedisplayAlarms = true;
0697             showRestoredWindows();
0698         }
0699     }
0700 
0701     --mActiveCount;
0702     firstInstance = false;
0703 
0704     // Quit the application if this was the last/only running "instance" of the program.
0705     // Executing 'return' doesn't work very well since the program continues to
0706     // run if no windows were created.
0707     if (quitIf(exitCode >= 0 ? exitCode : 0))
0708         return exitCode;    // exit this application instance
0709 
0710     return -1;   // continue executing the application instance
0711 }
0712 
0713 /******************************************************************************
0714 * Create a minimised main window if none already exists.
0715 * This prevents the application from quitting.
0716 */
0717 void KAlarmApp::createOnlyMainWindow()
0718 {
0719     if (!MainWindow::count())
0720     {
0721         if (wantShowInSystemTray())
0722         {
0723             if (displayTrayIcon(true))
0724                 return;
0725         }
0726         MainWindow* win = MainWindow::create();
0727         win->setWindowState(Qt::WindowMinimized);
0728         win->show();
0729     }
0730 }
0731 
0732 /******************************************************************************
0733 * Quit the program, optionally only if there are no more "instances" running.
0734 * Reply = true if program exited.
0735 */
0736 bool KAlarmApp::quitIf(int exitCode, bool force)
0737 {
0738     if (force)
0739     {
0740         // Quit regardless, except for message windows
0741         mQuitting = true;
0742         MainWindow::closeAll();
0743         mQuitting = false;
0744         displayTrayIcon(false);
0745         if (MessageDisplay::instanceCount(true))    // ignore always-hidden displays (e.g. audio alarms)
0746             return false;
0747     }
0748     else if (mQuitting)
0749         return false;   // MainWindow::closeAll() causes quitIf() to be called again
0750     else
0751     {
0752         // Quit only if there are no more "instances" running
0753         mPendingQuit = false;
0754         if (mActiveCount > 0  ||  MessageDisplay::instanceCount(true))  // ignore always-hidden displays (e.g. audio alarms)
0755             return false;
0756         const int mwcount = MainWindow::count();
0757         MainWindow* mw = mwcount ? MainWindow::firstWindow() : nullptr;
0758         if (mwcount > 1  ||  (mwcount && (!mw->isHidden() || !mw->isTrayParent())))
0759             return false;
0760         // There are no windows left except perhaps a main window which is a hidden
0761         // tray icon parent, or an always-hidden message window.
0762         if (mTrayWindow)
0763         {
0764             // There is a system tray icon.
0765             // Don't exit unless the system tray doesn't seem to exist.
0766             if (checkSystemTray())
0767                 return false;
0768         }
0769         if (!mActionQueue.isEmpty()  ||  !mCommandProcesses.isEmpty())
0770         {
0771             // Don't quit yet if there are outstanding actions on the execution queue
0772             mPendingQuit = true;
0773             mPendingQuitCode = exitCode;
0774             return false;
0775         }
0776     }
0777 
0778     // This was the last/only running "instance" of the program, so exit completely.
0779     // NOTE: Everything which is terminated/deleted here must where applicable
0780     //       be initialised in the initialiseTimerResources() method, in case
0781     //       KAlarm is started again before application exit completes!
0782     qCDebug(KALARM_LOG) << "KAlarmApp::quitIf:" << exitCode << ": quitting";
0783     MessageDisplay::stopAudio();
0784 #if ENABLE_RTC_WAKE_FROM_SUSPEND
0785     if (mCancelRtcWake)
0786     {
0787         // Just in case, cancel RTC wake even if kernel wake alarms are available
0788         KAlarm::setRtcWakeTime(0, nullptr);
0789         KAlarm::deleteRtcWakeConfig();
0790     }
0791 #endif
0792     delete mAlarmTimer;     // prevent checking for alarms after deleting calendars
0793     mAlarmTimer = nullptr;
0794     mInitialised = false;   // prevent processQueue() from running
0795     ResourcesCalendar::terminate();
0796     DisplayCalendar::terminate();
0797     DataModel::terminate();
0798     Q_EMIT setExitValue(exitCode);
0799     exit(exitCode);
0800     return true;    // sometimes we actually get to here, despite calling exit()
0801 }
0802 
0803 /******************************************************************************
0804 * Called when the Quit menu item is selected.
0805 * Closes the system tray window and all main windows, but does not exit the
0806 * program if other windows are still open.
0807 */
0808 void KAlarmApp::doQuit(QWidget* parent)
0809 {
0810     qCDebug(KALARM_LOG) << "KAlarmApp::doQuit";
0811     if (KAMessageBox::warningCancelContinue(parent,
0812                                             i18nc("@info", "Quitting will disable alarms (once any alarm message windows are closed)."),
0813                                             QString(), KStandardGuiItem::quit(),
0814                                             KStandardGuiItem::cancel(), Preferences::QUIT_WARN
0815                                            ) != KMessageBox::Continue)
0816         return;
0817 #if ENABLE_RTC_WAKE_FROM_SUSPEND
0818     if (!KAlarm::checkRtcWakeConfig(true).isEmpty())
0819     {
0820         // A wake-on-suspend alarm is set
0821         if (KAMessageBox::warningCancelContinue(parent,
0822                                                 i18nc("@info", "Quitting will cancel the scheduled Wake from Suspend."),
0823                                                 QString(), KStandardGuiItem::quit()
0824                                                ) != KMessageBox::Continue)
0825             return;
0826         mCancelRtcWake = true;
0827     }
0828 #endif
0829     if (!Preferences::autoStart())
0830     {
0831         int option = KMessageBox::ButtonCode::SecondaryAction;
0832         if (!Preferences::autoStartChangedByUser())
0833         {
0834             option = KAMessageBox::questionYesNoCancel(parent,
0835                                          xi18nc("@info", "Do you want to start KAlarm at login?<nl/>"
0836                                                         "(Note that alarms will be disabled if KAlarm is not started.)"),
0837                                          QString(), KGuiItem(i18n("Yes")), KGuiItem(i18n("No")),
0838                                          KStandardGuiItem::cancel(), Preferences::ASK_AUTO_START);
0839         }
0840         switch (option)
0841         {
0842             case KMessageBox::ButtonCode::PrimaryAction:
0843                 Preferences::setAutoStart(true);
0844                 Preferences::setNoAutoStart(false);
0845                 break;
0846             case KMessageBox::ButtonCode::SecondaryAction:
0847                 Preferences::setNoAutoStart(true);
0848                 break;
0849             case KMessageBox::Cancel:
0850             default:
0851                 return;
0852         }
0853         Preferences::self()->save();
0854     }
0855     quitIf(0, true);
0856 }
0857 
0858 /******************************************************************************
0859 * Display an error message for a fatal error. Prevent further actions since
0860 * the program state is unsafe.
0861 */
0862 void KAlarmApp::displayFatalError(const QString& message)
0863 {
0864     if (!mFatalError)
0865     {
0866         mFatalError = 1;
0867         mFatalMessage = message;
0868         if (mInstance)
0869             QTimer::singleShot(0, mInstance, &KAlarmApp::quitFatal);   //NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks)
0870     }
0871 }
0872 
0873 /******************************************************************************
0874 * Quit the program, once the fatal error message has been acknowledged.
0875 */
0876 void KAlarmApp::quitFatal()
0877 {
0878     switch (mFatalError)
0879     {
0880         case 0:
0881         case 2:
0882             return;
0883         case 1:
0884             mFatalError = 2;
0885             KMessageBox::error(nullptr, mFatalMessage);   // this is an application modal window
0886             mFatalError = 3;
0887             [[fallthrough]];   // fall through to '3'
0888         case 3:
0889             if (mInstance)
0890                 mInstance->quitIf(1, true);
0891             break;
0892     }
0893     QTimer::singleShot(1000, this, &KAlarmApp::quitFatal);   //NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks)
0894 }
0895 
0896 /******************************************************************************
0897 * Called by the alarm timer when the next alarm is due.
0898 * Also called when the execution queue has finished processing to check for the
0899 * next alarm.
0900 */
0901 void KAlarmApp::checkNextDueAlarm()
0902 {
0903     if (!mAlarmsEnabled)
0904         return;
0905     // Find the first alarm due
0906     KADateTime nextDt;
0907     const KAEvent nextEvent = ResourcesCalendar::earliestAlarm(nextDt, mNotificationsInhibited);
0908     if (!nextEvent.isValid())
0909         return;   // there are no alarms pending
0910     const KADateTime now = KADateTime::currentDateTime(Preferences::timeSpec());
0911     qint64 interval = now.msecsTo(nextDt);
0912     qCDebug(KALARM_LOG) << "KAlarmApp::checkNextDueAlarm: now:" << qPrintable(now.toString(QStringLiteral("%Y-%m-%d %H:%M %:Z"))) << ", next:" << qPrintable(nextDt.toString(QStringLiteral("%Y-%m-%d %H:%M %:Z"))) << ", due:" << interval;
0913     if (interval <= 0)
0914     {
0915         // Queue the alarm
0916         queueAlarmId(nextEvent);
0917         qCDebug(KALARM_LOG) << "KAlarmApp::checkNextDueAlarm:" << nextEvent.id() << ": due now";
0918         QTimer::singleShot(0, this, &KAlarmApp::processQueue);   //NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks)
0919     }
0920     else
0921     {
0922         // No alarm is due yet, so set timer to wake us when it's due.
0923         // Check for integer overflow before setting timer.
0924 #ifndef HIBERNATION_SIGNAL
0925         /* TODO: Use hibernation wakeup signal:
0926          *   #include <Solid/Power>
0927          *   connect(Solid::Power::self(), &Solid::Power::resumeFromSuspend, ...)
0928          *   (or resumingFromSuspend?)
0929          * to be notified when wakeup from hibernation occurs. But can't use it
0930          * unless we know that this notification is supported by the system!
0931          */
0932         /* Re-evaluate the next alarm time every minute, in case the
0933          * system clock jumps. The most common case when the clock jumps
0934          * is when a laptop wakes from hibernation. If timers were left to
0935          * run, they would trigger late by the length of time the system
0936          * was asleep.
0937          */
0938         if (interval > 60000)    // 1 minute
0939             interval = 60000;
0940 #endif
0941         ++interval;    // ensure we don't trigger just before the minute boundary
0942         if (interval > INT_MAX)
0943             interval = INT_MAX;
0944         qCDebug(KALARM_LOG) << "KAlarmApp::checkNextDueAlarm:" << nextEvent.id() << "wait" << interval/1000 << "seconds";
0945         mAlarmTimer->start(static_cast<int>(interval));
0946     }
0947 }
0948 
0949 /******************************************************************************
0950 * Called by the alarm timer when the next alarm is due.
0951 * Also called when the execution queue has finished processing to check for the
0952 * next alarm.
0953 */
0954 void KAlarmApp::queueAlarmId(const KAEvent& event)
0955 {
0956     const EventId id(event);
0957     for (const ActionQEntry& entry : std::as_const(mActionQueue))
0958     {
0959         if (entry.action == QueuedAction::Handle  &&  entry.eventId == id)
0960             return;  // the alarm is already queued
0961     }
0962     mActionQueue.enqueue(ActionQEntry(QueuedAction::Handle, id));
0963 }
0964 
0965 /******************************************************************************
0966 * Start processing the execution queue.
0967 */
0968 void KAlarmApp::startProcessQueue(bool evenIfStarted)
0969 {
0970     if (!mInitialised  ||  evenIfStarted)
0971     {
0972         qCDebug(KALARM_LOG) << "KAlarmApp::startProcessQueue";
0973         mInitialised = true;
0974         // Process anything already queued.
0975         QTimer::singleShot(0, this, &KAlarmApp::processQueue);   //NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks)
0976     }
0977 }
0978 
0979 /******************************************************************************
0980 * The main processing loop for KAlarm.
0981 * All KAlarm operations involving opening or updating calendar files are called
0982 * from this loop to ensure that only one operation is active at any one time.
0983 * This precaution is necessary because KAlarm's activities are mostly
0984 * asynchronous, being in response to D-Bus calls from other programs or timer
0985 * events, any of which can be received in the middle of performing another
0986 * operation. If a calendar file is opened or updated while another calendar
0987 * operation is in progress, the program has been observed to hang, or the first
0988 * calendar call has failed with data loss - clearly unacceptable!!
0989 */
0990 void KAlarmApp::processQueue()
0991 {
0992     if (mInitialised  &&  !mProcessingQueue)
0993     {
0994         qCDebug(KALARM_LOG) << "KAlarmApp::processQueue";
0995         mProcessingQueue = true;
0996 
0997         // Refresh alarms if that's been queued
0998         KAlarm::refreshAlarmsIfQueued();
0999 
1000         // Process queued events
1001         while (!mActionQueue.isEmpty())
1002         {
1003             ActionQEntry& entry = mActionQueue.head();
1004 
1005             // If the first action's resource ID is a string, can't process it
1006             // until its numeric resource ID can be found.
1007             if (!entry.resourceId.isEmpty())
1008             {
1009                 if (!Resources::allCreated())
1010                 {
1011                     // If resource population has timed out, discard all queued events.
1012                     if (mResourcesTimedOut)
1013                     {
1014                         qCCritical(KALARM_LOG) << "Error! Timeout creating calendars";
1015                         mActionQueue.clear();
1016                     }
1017                     break;
1018                 }
1019                 // Convert the resource ID string to the numeric resource ID.
1020                 entry.eventId.setResourceId(EventId::getResourceId(entry.resourceId));
1021                 entry.resourceId.clear();
1022             }
1023 
1024             // Can't process the first action until its resource has been populated.
1025             const ResourceId id = entry.eventId.resourceId();
1026             if ((id <  0 && !Resources::allPopulated())
1027             ||  (id >= 0 && !Resources::resource(id).isPopulated()))
1028             {
1029                 // If resource population has timed out, discard all queued events.
1030                 if (mResourcesTimedOut)
1031                 {
1032                     qCCritical(KALARM_LOG) << "Error! Timeout reading calendars";
1033                     mActionQueue.clear();
1034                 }
1035                 break;
1036             }
1037 
1038             // Process the first action in the queue.
1039             const bool findUniqueId   = int(entry.action) & int(QueuedAction::FindId);
1040             bool       exitAfter      = int(entry.action) & int(QueuedAction::Exit);
1041             const bool exitAfterError = int(entry.action) & int(QueuedAction::ErrorExit);
1042             const bool commandLine    = int(entry.action) & int(QueuedAction::CmdLine);
1043             const auto action = static_cast<QueuedAction>(int(entry.action) & int(QueuedAction::ActionMask));
1044 
1045             bool ok = true;
1046             bool inhibit = false;
1047             if (entry.eventId.isEmpty())
1048             {
1049                 // It's a new alarm
1050                 switch (action)
1051                 {
1052                     case QueuedAction::Trigger:
1053                         if (execAlarm(entry.event, entry.event.firstAlarm()) == (void*)-2)
1054                             inhibit = true;
1055                         break;
1056                     case QueuedAction::Handle:
1057                     {
1058                         Resource resource = Resources::destination(CalEvent::ACTIVE, nullptr, Resources::NoResourcePrompt | Resources::UseOnlyResource);
1059                         if (!resource.isValid())
1060                         {
1061                             qCWarning(KALARM_LOG) << "KAlarmApp::processQueue: Error! Cannot create alarm (no default calendar is defined)";
1062 
1063                             if (commandLine)
1064                             {
1065                                 const QString errmsg = xi18nc("@info:shell", "Cannot create alarm: No default calendar is defined");
1066                                 std::cerr << qPrintable(errmsg) << std::endl;
1067                             }
1068                             ok = false;
1069                         }
1070                         else
1071                         {
1072                             const KAlarm::UpdateResult result = KAlarm::addEvent(entry.event, resource, nullptr, KAlarm::ALLOW_KORG_UPDATE | KAlarm::NO_RESOURCE_PROMPT);
1073                             if (result >= KAlarm::UPDATE_ERROR)
1074                             {
1075 //TODO: display error message for command line action, but first ensure that one is returned by addEvent()!
1076 #if 0
1077                                 if (commandLine)
1078                                 {
1079                                     const QString errmsg = xi18nc("@info:shell", "Error creating alarm");
1080                                     std::cerr << errmsg.toLocal8Bit().data();
1081                                 }
1082 #endif
1083                                 ok = false;
1084                             }
1085                         }
1086                         if (!ok  &&  exitAfterError)
1087                             exitAfter = true;
1088                         break;
1089                     }
1090                     case QueuedAction::List:
1091                     {
1092                         const QStringList alarms = scheduledAlarmList();
1093                         for (const QString& alarm : alarms)
1094                             std::cout << qUtf8Printable(alarm) << std::endl;
1095                         break;
1096                     }
1097                     default:
1098                         break;
1099                 }
1100             }
1101             else
1102             {
1103                 if (action == QueuedAction::Edit)
1104                 {
1105                     int editingCmdLineAlarm = mEditingCmdLineAlarm & 3;
1106                     bool keepQueued = editingCmdLineAlarm <= 1;
1107                     switch (editingCmdLineAlarm)
1108                     {
1109                         case 0:
1110                             // Initiate editing an alarm specified on the command line.
1111                             mEditingCmdLineAlarm |= 1;
1112                             QTimer::singleShot(0, this, &KAlarmApp::slotEditAlarmById);   //NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks)
1113                             break;
1114                         case 1:
1115                             // Currently editing the alarm.
1116                             break;
1117                         case 2:
1118                             // The edit has completed.
1119                             mEditingCmdLineAlarm = 0;
1120                             break;
1121                         default:
1122                             break;
1123                     }
1124                     if (keepQueued)
1125                         break;
1126                 }
1127                 else
1128                 {
1129                     // Trigger the event if it's due.
1130                     const int result = handleEvent(entry.eventId, action, findUniqueId);
1131                     if (!result)
1132                         inhibit = true;
1133                     else if (result < 0  &&  exitAfter)
1134                     {
1135                         CommandOptions::printError(xi18nc("@info:shell", "%1: Event <resource>%2</resource> not found, or not unique", mCommandOption, entry.eventId.eventId()));
1136                         ok = false;
1137                     }
1138                 }
1139             }
1140 
1141             mActionQueue.dequeue();
1142 
1143             if (inhibit)
1144             {
1145                 // It's a display event which can't be executed because notifications
1146                 // are inhibited. Move it to the inhibited queue until the inhibition
1147                 // is removed.
1148             }
1149             else if (exitAfter)
1150             {
1151                 mProcessingQueue = false;   // don't inhibit processing if there is another instance
1152                 quitIf((ok ? 0 : 1), exitAfterError);
1153                 return;  // quitIf() can sometimes return, despite calling exit()
1154             }
1155         }
1156 
1157         // Purge the default archived alarms resource if it's time to do so
1158         if (mPurgeDaysQueued >= 0)
1159         {
1160             KAlarm::purgeArchive(mPurgeDaysQueued);
1161             mPurgeDaysQueued = -1;
1162         }
1163 
1164         // Now that the queue has been processed, quit if a quit was queued
1165         if (mPendingQuit)
1166         {
1167             if (quitIf(mPendingQuitCode))
1168                 return;  // quitIf() can sometimes return, despite calling exit()
1169         }
1170 
1171         mProcessingQueue = false;
1172 
1173         if (!mEditingCmdLineAlarm)
1174         {
1175             // Schedule the application to be woken when the next alarm is due
1176             checkNextDueAlarm();
1177         }
1178     }
1179 }
1180 
1181 /******************************************************************************
1182 * Called when a repeat-at-login alarm has been added externally.
1183 * Queues the alarm for triggering.
1184 * First, cancel any scheduled reminder or deferral for it, since these will be
1185 * superseded by the new at-login trigger.
1186 */
1187 void KAlarmApp::atLoginEventAdded(const KAEvent& event)
1188 {
1189     KAEvent ev = event;
1190     if (!cancelReminderAndDeferral(ev))
1191     {
1192         if (mAlarmsEnabled)
1193         {
1194             mActionQueue.enqueue(ActionQEntry(QueuedAction::Handle, EventId(ev)));
1195             if (mInitialised)
1196                 QTimer::singleShot(0, this, &KAlarmApp::processQueue);   //NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks)
1197         }
1198     }
1199 }
1200 
1201 /******************************************************************************
1202 * Called when the system tray main window is closed.
1203 */
1204 void KAlarmApp::removeWindow(TrayWindow*)
1205 {
1206     mTrayWindow = nullptr;
1207 }
1208 
1209 /******************************************************************************
1210 * Display or close the system tray icon.
1211 */
1212 bool KAlarmApp::displayTrayIcon(bool show, MainWindow* parent)
1213 {
1214     qCDebug(KALARM_LOG) << "KAlarmApp::displayTrayIcon";
1215     static bool creating = false;
1216     if (show)
1217     {
1218         if (!mTrayWindow  &&  !creating)
1219         {
1220             if (!QSystemTrayIcon::isSystemTrayAvailable())
1221                 return false;
1222             if (!MainWindow::count())
1223             {
1224                 // We have to have at least one main window to act
1225                 // as parent to the system tray icon (even if the
1226                 // window is hidden).
1227                 creating = true;    // prevent main window constructor from creating an additional tray icon
1228                 parent = MainWindow::create();
1229                 creating = false;
1230             }
1231             mTrayWindow = new TrayWindow(parent ? parent : MainWindow::firstWindow());
1232             connect(mTrayWindow, &TrayWindow::deleted, this, &KAlarmApp::trayIconToggled);
1233             Q_EMIT trayIconToggled();
1234 
1235             if (!checkSystemTray())
1236                 quitIf(0);    // exit the application if there are no open windows
1237         }
1238     }
1239     else
1240     {
1241         delete mTrayWindow;
1242         mTrayWindow = nullptr;
1243     }
1244     return true;
1245 }
1246 
1247 /******************************************************************************
1248 * Check whether the system tray icon has been housed in the system tray.
1249 */
1250 bool KAlarmApp::checkSystemTray()
1251 {
1252     if (!mTrayWindow)
1253         return true;
1254     if (QSystemTrayIcon::isSystemTrayAvailable() == mNoSystemTray)
1255     {
1256         qCDebug(KALARM_LOG) << "KAlarmApp::checkSystemTray: changed ->" << mNoSystemTray;
1257         mNoSystemTray = !mNoSystemTray;
1258 
1259         // Store the new setting in the config file, so that if KAlarm exits it will
1260         // restart with the correct default.
1261         KConfigGroup config(KSharedConfig::openConfig(), GENERAL_GROUP);
1262         config.writeEntry("NoSystemTray", mNoSystemTray);
1263         config.sync();
1264 
1265         // Update other settings
1266         slotShowInSystemTrayChanged();
1267     }
1268     return !mNoSystemTray;
1269 }
1270 
1271 /******************************************************************************
1272 * Return the main window associated with the system tray icon.
1273 */
1274 MainWindow* KAlarmApp::trayMainWindow() const
1275 {
1276     return mTrayWindow ? mTrayWindow->assocMainWindow() : nullptr;
1277 }
1278 
1279 /******************************************************************************
1280 * Called when the show-in-system-tray preference setting has changed, to show
1281 * or hide the system tray icon.
1282 */
1283 void KAlarmApp::slotShowInSystemTrayChanged()
1284 {
1285     const bool newShowInSysTray = wantShowInSystemTray();
1286     if (newShowInSysTray != mOldShowInSystemTray)
1287     {
1288         // The system tray run mode has changed
1289         ++mActiveCount;         // prevent the application from quitting
1290         MainWindow* win = mTrayWindow ? mTrayWindow->assocMainWindow() : nullptr;
1291         delete mTrayWindow;     // remove the system tray icon if it is currently shown
1292         mTrayWindow = nullptr;
1293         mOldShowInSystemTray = newShowInSysTray;
1294         if (newShowInSysTray)
1295         {
1296             // Show the system tray icon
1297             displayTrayIcon(true);
1298         }
1299         else
1300         {
1301             // Stop showing the system tray icon
1302             if (win  &&  win->isHidden())
1303             {
1304                 if (MainWindow::count() > 1)
1305                     delete win;
1306                 else
1307                 {
1308                     win->setWindowState(win->windowState() | Qt::WindowMinimized);
1309                     win->show();
1310                 }
1311             }
1312         }
1313         --mActiveCount;
1314     }
1315 }
1316 
1317 /******************************************************************************
1318 * Called when the start-of-day time preference setting has changed.
1319 * Change alarm times for date-only alarms.
1320 */
1321 void KAlarmApp::changeStartOfDay()
1322 {
1323     DateTime::setStartOfDay(Preferences::startOfDay());
1324     KAEvent::setStartOfDay(Preferences::startOfDay());
1325     Resources::adjustStartOfDay();
1326     DisplayCalendar::adjustStartOfDay();
1327 }
1328 
1329 /******************************************************************************
1330 * Called when the default alarm message font preference setting has changed.
1331 * Notify KAEvent.
1332 */
1333 void KAlarmApp::slotMessageFontChanged(const QFont& font)
1334 {
1335     KAEvent::setDefaultFont(font);
1336 }
1337 
1338 /******************************************************************************
1339 * Called when the working time preference settings have changed.
1340 * Notify KAEvent.
1341 */
1342 void KAlarmApp::slotWorkTimeChanged(const QTime& start, const QTime& end, const QBitArray& days)
1343 {
1344     KAEvent::setWorkTime(days, start, end, Preferences::timeSpec());
1345 }
1346 
1347 /******************************************************************************
1348 * Called when the holiday region preference setting has changed.
1349 * Notify KAEvent.
1350 */
1351 void KAlarmApp::slotHolidaysChanged(const KAlarmCal::Holidays& holidays)
1352 {
1353     KAEvent::setHolidays(holidays);
1354 }
1355 
1356 /******************************************************************************
1357 * Called when the date for February 29th recurrences has changed in the
1358 * preferences settings.
1359 */
1360 void KAlarmApp::slotFeb29TypeChanged(Preferences::Feb29Type type)
1361 {
1362     KARecurrence::Feb29Type rtype;
1363     switch (type)
1364     {
1365         default:
1366         case Preferences::Feb29_None:   rtype = KARecurrence::Feb29_None;  break;
1367         case Preferences::Feb29_Feb28:  rtype = KARecurrence::Feb29_Feb28;  break;
1368         case Preferences::Feb29_Mar1:   rtype = KARecurrence::Feb29_Mar1;  break;
1369     }
1370     KARecurrence::setDefaultFeb29Type(rtype);
1371 }
1372 
1373 /******************************************************************************
1374 * Return whether the program is configured to be running in the system tray.
1375 */
1376 bool KAlarmApp::wantShowInSystemTray() const
1377 {
1378     return Preferences::showInSystemTray()  &&  QSystemTrayIcon::isSystemTrayAvailable();
1379 }
1380 
1381 /******************************************************************************
1382 * Set a timeout for populating resources.
1383 */
1384 void KAlarmApp::setResourcesTimeout()
1385 {
1386     QTimer::singleShot(RESOURCES_TIMEOUT * 1000, this, &KAlarmApp::slotResourcesTimeout);   //NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks)
1387 }
1388 
1389 /******************************************************************************
1390 * Called on a timeout to check whether resources have been populated.
1391 * If not, exit the program with code 1.
1392 */
1393 void KAlarmApp::slotResourcesTimeout()
1394 {
1395     if (!Resources::allPopulated())
1396     {
1397         // Resource population has timed out.
1398         mResourcesTimedOut = true;
1399         quitIf(1);
1400     }
1401 }
1402 
1403 /******************************************************************************
1404 * Called when all resources have been created at startup.
1405 * Check whether there are any writable active calendars, and if not, warn the
1406 * user.
1407 * If alarms are being archived, check whether there is a default archived
1408 * calendar, and if not, warn the user.
1409 */
1410 void KAlarmApp::slotResourcesCreated()
1411 {
1412     showRestoredWindows();   // display message windows restored at startup.
1413     checkWritableCalendar();
1414     checkArchivedCalendar();
1415     processQueue();
1416 }
1417 
1418 /******************************************************************************
1419 * Called when all calendars have been fetched at startup, or calendar migration
1420 * has completed.
1421 * Check whether there are any writable active calendars, and if not, warn the
1422 * user.
1423 */
1424 void KAlarmApp::checkWritableCalendar()
1425 {
1426     if (mReadOnly)
1427         return;    // don't need write access to calendars
1428     if (!Resources::allCreated()
1429     ||  !DataModel::isMigrationComplete())
1430         return;
1431     static bool done = false;
1432     if (done)
1433         return;
1434     done = true;
1435     qCDebug(KALARM_LOG) << "KAlarmApp::checkWritableCalendar";
1436 
1437     // Check for, and remove, any duplicate resources, i.e. those which use the
1438     // same calendar file/directory.
1439     DataModel::removeDuplicateResources();
1440 
1441     // Find whether there are any writable active alarm calendars
1442     const bool active = !Resources::enabledResources(CalEvent::ACTIVE, true).isEmpty();
1443     if (!active)
1444     {
1445         qCWarning(KALARM_LOG) << "KAlarmApp::checkWritableCalendar: No writable active calendar";
1446         KAMessageBox::information(MainWindow::mainMainWindow(),
1447                                   xi18nc("@info", "Alarms cannot be created or updated, because no writable active alarm calendar is enabled.<nl/><nl/>"
1448                                                  "To fix this, use <interface>View | Show Calendars</interface> to check or change calendar statuses."),
1449                                   QString(), QStringLiteral("noWritableCal"));
1450     }
1451 }
1452 
1453 /******************************************************************************
1454 * If alarms are being archived, check whether there is a default archived
1455 * calendar, and if not, warn the user.
1456 */
1457 void KAlarmApp::checkArchivedCalendar()
1458 {
1459     static bool done = false;
1460     if (done)
1461         return;
1462     done = true;
1463 
1464     // If alarms are to be archived, check that the default archived alarm
1465     // calendar is writable.
1466     if (Preferences::archivedKeepDays())
1467     {
1468         Resource standard = Resources::getStandard(CalEvent::ARCHIVED, true);
1469         if (!standard.isValid())
1470         {
1471             // Schedule the display of a user prompt, without holding up
1472             // other processing.
1473             QTimer::singleShot(0, this, &KAlarmApp::promptArchivedCalendar);   //NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks)
1474         }
1475     }
1476 }
1477 
1478 /******************************************************************************
1479 * Edit an alarm specified on the command line.
1480 */
1481 void KAlarmApp::slotEditAlarmById()
1482 {
1483     qCDebug(KALARM_LOG) << "KAlarmApp::slotEditAlarmById";
1484     ActionQEntry& entry = mActionQueue.head();
1485     if (!KAlarm::editAlarmById(entry.eventId))
1486     {
1487         CommandOptions::printError(xi18nc("@info:shell", "%1: Event <resource>%2</resource> not found, or not editable", mCommandOption, entry.eventId.eventId()));
1488         mActionQueue.clear();
1489         quitIf(1);
1490     }
1491     else
1492     {
1493         createOnlyMainWindow();    // prevent the application from quitting
1494         if (mEditingCmdLineAlarm & 0x10)
1495         {
1496             mRedisplayAlarms = true;
1497             showRestoredWindows();
1498         }
1499         mEditingCmdLineAlarm = 2;  // indicate edit completion
1500         QTimer::singleShot(0, this, &KAlarmApp::processQueue);   //NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks)
1501     }
1502 }
1503 
1504 /******************************************************************************
1505 * If alarms are being archived, check whether there is a default archived
1506 * calendar, and if not, warn the user.
1507 */
1508 void KAlarmApp::promptArchivedCalendar()
1509 {
1510     const bool archived = !Resources::enabledResources(CalEvent::ARCHIVED, true).isEmpty();
1511     if (archived)
1512     {
1513         qCWarning(KALARM_LOG) << "KAlarmApp::checkArchivedCalendar: Archiving, but no writable archived calendar";
1514         KAMessageBox::information(MainWindow::mainMainWindow(),
1515                                   xi18nc("@info", "Alarms are configured to be archived, but this is not possible because no writable archived alarm calendar is enabled.<nl/><nl/>"
1516                                                  "To fix this, use <interface>View | Show Calendars</interface> to check or change calendar statuses."),
1517                                   QString(), QStringLiteral("noWritableArch"));
1518     }
1519     else
1520     {
1521         qCWarning(KALARM_LOG) << "KAlarmApp::checkArchivedCalendar: Archiving, but no standard archived calendar";
1522         KAMessageBox::information(MainWindow::mainMainWindow(),
1523                                   xi18nc("@info", "Alarms are configured to be archived, but this is not possible because no archived alarm calendar is set as default.<nl/><nl/>"
1524                                                  "To fix this, use <interface>View | Show Calendars</interface>, select an archived alarms calendar, and check <interface>Use as Default for Archived Alarms</interface>."),
1525                                   QString(), QStringLiteral("noStandardArch"));
1526     }
1527 }
1528 
1529 /******************************************************************************
1530 * Called when a new resource has been added, to note the possible need to purge
1531 * its old alarms if it is the default archived calendar.
1532 */
1533 void KAlarmApp::slotResourceAdded(const Resource& resource)
1534 {
1535     if (resource.alarmTypes() & CalEvent::ARCHIVED)
1536         mPendingPurges += resource.id();
1537 }
1538 
1539 /******************************************************************************
1540 * Called when a resource has been populated, to purge its old alarms if it is
1541 * the default archived calendar.
1542 */
1543 void KAlarmApp::slotResourcePopulated(const Resource& resource)
1544 {
1545     if (mPendingPurges.removeAll(resource.id()) > 0)
1546         purgeNewArchivedDefault(resource);
1547 }
1548 
1549 /******************************************************************************
1550 * Called when a new resource has been populated, or when a resource has been
1551 * set as the standard resource for its type.
1552 * If it is the default archived calendar, purge its old alarms if necessary.
1553 */
1554 void KAlarmApp::purgeNewArchivedDefault(const Resource& resource)
1555 {
1556     if (Resources::isStandard(resource, CalEvent::ARCHIVED))
1557     {
1558         qCDebug(KALARM_LOG) << "KAlarmApp::purgeNewArchivedDefault:" << resource.displayId() << ": standard archived...";
1559         if (mArchivedPurgeDays >= 0)
1560             purge(mArchivedPurgeDays);
1561         else
1562             setArchivePurgeDays();
1563     }
1564 }
1565 
1566 /******************************************************************************
1567 * Called when the length of time to keep archived alarms changes in KAlarm's
1568 * preferences.
1569 * Set the number of days to keep archived alarms.
1570 * Alarms which are older are purged immediately, and at the start of each day.
1571 */
1572 void KAlarmApp::setArchivePurgeDays()
1573 {
1574     const int newDays = Preferences::archivedKeepDays();
1575     if (newDays != mArchivedPurgeDays)
1576     {
1577         const int oldDays = mArchivedPurgeDays;
1578         mArchivedPurgeDays = newDays;
1579         if (mArchivedPurgeDays <= 0)
1580             StartOfDayTimer::disconnect(this);
1581         if (mArchivedPurgeDays < 0)
1582             return;   // keep indefinitely, so don't purge
1583         if (oldDays < 0  ||  mArchivedPurgeDays < oldDays)
1584         {
1585             // Alarms are now being kept for less long, so purge them
1586             purge(mArchivedPurgeDays);
1587             if (!mArchivedPurgeDays)
1588                 return;   // don't archive any alarms
1589         }
1590         // Start the purge timer to expire at the start of the next day
1591         // (using the user-defined start-of-day time).
1592         StartOfDayTimer::connect(this, SLOT(slotPurge()));
1593     }
1594 }
1595 
1596 /******************************************************************************
1597 * Purge all archived events from the calendar whose end time is longer ago than
1598 * 'daysToKeep'. All events are deleted if 'daysToKeep' is zero.
1599 */
1600 void KAlarmApp::purge(int daysToKeep)
1601 {
1602     if (mPurgeDaysQueued < 0  ||  daysToKeep < mPurgeDaysQueued)
1603         mPurgeDaysQueued = daysToKeep;
1604 
1605     // Do the purge once any other current operations are completed
1606     processQueue();
1607 }
1608 
1609 /******************************************************************************
1610 * Called when the Freedesktop notifications properties have changed.
1611 * Check whether the inhibited property has changed.
1612 */
1613 void KAlarmApp::slotFDOPropertiesChanged(const QString& interface,
1614                                          const QVariantMap& changedProperties,
1615                                          const QStringList& invalidatedProperties)
1616 {
1617     Q_UNUSED(interface);     // always "org.freedesktop.Notifications"
1618     Q_UNUSED(invalidatedProperties);
1619     const auto it = changedProperties.find(QStringLiteral("Inhibited"));
1620     if (it != changedProperties.end())
1621     {
1622         const bool inhibited = it.value().toBool();
1623         if (inhibited != mNotificationsInhibited)
1624         {
1625             qCDebug(KALARM_LOG) << "KAlarmApp::slotFDOPropertiesChanged: Notifications inhibited ->" << inhibited;
1626             mNotificationsInhibited = inhibited;
1627             if (!mNotificationsInhibited)
1628             {
1629                 showRestoredWindows();   // display message windows restored at startup.
1630                 QTimer::singleShot(0, this, &KAlarmApp::processQueue);   //NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks)
1631             }
1632         }
1633     }
1634 }
1635 
1636 /******************************************************************************
1637 * Output a list of pending alarms, with their next scheduled occurrence.
1638 */
1639 QStringList KAlarmApp::scheduledAlarmList()
1640 {
1641     QStringList alarms;
1642     const QList<KAEvent> events = KAlarm::getSortedActiveEvents(this);
1643     for (const KAEvent& event : events)
1644     {
1645         const KADateTime dateTime = event.nextTrigger(KAEvent::Trigger::Display).effectiveKDateTime().toLocalZone();
1646         const Resource resource = Resources::resource(event.resourceId());
1647         QString text(resource.configName() + QLatin1String(":"));
1648         text += event.id() + QLatin1Char(' ')
1649              +  dateTime.toString(QStringLiteral("%Y%m%dT%H%M "))
1650              +  AlarmText::summary(event, 1);
1651         alarms << text;
1652     }
1653     return alarms;
1654 }
1655 
1656 /******************************************************************************
1657 * Enable or disable alarm monitoring.
1658 */
1659 void KAlarmApp::setAlarmsEnabled(bool enabled)
1660 {
1661     if (enabled != mAlarmsEnabled)
1662     {
1663         mAlarmsEnabled = enabled;
1664         Q_EMIT alarmEnabledToggled(enabled);
1665         if (!enabled)
1666 #if ENABLE_RTC_WAKE_FROM_SUSPEND
1667             KAlarm::cancelRtcWake(nullptr);
1668 #else
1669             ;
1670 #endif
1671         else if (!mProcessingQueue)
1672             checkNextDueAlarm();
1673     }
1674 }
1675 
1676 /******************************************************************************
1677 * Spread or collect alarm message and error message windows.
1678 */
1679 void KAlarmApp::spreadWindows(bool spread)
1680 {
1681     if (!KWindowSystem::isPlatformWayland())  // Wayland doesn't allow positioning of windows
1682     {
1683         spread = MessageWindow::spread(spread);
1684         Q_EMIT spreadWindowsToggled(spread);
1685     }
1686 }
1687 
1688 /******************************************************************************
1689 * Called when the spread status of message windows changes.
1690 * Set the 'spread windows' action state.
1691 */
1692 void KAlarmApp::setSpreadWindowsState(bool spread)
1693 {
1694     if (!KWindowSystem::isPlatformWayland())  // Wayland doesn't allow positioning of windows
1695         Q_EMIT spreadWindowsToggled(spread);
1696 }
1697 
1698 /******************************************************************************
1699 * Check whether the window manager's handling of keyboard focus transfer
1700 * between application windows is broken. This is true for Ubuntu's Unity
1701 * desktop, where MessageWindow windows steal keyboard focus from EditAlarmDlg
1702 * windows.
1703 */
1704 bool KAlarmApp::windowFocusBroken() const
1705 {
1706     return mWindowFocusBroken;
1707 }
1708 
1709 /******************************************************************************
1710 * Check whether window/keyboard focus currently needs to be fixed manually due
1711 * to the window manager not handling it correctly. This will occur if there are
1712 * both EditAlarmDlg and MessageWindow windows currently active.
1713 */
1714 bool KAlarmApp::needWindowFocusFix() const
1715 {
1716     return mWindowFocusBroken && MessageWindow::windowCount(true) && EditAlarmDlg::instanceCount();
1717 }
1718 
1719 /******************************************************************************
1720 * Called to schedule a new alarm, either in response to a D-Bus notification or
1721 * to command line options.
1722 * Reply = true unless there was a parameter error or an error opening calendar file.
1723 */
1724 bool KAlarmApp::scheduleEvent(QueuedAction queuedActionFlags,
1725                               KAEvent::SubAction action, const QString& name, const QString& text,
1726                               const KADateTime& dateTime, int lateCancel, KAEvent::Flags flags,
1727                               const QColor& bg, const QColor& fg, const QFont& font,
1728                               const QString& audioFile, float audioVolume, int reminderMinutes,
1729                               const KARecurrence& recurrence, const KCalendarCore::Duration& repeatInterval, int repeatCount,
1730                               uint mailFromID, const KCalendarCore::Person::List& mailAddresses,
1731                               const QString& mailSubject, const QStringList& mailAttachments)
1732 {
1733     if (!dateTime.isValid())
1734     {
1735         qCWarning(KALARM_LOG) << "KAlarmApp::scheduleEvent: Error! Invalid time" << text;
1736         return false;
1737     }
1738     const KADateTime now = KADateTime::currentUtcDateTime();
1739     if (lateCancel  &&  dateTime < now.addSecs(-maxLateness(lateCancel)))
1740     {
1741         qCDebug(KALARM_LOG) << "KAlarmApp::scheduleEvent: not executed (late-cancel)" << text;
1742         return true;               // alarm time was already archived too long ago
1743     }
1744     KADateTime alarmTime = dateTime;
1745     // Round down to the nearest minute to avoid scheduling being messed up
1746     if (!dateTime.isDateOnly())
1747         alarmTime.setTime(QTime(alarmTime.time().hour(), alarmTime.time().minute(), 0));
1748 
1749     KAEvent event(alarmTime, name, text, bg, fg, font, action, lateCancel, flags, true);
1750     if (reminderMinutes)
1751     {
1752         const bool onceOnly = flags & KAEvent::REMINDER_ONCE;
1753         event.setReminder(reminderMinutes, onceOnly);
1754     }
1755     if (!audioFile.isEmpty())
1756         event.setAudioFile(audioFile, audioVolume, -1, 0, (flags & KAEvent::REPEAT_SOUND) ? 0 : -1);
1757     if (!mailAddresses.isEmpty())
1758         event.setEmail(mailFromID, mailAddresses, mailSubject, mailAttachments);
1759     event.setRecurrence(recurrence);
1760     event.setFirstRecurrence();
1761     event.setRepetition(Repetition(repeatInterval, repeatCount - 1));
1762     event.endChanges();
1763     if (alarmTime <= now)
1764     {
1765         // Alarm is due for execution already.
1766         // First execute it once without adding it to the calendar file.
1767         qCDebug(KALARM_LOG) << "KAlarmApp::scheduleEvent: executing" << text;
1768         if (!mInitialised
1769         ||  execAlarm(event, event.firstAlarm()) == (void*)-2)
1770             mActionQueue.enqueue(ActionQEntry(event, QueuedAction::Trigger));
1771         // If it's a recurring alarm, reschedule it for its next occurrence
1772         if (!event.recurs()
1773         ||  event.setNextOccurrence(now) == KAEvent::OccurType::None)
1774             return true;
1775         // It has recurrences in the future
1776     }
1777 
1778     // Queue the alarm for insertion into the calendar file
1779     qCDebug(KALARM_LOG) << "KAlarmApp::scheduleEvent: creating new alarm" << text;
1780     const QueuedAction qaction = static_cast<QueuedAction>(int(QueuedAction::Handle) + int(queuedActionFlags));
1781     mActionQueue.enqueue(ActionQEntry(event, qaction));
1782     if (mInitialised)
1783         QTimer::singleShot(0, this, &KAlarmApp::processQueue);   //NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks)
1784     return true;
1785 }
1786 
1787 /******************************************************************************
1788 * Called in response to a D-Bus request to trigger or cancel an event.
1789 * Optionally display the event. Delete the event from the calendar file and
1790 * from every main window instance.
1791 */
1792 bool KAlarmApp::dbusHandleEvent(const EventId& eventID, QueuedAction action)
1793 {
1794     qCDebug(KALARM_LOG) << "KAlarmApp::dbusHandleEvent:" << eventID;
1795     mActionQueue.append(ActionQEntry(action, eventID));
1796     if (mInitialised)
1797         QTimer::singleShot(0, this, &KAlarmApp::processQueue);   //NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks)
1798     return true;
1799 }
1800 
1801 /******************************************************************************
1802 * Called in response to a D-Bus request to list all pending alarms.
1803 */
1804 QString KAlarmApp::dbusList()
1805 {
1806     qCDebug(KALARM_LOG) << "KAlarmApp::dbusList";
1807     return scheduledAlarmList().join(QLatin1Char('\n')) + QLatin1Char('\n');
1808 }
1809 
1810 /******************************************************************************
1811 * Either:
1812 * a) Execute the event if it's due, and then delete it if it has no outstanding
1813 *    repetitions.
1814 * b) Delete the event.
1815 * c) Reschedule the event for its next repetition. If none remain, delete it.
1816 * If the event is deleted, it is removed from the calendar file and from every
1817 * main window instance.
1818 * If 'findUniqueId' is true and 'id' does not specify a resource, all resources
1819 * will be searched for the event's unique ID.
1820 * Reply = -1 if event ID not found, or if more than one event with the same ID
1821 *            is found.
1822 *       =  0 if can't trigger display event because notifications are inhibited.
1823 *       =  1 if success.
1824 */
1825 int KAlarmApp::handleEvent(const EventId& id, QueuedAction action, bool findUniqueId)
1826 {
1827     Q_ASSERT(!(int(action) & ~int(QueuedAction::ActionMask)));
1828 
1829 #if ENABLE_RTC_WAKE_FROM_SUSPEND
1830     // Delete any expired wake-on-suspend config data
1831     KAlarm::checkRtcWakeConfig();
1832 #endif
1833 
1834     const QString eventID(id.eventId());
1835     KAEvent event = ResourcesCalendar::event(id, findUniqueId);
1836     if (!event.isValid())
1837     {
1838         if (id.resourceId() != -1)
1839             qCWarning(KALARM_LOG) << "KAlarmApp::handleEvent: Event ID not found:" << eventID;
1840         else if (findUniqueId)
1841             qCWarning(KALARM_LOG) << "KAlarmApp::handleEvent: Event ID not found, or duplicated:" << eventID;
1842         else
1843             qCCritical(KALARM_LOG) << "KAlarmApp::handleEvent: No resource ID specified for event:" << eventID;
1844         return -1;
1845     }
1846     switch (action)
1847     {
1848         case QueuedAction::Cancel:
1849         {
1850             qCDebug(KALARM_LOG) << "KAlarmApp::handleEvent:" << eventID << ", CANCEL";
1851             Resource resource = Resources::resource(event.resourceId());
1852             KAlarm::deleteEvent(event, resource, true);
1853             break;
1854         }
1855         case QueuedAction::Trigger:    // handle it if it's due, else execute it regardless
1856         case QueuedAction::Handle:     // handle it if it's due
1857         {
1858             const KADateTime now = KADateTime::currentUtcDateTime();
1859             qCDebug(KALARM_LOG) << "KAlarmApp::handleEvent:" << eventID << "," << (action==QueuedAction::Trigger?"TRIGGER:":"HANDLE:") << qPrintable(now.qDateTime().toString(QStringLiteral("yyyy-MM-dd hh:mm"))) << "UTC";
1860             bool updateCalAndDisplay = false;
1861             bool alarmToExecuteValid = false;
1862             KAAlarm alarmToExecute;
1863             bool restart = false;
1864             // Check all the alarms in turn.
1865             // Note that the main alarm is fetched before any other alarms.
1866             for (KAAlarm alarm = event.firstAlarm();
1867                  alarm.isValid();
1868                  alarm = (restart ? event.firstAlarm() : event.nextAlarm(alarm)), restart = false)
1869             {
1870                 // Check if the alarm is due yet.
1871                 const KADateTime nextDT = alarm.dateTime(true).effectiveKDateTime();
1872                 const int secs = nextDT.secsTo(now);
1873                 if (secs < 0)
1874                 {
1875                     // The alarm appears to be in the future.
1876                     // Check if it's an invalid local time during a daylight
1877                     // saving time shift, which has actually passed.
1878                     if (alarm.dateTime().timeSpec() != KADateTime::LocalZone
1879                     ||  nextDT > now.toTimeSpec(KADateTime::LocalZone))
1880                     {
1881                         // This alarm is definitely not due yet
1882                         qCDebug(KALARM_LOG) << "KAlarmApp::handleEvent: Alarm" << alarm.type() << "at" << nextDT.qDateTime() << ": not due";
1883                         continue;
1884                     }
1885                 }
1886                 bool reschedule = false;
1887                 bool rescheduleWork = false;
1888                 if ((event.workTimeOnly() || event.holidaysExcluded())  &&  !alarm.deferred())
1889                 {
1890                     // The alarm is restricted to working hours and/or non-holidays
1891                     // (apart from deferrals). This needs to be re-evaluated every
1892                     // time it triggers, since working hours could change.
1893                     if (alarm.dateTime().isDateOnly())
1894                     {
1895                         KADateTime dt(nextDT);
1896                         dt.setDateOnly(true);
1897                         reschedule = event.excludedByWorkTimeOrHoliday(dt);
1898                     }
1899                     else
1900                         reschedule = event.excludedByWorkTimeOrHoliday(nextDT);
1901                     rescheduleWork = reschedule;
1902                     if (reschedule)
1903                         qCDebug(KALARM_LOG) << "KAlarmApp::handleEvent: Alarm" << alarm.type() << "at" << nextDT.qDateTime() << ": not during working hours";
1904                 }
1905                 if (!reschedule  &&  alarm.repeatAtLogin())
1906                 {
1907                     // Alarm is to be displayed at every login.
1908                     qCDebug(KALARM_LOG) << "KAlarmApp::handleEvent: REPEAT_AT_LOGIN";
1909                     // Check if the main alarm is already being displayed.
1910                     // (We don't want to display both at the same time.)
1911                     if (alarmToExecute.isValid())
1912                         continue;
1913 
1914                     // Set the time to display if it's a display alarm
1915                     alarm.setTime(now);
1916                 }
1917                 if (!reschedule  &&  event.lateCancel())
1918                 {
1919                     // Alarm is due, and it is to be cancelled if too late.
1920                     qCDebug(KALARM_LOG) << "KAlarmApp::handleEvent: LATE_CANCEL";
1921                     bool cancel = false;
1922                     if (alarm.dateTime().isDateOnly())
1923                     {
1924                         // The alarm has no time, so cancel it if its date is too far past
1925                         const int maxlate = event.lateCancel() / 1440;    // maximum lateness in days
1926                         KADateTime limit(DateTime(nextDT.addDays(maxlate + 1)).effectiveKDateTime());
1927                         if (now >= limit)
1928                         {
1929                             // It's too late to display the scheduled occurrence.
1930                             // Find the last previous occurrence of the alarm.
1931                             DateTime next;
1932                             const KAEvent::OccurType type = event.previousOccurrence(now, next, true);
1933                             switch (static_cast<KAEvent::OccurType>(type & ~KAEvent::OccurType::Repeat))
1934                             {
1935                                 case KAEvent::OccurType::FirstOrOnly:
1936                                 case KAEvent::OccurType::RecurDate:
1937                                 case KAEvent::OccurType::RecurDateTime:
1938                                 case KAEvent::OccurType::LastRecur:
1939                                     limit.setDate(next.date().addDays(maxlate + 1));
1940                                     if (now >= limit)
1941                                     {
1942                                         if (type == KAEvent::OccurType::LastRecur
1943                                         ||  (type == KAEvent::OccurType::FirstOrOnly && !event.recurs()))
1944                                             cancel = true;   // last occurrence (and there are no repetitions)
1945                                         else
1946                                             reschedule = true;
1947                                     }
1948                                     break;
1949                                 case KAEvent::OccurType::None:
1950                                 default:
1951                                     reschedule = true;
1952                                     break;
1953                             }
1954                         }
1955                     }
1956                     else
1957                     {
1958                         // The alarm is timed. Allow it to be the permitted amount late before cancelling it.
1959                         const int maxlate = maxLateness(event.lateCancel());
1960                         if (secs > maxlate)
1961                         {
1962                             // It's over the maximum interval late.
1963                             // Find the most recent occurrence of the alarm.
1964                             DateTime next;
1965                             const KAEvent::OccurType type = event.previousOccurrence(now, next, true);
1966                             switch (static_cast<KAEvent::OccurType>(type & ~KAEvent::OccurType::Repeat))
1967                             {
1968                                 case KAEvent::OccurType::FirstOrOnly:
1969                                 case KAEvent::OccurType::RecurDate:
1970                                 case KAEvent::OccurType::RecurDateTime:
1971                                 case KAEvent::OccurType::LastRecur:
1972                                     if (next.effectiveKDateTime().secsTo(now) > maxlate)
1973                                     {
1974                                         if (type == KAEvent::OccurType::LastRecur
1975                                         ||  (type == KAEvent::OccurType::FirstOrOnly && !event.recurs()))
1976                                             cancel = true;   // last occurrence (and there are no repetitions)
1977                                         else
1978                                             reschedule = true;
1979                                     }
1980                                     break;
1981                                 case KAEvent::OccurType::None:
1982                                 default:
1983                                     reschedule = true;
1984                                     break;
1985                             }
1986                         }
1987                     }
1988 
1989                     if (cancel)
1990                     {
1991                         // All recurrences are finished, so cancel the event
1992                         event.setArchive();
1993                         if (cancelAlarm(event, alarm.type(), false))
1994                             return 1;   // event has been deleted
1995                         updateCalAndDisplay = true;
1996                         continue;
1997                     }
1998                 }
1999                 if (reschedule)
2000                 {
2001                     // The latest repetition was too long ago, so schedule the next one
2002                     switch (rescheduleAlarm(event, alarm, false, (rescheduleWork ? nextDT : KADateTime())))
2003                     {
2004                         case 1:
2005                             // A working-time-only alarm has been rescheduled and the
2006                             // rescheduled time is already due. Start processing the
2007                             // event again.
2008                             alarmToExecuteValid = false;
2009                             restart = true;
2010                             break;
2011                         case -1:
2012                             return 1;   // event has been deleted
2013                         default:
2014                             break;
2015                     }
2016                     updateCalAndDisplay = true;
2017                     continue;
2018                 }
2019                 if (!alarmToExecuteValid)
2020                 {
2021                     qCDebug(KALARM_LOG) << "KAlarmApp::handleEvent: Alarm" << alarm.type() << ": execute";
2022                     alarmToExecute = alarm;             // note the alarm to be displayed
2023                     alarmToExecuteValid = true;         // only trigger one alarm for the event
2024                 }
2025                 else
2026                     qCDebug(KALARM_LOG) << "KAlarmApp::handleEvent: Alarm" << alarm.type() << ": skip";
2027             }
2028 
2029             // If there is an alarm to execute, do this last after rescheduling/cancelling
2030             // any others. This ensures that the updated event is only saved once to the calendar.
2031             if (alarmToExecute.isValid())
2032             {
2033                 if (execAlarm(event, alarmToExecute, Reschedule | (alarmToExecute.repeatAtLogin() ? NoExecFlag : AllowDefer)) == (void*)-2)
2034                     return 0;    // display alarm, but notifications are inhibited
2035             }
2036             else
2037             {
2038                 if (action == QueuedAction::Trigger)
2039                 {
2040                     // The alarm is to be executed regardless of whether it's due.
2041                     // Only trigger one alarm from the event - we don't want multiple
2042                     // identical messages, for example.
2043                     const KAAlarm alarm = event.firstAlarm();
2044                     if (alarm.isValid())
2045                     {
2046                         if (execAlarm(event, alarm) == (void*)-2)
2047                             return 0;    // display alarm, but notifications are inhibited
2048                     }
2049                 }
2050                 if (updateCalAndDisplay)
2051                     KAlarm::updateEvent(event);     // update the window lists and calendar file
2052                 else if (action != QueuedAction::Trigger) { qCDebug(KALARM_LOG) << "KAlarmApp::handleEvent: No action"; }
2053             }
2054             break;
2055         }
2056         default:
2057             break;
2058     }
2059     return 1;
2060 }
2061 
2062 /******************************************************************************
2063 * Called when an alarm action has completed, to perform any post-alarm actions.
2064 */
2065 void KAlarmApp::alarmCompleted(const KAEvent& event)
2066 {
2067     if (!event.postAction().isEmpty())
2068     {
2069         // doShellCommand() will error if the user is not authorised to run
2070         // shell commands.
2071         const QString command = event.postAction();
2072         qCDebug(KALARM_LOG) << "KAlarmApp::alarmCompleted:" << event.id() << ":" << command;
2073         doShellCommand(command, event, nullptr, ProcData::POST_ACTION);
2074     }
2075 }
2076 
2077 /******************************************************************************
2078 * Reschedule the alarm for its next recurrence after now. If none remain,
2079 * delete it.  If the alarm is deleted and it is the last alarm for its event,
2080 * the event is removed from the calendar file and from every main window
2081 * instance.
2082 * If 'nextDt' is valid, the event is rescheduled for the next non-working
2083 * time occurrence after that.
2084 * Reply = 1 if 'nextDt' is valid and the rescheduled event is already due
2085 *       = -1 if the event has been deleted
2086 *       = 0 otherwise.
2087 */
2088 int KAlarmApp::rescheduleAlarm(KAEvent& event, const KAAlarm& alarm, bool updateCalAndDisplay, const KADateTime& nextDt)
2089 {
2090     qCDebug(KALARM_LOG) << "KAlarmApp::rescheduleAlarm: Alarm type:" << alarm.type();
2091     int reply = 0;
2092     bool update = false;
2093     event.startChanges();
2094     if (alarm.repeatAtLogin())
2095     {
2096         // Leave an alarm which repeats at every login until its main alarm triggers
2097         if (!event.reminderActive()  &&  event.reminderMinutes() < 0)
2098         {
2099             // Executing an at-login alarm: first schedule the reminder
2100             // which occurs AFTER the main alarm.
2101             event.activateReminderAfter(KADateTime::currentUtcDateTime());
2102         }
2103         // Repeat-at-login alarms are usually unchanged after triggering.
2104         // Ensure that the archive flag (which was set in execAlarm()) is saved.
2105         update = true;
2106     }
2107     else if (alarm.isReminder()  ||  alarm.deferred())
2108     {
2109         // It's a reminder alarm or an extra deferred alarm, so delete it
2110         event.removeExpiredAlarm(alarm.type());
2111         update = true;
2112     }
2113     else
2114     {
2115         // Reschedule the alarm for its next occurrence.
2116         bool cancelled = false;
2117         DateTime last = event.mainDateTime(false);   // note this trigger time
2118         if (last != event.mainDateTime(true))
2119             last = DateTime();                       // but ignore sub-repetition triggers
2120         bool next = nextDt.isValid();
2121         KADateTime next_dt = nextDt;
2122         const KADateTime now = KADateTime::currentUtcDateTime();
2123         do
2124         {
2125             const KAEvent::OccurType type = event.setNextOccurrence(next ? next_dt : now);
2126             switch (type)
2127             {
2128                 case KAEvent::OccurType::None:
2129                     // All repetitions are finished, so cancel the event
2130                     qCDebug(KALARM_LOG) << "KAlarmApp::rescheduleAlarm: No occurrence";
2131                     if (event.reminderMinutes() < 0  &&  last.isValid()
2132                     &&  alarm.type() != KAAlarm::Type::AtLogin  &&  !event.mainExpired())
2133                     {
2134                         // Set the reminder which is now due after the last main alarm trigger.
2135                         // Note that at-login reminders are scheduled in execAlarm().
2136                         event.activateReminderAfter(last);
2137                         updateCalAndDisplay = true;
2138                     }
2139                     if (cancelAlarm(event, alarm.type(), updateCalAndDisplay))
2140                         return -1;
2141                     break;
2142                 default:
2143                     if (!(type & KAEvent::OccurType::Repeat))
2144                         break;
2145                     // Next occurrence is a repeat, so fall through to recurrence handling
2146                     [[fallthrough]];
2147                 case KAEvent::OccurType::RecurDate:
2148                 case KAEvent::OccurType::RecurDateTime:
2149                 case KAEvent::OccurType::LastRecur:
2150                     // The event is due by now and repetitions still remain, so rewrite the event
2151                     if (updateCalAndDisplay)
2152                         update = true;
2153                     break;
2154                 case KAEvent::OccurType::FirstOrOnly:
2155                     // The first occurrence is still due?!?, so don't do anything
2156                     break;
2157             }
2158             if (cancelled)
2159                 break;
2160             if (event.deferred())
2161             {
2162                 // Just in case there's also a deferred alarm, ensure it's removed
2163                 event.removeExpiredAlarm(KAAlarm::Type::Deferred);
2164                 update = true;
2165             }
2166             if (next)
2167             {
2168                 // The alarm is restricted to working hours and/or non-holidays.
2169                 // Check if the calculated next time is valid.
2170                 next_dt = event.mainDateTime(true).effectiveKDateTime();
2171                 if (event.mainDateTime(false).isDateOnly())
2172                 {
2173                     KADateTime dt(next_dt);
2174                     dt.setDateOnly(true);
2175                     next = event.excludedByWorkTimeOrHoliday(dt);
2176                 }
2177                 else
2178                     next = event.excludedByWorkTimeOrHoliday(next_dt);
2179             }
2180         } while (next && next_dt <= now);
2181         reply = (!cancelled && next_dt.isValid() && (next_dt <= now)) ? 1 : 0;
2182 
2183         if (event.reminderMinutes() < 0  &&  last.isValid()
2184         &&  alarm.type() != KAAlarm::Type::AtLogin)
2185         {
2186             // Set the reminder which is now due after the last main alarm trigger.
2187             // Note that at-login reminders are scheduled in execAlarm().
2188             event.activateReminderAfter(last);
2189         }
2190     }
2191     event.endChanges();
2192     if (update)
2193         KAlarm::updateEvent(event, nullptr, true, false);   // update the window lists and calendar file
2194     return reply;
2195 }
2196 
2197 /******************************************************************************
2198 * Delete the alarm. If it is the last alarm for its event, the event is removed
2199 * from the calendar file and from every main window instance.
2200 * Reply = true if event has been deleted.
2201 */
2202 bool KAlarmApp::cancelAlarm(KAEvent& event, KAAlarm::Type alarmType, bool updateCalAndDisplay)
2203 {
2204     qCDebug(KALARM_LOG) << "KAlarmApp::cancelAlarm";
2205     if (alarmType == KAAlarm::Type::Main  &&  !event.displaying()  &&  event.toBeArchived())
2206     {
2207         // The event is being deleted. Save it in the archived resource first.
2208         Resource resource;
2209         KAEvent ev(event);
2210         KAlarm::addArchivedEvent(ev, resource);
2211     }
2212     event.removeExpiredAlarm(alarmType);
2213     if (!event.alarmCount())
2214     {
2215         // If it's a command alarm being executed, mark it as deleted
2216         ProcData* pd = findCommandProcess(event.id());
2217         if (pd)
2218             pd->eventDeleted = true;
2219 
2220         // Delete it
2221         Resource resource;
2222         KAlarm::deleteEvent(event, resource, false);
2223         return true;
2224     }
2225     if (updateCalAndDisplay)
2226         KAlarm::updateEvent(event);    // update the window lists and calendar file
2227     return false;
2228 }
2229 
2230 /******************************************************************************
2231 * Cancel any reminder or deferred alarms in an repeat-at-login event.
2232 * This should be called when the event is first loaded.
2233 * If there are no more alarms left in the event, the event is removed from the
2234 * calendar file and from every main window instance.
2235 * Reply = true if event has been deleted.
2236 */
2237 bool KAlarmApp::cancelReminderAndDeferral(KAEvent& event)
2238 {
2239     return cancelAlarm(event, KAAlarm::Type::Reminder, false)
2240        ||  cancelAlarm(event, KAAlarm::Type::DeferredReminder, false)
2241        ||  cancelAlarm(event, KAAlarm::Type::Deferred, true);
2242 }
2243 
2244 /******************************************************************************
2245 * Execute an alarm by displaying its message or file, or executing its command.
2246 * Reply = ShellProcess instance if a command alarm
2247 *       = MessageWindow if an audio alarm
2248 *       != null if successful
2249 *       = -1 if execution has not completed
2250 *       = -2 if can't execute display event because notifications are inhibited.
2251 *       = null if the alarm is disabled, or if an error message was output.
2252 */
2253 void* KAlarmApp::execAlarm(KAEvent& event, const KAAlarm& alarm, ExecAlarmFlags flags)
2254 {
2255     if (!mAlarmsEnabled  ||  !event.enabled())
2256     {
2257         // The event (or all events) is disabled
2258         qCDebug(KALARM_LOG) << "KAlarmApp::execAlarm:" << event.id() << ": disabled";
2259         if (flags & Reschedule)
2260             rescheduleAlarm(event, alarm, true);
2261         return nullptr;
2262     }
2263 
2264     if (mNotificationsInhibited  &&  !(flags & NoNotifyInhibit)
2265     &&  (event.actionTypes() & KAEvent::Action::Display))
2266     {
2267         // It's a display event and notifications are inhibited.
2268         qCDebug(KALARM_LOG) << "KAlarmApp::execAlarm:" << event.id() << ": notifications inhibited";
2269         return (void*)-2;
2270     }
2271 
2272     void* result = (void*)1;
2273     event.setArchive();
2274 
2275     switch (alarm.action())
2276     {
2277         case KAAlarm::Action::Command:
2278             if (!event.commandDisplay())
2279             {
2280                 // execCommandAlarm() will error if the user is not authorised
2281                 // to run shell commands.
2282                 result = execCommandAlarm(event, alarm, flags & NoRecordCmdError);
2283                 if (flags & Reschedule)
2284                     rescheduleAlarm(event, alarm, true);
2285                 break;
2286             }
2287             [[fallthrough]];   // fall through to Message
2288         case KAAlarm::Action::Message:
2289         case KAAlarm::Action::File:
2290         {
2291             // Display a message, file or command output, provided that the same event
2292             // isn't already being displayed
2293             MessageDisplay* disp = MessageDisplay::findEvent(EventId(event));
2294             // Find if we're changing a reminder message to the real message
2295             const bool reminder = (alarm.type() & KAAlarm::Type::Reminder);
2296             const bool replaceReminder = !reminder && disp && (disp->alarmType() & KAAlarm::Type::Reminder);
2297             if (!reminder
2298             &&  (!event.deferred() || (event.extraActionOptions() & KAEvent::ExecPreActOnDeferral))
2299             &&  (replaceReminder || !disp)  &&  !(flags & NoPreAction)
2300             &&  !event.preAction().isEmpty())
2301             {
2302                 // It's not a reminder alarm, and it's not a deferred alarm unless the
2303                 // pre-alarm action applies to deferred alarms, and there is no message
2304                 // window (other than a reminder window) currently displayed for this
2305                 // alarm, and we need to execute a command before displaying the new window.
2306                 //
2307                 // NOTE: The pre-action is not executed for a recurring alarm if an
2308                 // alarm message window for a previous occurrence is still visible.
2309                 // Check whether the command is already being executed for this alarm.
2310                 for (const ProcData* pd : std::as_const(mCommandProcesses))
2311                 {
2312                     if (pd->event->id() == event.id()  &&  (pd->flags & ProcData::PRE_ACTION))
2313                     {
2314                         qCDebug(KALARM_LOG) << "KAlarmApp::execAlarm: Already executing pre-DISPLAY command";
2315                         return pd->process;   // already executing - don't duplicate the action
2316                     }
2317                 }
2318 
2319                 // doShellCommand() will error if the user is not authorised to run
2320                 // shell commands.
2321                 const QString command = event.preAction();
2322                 qCDebug(KALARM_LOG) << "KAlarmApp::execAlarm: Pre-DISPLAY command:" << command;
2323                 const int pdFlags = (flags & Reschedule ? ProcData::RESCHEDULE : 0) | (flags & AllowDefer ? ProcData::ALLOW_DEFER : 0);
2324                 if (doShellCommand(command, event, &alarm, (pdFlags | ProcData::PRE_ACTION)))
2325                 {
2326                     ResourcesCalendar::setAlarmPending(event);
2327                     return result;     // display the message after the command completes
2328                 }
2329                 // Error executing command
2330                 if (event.extraActionOptions() & KAEvent::CancelOnPreActError)
2331                 {
2332                     // Cancel the rest of the alarm execution
2333                     qCDebug(KALARM_LOG) << "KAlarmApp::execAlarm:" << event.id() << ": pre-action failed: cancelled";
2334                     if (flags & Reschedule)
2335                         rescheduleAlarm(event, alarm, true);
2336                     return nullptr;
2337                 }
2338                 // Display the message even though it failed
2339             }
2340 
2341             if (!disp)
2342             {
2343                 // There isn't already a message for this event
2344                 const int mdFlags = (flags & Reschedule ? 0 : MessageDisplay::NoReschedule)
2345                                   | (flags & AllowDefer ? 0 : MessageDisplay::NoDefer)
2346                                   | (flags & NoRecordCmdError ? MessageDisplay::NoRecordCmdError : 0);
2347                 MessageDisplay::create(event, alarm, mdFlags)->showDisplay();
2348             }
2349             else if (replaceReminder)
2350             {
2351                 // The caption needs to be changed from "Reminder" to "Message"
2352                 disp->cancelReminder(event, alarm);
2353             }
2354             else if (!disp->hasDefer() && event.repeatAtLogin() && !alarm.repeatAtLogin())
2355             {
2356                 // It's a repeat-at-login message with no Defer button,
2357                 // which has now reached its final trigger time and needs
2358                 // to be replaced with a new message.
2359                 disp->showDefer();
2360                 disp->showDateTime(event, alarm);
2361             }
2362             else
2363             {
2364                 // Use the existing message window
2365             }
2366             if (disp)
2367             {
2368                 // Raise the existing message window and replay any sound
2369                 disp->repeat(alarm);    // N.B. this reschedules the alarm
2370             }
2371             break;
2372         }
2373         case KAAlarm::Action::Email:
2374         {
2375             qCDebug(KALARM_LOG) << "KAlarmApp::execAlarm: EMAIL to:" << event.emailAddresses(QStringLiteral(","));
2376             QStringList errmsgs;
2377             MailSend::JobData data(event, alarm, flags & Reschedule, flags & (Reschedule | AllowDefer));
2378             data.queued = true;
2379             int ans = KAMail::send(data, errmsgs);
2380             if (ans)
2381             {
2382                 // The email has either been sent or failed - not queued
2383                 if (ans < 0)
2384                     result = nullptr;  // failure
2385                 data.queued = false;
2386                 emailSent(data, errmsgs, (ans > 0));
2387             }
2388             else
2389             {
2390                 result = (void*)-1;   // email has been queued
2391             }
2392             if (flags & Reschedule)
2393                 rescheduleAlarm(event, alarm, true);
2394             break;
2395         }
2396         case KAAlarm::Action::Audio:
2397         {
2398             // Play the sound, provided that the same event
2399             // isn't already playing
2400             MessageDisplay* disp = MessageDisplay::findEvent(EventId(event));
2401             if (!disp)
2402             {
2403                 // There isn't already a message for this event.
2404                 const int mdFlags = (flags & Reschedule ? 0 : MessageDisplay::NoReschedule) | MessageDisplay::AlwaysHide;
2405                 event.setNotify(false);   // can't use notification system if audio only
2406                 disp = MessageDisplay::create(event, alarm, mdFlags);
2407             }
2408             else
2409             {
2410                 // There's an existing message window: replay the sound
2411                 disp->repeat(alarm);    // N.B. this reschedules the alarm
2412             }
2413             return dynamic_cast<MessageWindow*>(disp);
2414         }
2415         default:
2416             return nullptr;
2417     }
2418     return result;
2419 }
2420 
2421 /******************************************************************************
2422 * Called when sending an email has completed.
2423 */
2424 void KAlarmApp::emailSent(const MailSend::JobData& data, const QStringList& errmsgs, bool copyerr)
2425 {
2426     if (!errmsgs.isEmpty())
2427     {
2428         // Some error occurred, although the email may have been sent successfully
2429         if (errmsgs.count() > 1)
2430             qCDebug(KALARM_LOG) << "KAlarmApp::emailSent:" << (copyerr ? "Copy error:" : "Failed:") << errmsgs[1];
2431         MessageDisplay::showError(data.event, data.alarm.dateTime(), errmsgs);
2432     }
2433     else if (data.queued)
2434         Q_EMIT execAlarmSuccess();
2435 }
2436 
2437 /******************************************************************************
2438 * Execute the command specified in a command alarm.
2439 * To connect to the output ready signals of the process, specify a slot to be
2440 * called by supplying 'receiver' and 'slot' parameters.
2441 */
2442 ShellProcess* KAlarmApp::execCommandAlarm(const KAEvent& event, const KAAlarm& alarm, bool noRecordError,
2443                                           QObject* receiver, const char* slotOutput, const char* methodExited)
2444 {
2445     // doShellCommand() will error if the user is not authorised to run
2446     // shell commands.
2447     const int flags = (event.commandXterm()   ? ProcData::EXEC_IN_XTERM : 0)
2448                     | (event.commandDisplay() ? ProcData::DISP_OUTPUT : 0)
2449                     | (noRecordError          ? ProcData::NO_RECORD_ERROR : 0);
2450     const QString command = event.cleanText();
2451     if (event.commandScript())
2452     {
2453         // Store the command script in a temporary file for execution
2454         qCDebug(KALARM_LOG) << "KAlarmApp::execCommandAlarm: Script";
2455         const QString tmpfile = createTempScriptFile(command, false, event, alarm);
2456         if (tmpfile.isEmpty())
2457         {
2458             setEventCommandError(event, KAEvent::CmdErr::Fail);
2459             return nullptr;
2460         }
2461         return doShellCommand(tmpfile, event, &alarm, (flags | ProcData::TEMP_FILE), receiver, slotOutput, methodExited);
2462     }
2463     else
2464     {
2465         qCDebug(KALARM_LOG) << "KAlarmApp::execCommandAlarm:" << command;
2466         return doShellCommand(command, event, &alarm, flags, receiver, slotOutput, methodExited);
2467     }
2468 }
2469 
2470 /******************************************************************************
2471 * Execute a shell command line specified by an alarm.
2472 * If the PRE_ACTION bit of 'flags' is set, the alarm will be executed via
2473 * execAlarm() once the command completes, the execAlarm() parameters being
2474 * derived from the remaining bits in 'flags'.
2475 * 'flags' must contain the bit PRE_ACTION or POST_ACTION if and only if it is
2476 * a pre- or post-alarm action respectively.
2477 * To connect to the exited signal of the process, specify the name of a method
2478 * to be called by supplying 'receiver' and 'methodExited' parameters.
2479 * To connect to the output ready signals of the process, specify a slot to be
2480 * called by supplying 'receiver' and 'slotOutput' parameters.
2481 *
2482 * Note that if shell access is not authorised, the attempt to run the command
2483 * will be errored.
2484 *
2485 * Reply = process which has been started, or null if a process couldn't be started.
2486 */
2487 ShellProcess* KAlarmApp::doShellCommand(const QString& command, const KAEvent& event, const KAAlarm* alarm, int flags, QObject* receiver, const char* slotOutput, const char* methodExited)
2488 {
2489     qCDebug(KALARM_LOG) << "KAlarmApp::doShellCommand:" << command << "," << event.id();
2490     QIODevice::OpenMode mode = QIODevice::WriteOnly;
2491     QString cmd;
2492     QString tmpXtermFile;
2493     if (flags & ProcData::EXEC_IN_XTERM)
2494     {
2495         // Execute the command in a terminal window.
2496         cmd = composeXTermCommand(command, event, alarm, flags, tmpXtermFile);
2497         if (cmd.isEmpty())
2498         {
2499             qCWarning(KALARM_LOG) << "KAlarmApp::doShellCommand: Command failed (no terminal selected)";
2500             const QStringList errors{i18nc("@info", "Failed to execute command\n(no terminal selected for command alarms)")};
2501             commandErrorMsg(nullptr, event, alarm, flags, errors);
2502             return nullptr;
2503         }
2504     }
2505     else
2506     {
2507         cmd = command;
2508         mode = QIODevice::ReadWrite;
2509     }
2510 
2511     ProcData* pd = nullptr;
2512     ShellProcess* proc = nullptr;
2513     if (!cmd.isEmpty())
2514     {
2515         // Use ShellProcess, which automatically checks whether the user is
2516         // authorised to run shell commands.
2517         proc = new ShellProcess(cmd);
2518         proc->setEnv(QStringLiteral("KALARM_UID"), event.id(), true);
2519         proc->setOutputChannelMode(KProcess::MergedChannels);   // combine stdout & stderr
2520         connect(proc, &ShellProcess::shellExited, this, &KAlarmApp::slotCommandExited);
2521         if ((flags & ProcData::DISP_OUTPUT)  &&  receiver && slotOutput)
2522         {
2523             connect(proc, SIGNAL(receivedStdout(ShellProcess*)), receiver, slotOutput);
2524             connect(proc, SIGNAL(receivedStderr(ShellProcess*)), receiver, slotOutput);
2525         }
2526         if (mode == QIODevice::ReadWrite  &&  !event.logFile().isEmpty())
2527         {
2528             // Output is to be appended to a log file.
2529             // Set up a logging process to write the command's output to.
2530             QString heading;
2531             if (alarm  &&  alarm->dateTime().isValid())
2532             {
2533                 const QString dateTime = alarm->dateTime().formatLocale();
2534                 heading = QStringLiteral("\n******* KAlarm %1 *******\n").arg(dateTime);
2535             }
2536             else
2537                 heading = QStringLiteral("\n******* KAlarm *******\n");
2538             QFile logfile(event.logFile());
2539             if (logfile.open(QIODevice::Append | QIODevice::Text))
2540             {
2541                 QTextStream out(&logfile);
2542                 out << heading;
2543                 logfile.close();
2544             }
2545             proc->setStandardOutputFile(event.logFile(), QIODevice::Append);
2546         }
2547         pd = new ProcData(proc, new KAEvent(event), (alarm ? new KAAlarm(*alarm) : nullptr), flags);
2548         if (flags & ProcData::TEMP_FILE)
2549             pd->tempFiles += command;
2550         if (!tmpXtermFile.isEmpty())
2551             pd->tempFiles += tmpXtermFile;
2552         if (receiver && methodExited)
2553         {
2554             pd->exitReceiver = receiver;
2555             pd->exitMethod   = methodExited;
2556         }
2557         mCommandProcesses.append(pd);
2558         if (proc->start(mode))
2559             return proc;
2560     }
2561 
2562     // Error executing command - report it
2563     qCWarning(KALARM_LOG) << "KAlarmApp::doShellCommand: Command failed to start";
2564     commandErrorMsg(proc, event, alarm, flags);
2565     if (pd)
2566     {
2567         mCommandProcesses.removeAt(mCommandProcesses.indexOf(pd));
2568         delete pd;
2569     }
2570     return nullptr;
2571 }
2572 
2573 /******************************************************************************
2574 * Compose a command line to execute the given command in a terminal window.
2575 * 'tempScriptFile' receives the name of a temporary script file which is
2576 * invoked by the command line, if applicable.
2577 * Reply = command line, or empty string if error.
2578 */
2579 QString KAlarmApp::composeXTermCommand(const QString& command, const KAEvent& event, const KAAlarm* alarm, int flags, QString& tempScriptFile) const
2580 {
2581     qCDebug(KALARM_LOG) << "KAlarmApp::composeXTermCommand:" << command << "," << event.id();
2582     tempScriptFile.clear();
2583     QString cmd = Preferences::cmdXTermCommand();
2584     if (cmd.isEmpty())
2585         return {};   // no terminal application is configured
2586     cmd.replace(QLatin1String("%t"), KAboutData::applicationData().displayName());  // set the terminal window title
2587     if (cmd.indexOf(QLatin1String("%C")) >= 0)
2588     {
2589         // Execute the command from a temporary script file
2590         if (flags & ProcData::TEMP_FILE)
2591             cmd.replace(QLatin1String("%C"), command);    // the command is already calling a temporary file
2592         else
2593         {
2594             tempScriptFile = createTempScriptFile(command, true, event, *alarm);
2595             if (tempScriptFile.isEmpty())
2596                 return {};
2597             cmd.replace(QLatin1String("%C"), tempScriptFile);    // %C indicates where to insert the command
2598         }
2599     }
2600     else if (cmd.indexOf(QLatin1String("%W")) >= 0)
2601     {
2602         // Execute the command from a temporary script file,
2603         // with a sleep after the command is executed
2604         tempScriptFile = createTempScriptFile(command + QLatin1String("\nsleep 86400\n"), true, event, *alarm);
2605         if (tempScriptFile.isEmpty())
2606             return {};
2607         cmd.replace(QLatin1String("%W"), tempScriptFile);    // %w indicates where to insert the command
2608     }
2609     else if (cmd.indexOf(QLatin1String("%w")) >= 0)
2610     {
2611         // Append a sleep to the command.
2612         // Quote the command in case it contains characters such as [>|;].
2613         const QString exec = KShell::quoteArg(command + QLatin1String("; sleep 86400"));
2614         cmd.replace(QLatin1String("%w"), exec);    // %w indicates where to insert the command string
2615     }
2616     else
2617     {
2618         // Set the command to execute.
2619         // Put it in quotes in case it contains characters such as [>|;].
2620         const QString exec = KShell::quoteArg(command);
2621         if (cmd.indexOf(QLatin1String("%c")) >= 0)
2622             cmd.replace(QLatin1String("%c"), exec);    // %c indicates where to insert the command string
2623         else
2624             cmd.append(exec);           // otherwise, simply append the command string
2625     }
2626     return cmd;
2627 }
2628 
2629 /******************************************************************************
2630 * Create a temporary script file containing the specified command string.
2631 * Reply = path of temporary file, or null string if error.
2632 */
2633 QString KAlarmApp::createTempScriptFile(const QString& command, bool insertShell, const KAEvent& event, const KAAlarm& alarm) const
2634 {
2635     QTemporaryFile tmpFile;
2636     tmpFile.setAutoRemove(false);     // don't delete file when it is destructed
2637     if (!tmpFile.open())
2638         qCCritical(KALARM_LOG) << "Unable to create a temporary script file";
2639     else
2640     {
2641         tmpFile.setPermissions(QFile::ReadUser | QFile::WriteUser | QFile::ExeUser);
2642         QTextStream stream(&tmpFile);
2643         if (insertShell)
2644             stream << "#!" << ShellProcess::shellPath() << "\n";
2645         stream << command;
2646         stream.flush();
2647         if (tmpFile.error() != QFile::NoError)
2648             qCCritical(KALARM_LOG) << "Error" << tmpFile.errorString() << " writing to temporary script file";
2649         else
2650             return tmpFile.fileName();
2651     }
2652 
2653     const QStringList errmsgs(i18nc("@info", "Error creating temporary script file"));
2654     MessageDisplay::showError(event, alarm.dateTime(), errmsgs, QStringLiteral("Script"));
2655     return {};
2656 }
2657 
2658 /******************************************************************************
2659 * Called when a command alarm's execution completes.
2660 */
2661 void KAlarmApp::slotCommandExited(ShellProcess* proc)
2662 {
2663     qCDebug(KALARM_LOG) << "KAlarmApp::slotCommandExited";
2664     // Find this command in the command list
2665     for (int i = 0, end = mCommandProcesses.count();  i < end;  ++i)
2666     {
2667         ProcData* pd = mCommandProcesses.at(i);
2668         if (pd->process == proc)
2669         {
2670             // Found the command. Check its exit status.
2671             bool executeAlarm = pd->preAction();
2672             const ShellProcess::Status status = proc->status();
2673             if (status == ShellProcess::Status::Success  &&  !proc->exitCode())
2674             {
2675                 qCDebug(KALARM_LOG) << "KAlarmApp::slotCommandExited:" << pd->event->id() << ": SUCCESS";
2676                 clearEventCommandError(*pd->event, pd->preAction() ? KAEvent::CmdErr::Pre
2677                                                  : pd->postAction() ? KAEvent::CmdErr::Post
2678                                                  : KAEvent::CmdErr::Fail);
2679             }
2680             else
2681             {
2682                 QString errmsg = proc->errorMessage();
2683                 if (status == ShellProcess::Status::Success  ||  status == ShellProcess::Status::NotFound)
2684                     qCWarning(KALARM_LOG) << "KAlarmApp::slotCommandExited:" << pd->event->id() << ":" << errmsg << "exit status =" << status << ", code =" << proc->exitCode();
2685                 else
2686                     qCWarning(KALARM_LOG) << "KAlarmApp::slotCommandExited:" << pd->event->id() << ":" << errmsg << "exit status =" << status;
2687                 if (pd->messageBoxParent)
2688                 {
2689                     // Close the existing informational KMessageBox for this process
2690                     const QList<QDialog*> dialogs = pd->messageBoxParent->findChildren<QDialog*>();
2691                     if (!dialogs.isEmpty())
2692                         delete dialogs[0];
2693                     setEventCommandError(*pd->event, pd->preAction() ? KAEvent::CmdErr::Pre
2694                                                    : pd->postAction() ? KAEvent::CmdErr::Post
2695                                                    : KAEvent::CmdErr::Fail);
2696                     if (!pd->tempFile())
2697                     {
2698                         errmsg += QLatin1Char('\n');
2699                         errmsg += proc->command();
2700                     }
2701                     KAMessageBox::error(pd->messageBoxParent, errmsg);
2702                 }
2703                 else
2704                     commandErrorMsg(proc, *pd->event, pd->alarm, pd->flags);
2705 
2706                 if (executeAlarm
2707                 &&  (pd->event->extraActionOptions() & KAEvent::CancelOnPreActError))
2708                 {
2709                     qCDebug(KALARM_LOG) << "KAlarmApp::slotCommandExited:" << pd->event->id() << ": pre-action failed: cancelled";
2710                     if (pd->reschedule())
2711                         rescheduleAlarm(*pd->event, *pd->alarm, true);
2712                     executeAlarm = false;
2713                 }
2714             }
2715             if (pd->preAction())
2716                 ResourcesCalendar::setAlarmPending(*pd->event, false);
2717             if (executeAlarm)
2718             {
2719                 execAlarm(*pd->event, *pd->alarm,   (pd->reschedule()     ? Reschedule : NoExecFlag)
2720                                                   | (pd->allowDefer()     ? AllowDefer : NoExecFlag)
2721                                                   | (pd->noRecordCmdErr() ? NoRecordCmdError : NoExecFlag)
2722                                                   | NoPreAction);
2723             }
2724             mCommandProcesses.removeAt(i);
2725             if (pd->exitReceiver && !pd->exitMethod.isEmpty())
2726                 QMetaObject::invokeMethod(pd->exitReceiver, pd->exitMethod.constData(), Qt::DirectConnection, Q_ARG(ShellProcess::Status, status));
2727             delete pd;
2728             break;
2729         }
2730     }
2731 
2732     // If there are now no executing shell commands, quit if a quit was queued
2733     if (mPendingQuit  &&  mCommandProcesses.isEmpty())
2734         quitIf(mPendingQuitCode);
2735 }
2736 
2737 /******************************************************************************
2738 * Output an error message for a shell command, and record the alarm's error status.
2739 */
2740 void KAlarmApp::commandErrorMsg(const ShellProcess* proc, const KAEvent& event, const KAAlarm* alarm, int flags, const QStringList& errors)
2741 {
2742     KAEvent::CmdErr cmderr;
2743     QString dontShowAgain;
2744     QStringList errmsgs = errors;
2745     if (flags & ProcData::PRE_ACTION)
2746     {
2747         if (event.extraActionOptions() & KAEvent::DontShowPreActError)
2748             return;   // don't notify user of any errors for the alarm
2749         errmsgs += i18nc("@info", "Pre-alarm action:");
2750         dontShowAgain = QStringLiteral("Pre");
2751         cmderr = KAEvent::CmdErr::Pre;
2752     }
2753     else if (flags & ProcData::POST_ACTION)
2754     {
2755         errmsgs += i18nc("@info", "Post-alarm action:");
2756         dontShowAgain = QStringLiteral("Post");
2757         cmderr = (event.commandError() == KAEvent::CmdErr::Pre)
2758                ? KAEvent::CmdErr::PrePost : KAEvent::CmdErr::Post;
2759     }
2760     else
2761     {
2762         if (!event.commandHideError())
2763             dontShowAgain = QStringLiteral("Exec");
2764         cmderr = KAEvent::CmdErr::Fail;
2765     }
2766 
2767     // Record the alarm's error status
2768     if (!(flags & ProcData::NO_RECORD_ERROR))
2769         setEventCommandError(event, cmderr);
2770 
2771     if (!dontShowAgain.isEmpty())
2772     {
2773         // Display an error message
2774         if (proc)
2775         {
2776             errmsgs += proc->errorMessage();
2777             if (!(flags & ProcData::TEMP_FILE))
2778                 errmsgs += proc->command();
2779             dontShowAgain += QString::number(static_cast<int>(proc->status()));
2780         }
2781         MessageDisplay::showError(event, (alarm ? alarm->dateTime() : DateTime()), errmsgs, dontShowAgain);
2782     }
2783 }
2784 
2785 /******************************************************************************
2786 * Notes that an informational KMessageBox is displayed for this process.
2787 */
2788 void KAlarmApp::commandMessage(ShellProcess* proc, QWidget* parent)
2789 {
2790     // Find this command in the command list
2791     for (ProcData* pd : std::as_const(mCommandProcesses))
2792     {
2793         if (pd->process == proc)
2794         {
2795             pd->messageBoxParent = parent;
2796             break;
2797         }
2798     }
2799 }
2800 
2801 /******************************************************************************
2802 * If this is the first time through, open the calendar file, and start
2803 * processing the execution queue.
2804 */
2805 bool KAlarmApp::initCheck(bool calendarOnly)
2806 {
2807     static bool firstTime = true;
2808     if (firstTime)
2809         qCDebug(KALARM_LOG) << "KAlarmApp::initCheck: first time";
2810 
2811     if (initialiseTimerResources()  ||  firstTime)
2812     {
2813         /* Need to open the display calendar now, since otherwise if display
2814          * alarms are immediately due, they will often be processed while
2815          * MessageDisplay::redisplayAlarms() is executing open() (but before
2816          * open() completes), which causes problems!!
2817          */
2818         DisplayCalendar::open();
2819     }
2820     if (firstTime)
2821     {
2822         setArchivePurgeDays();
2823 
2824         firstTime = false;
2825     }
2826 
2827     if (!calendarOnly)
2828         startProcessQueue();      // start processing the execution queue
2829 
2830     return true;
2831 }
2832 
2833 /******************************************************************************
2834 * Called when an audio thread starts or stops.
2835 */
2836 void KAlarmApp::notifyAudioPlaying(bool playing)
2837 {
2838     Q_EMIT audioPlaying(playing);
2839 }
2840 
2841 /******************************************************************************
2842 * Stop audio play.
2843 */
2844 void KAlarmApp::stopAudio()
2845 {
2846     MessageDisplay::stopAudio();
2847 }
2848 
2849 /******************************************************************************
2850 * Set the command error for the specified alarm.
2851 */
2852 void KAlarmApp::setEventCommandError(const KAEvent& event, KAEvent::CmdErr err) const
2853 {
2854     ProcData* pd = findCommandProcess(event.id());
2855     if (pd && pd->eventDeleted)
2856         return;   // the alarm has been deleted, so can't set error status
2857 
2858     if (err == KAEvent::CmdErr::Post  &&  event.commandError() == KAEvent::CmdErr::Pre)
2859         err = KAEvent::CmdErr::PrePost;
2860     event.setCommandError(err);
2861     KAEvent ev = ResourcesCalendar::event(EventId(event));
2862     if (ev.isValid()  &&  ev.commandError() != err)
2863     {
2864         ev.setCommandError(err);
2865         ResourcesCalendar::updateEvent(ev);
2866     }
2867     Resource resource = Resources::resourceForEvent(event.id());
2868     resource.handleCommandErrorChange(event);
2869 }
2870 
2871 /******************************************************************************
2872 * Clear the command error for the specified alarm.
2873 */
2874 void KAlarmApp::clearEventCommandError(const KAEvent& event, KAEvent::CmdErr err) const
2875 {
2876     ProcData* pd = findCommandProcess(event.id());
2877     if (pd && pd->eventDeleted)
2878         return;   // the alarm has been deleted, so can't set error status
2879 
2880     auto newerr = static_cast<KAEvent::CmdErr>(event.commandError() & ~err);
2881     event.setCommandError(newerr);
2882     KAEvent ev = ResourcesCalendar::event(EventId(event));
2883     if (ev.isValid())
2884     {
2885         newerr = static_cast<KAEvent::CmdErr>(ev.commandError() & ~err);
2886         ev.setCommandError(newerr);
2887         ResourcesCalendar::updateEvent(ev);
2888     }
2889     Resource resource = Resources::resourceForEvent(event.id());
2890     resource.handleCommandErrorChange(event);
2891 }
2892 
2893 /******************************************************************************
2894 * Find the currently executing command process for an event ID, if any.
2895 */
2896 KAlarmApp::ProcData* KAlarmApp::findCommandProcess(const QString& eventId) const
2897 {
2898     for (ProcData* pd : std::as_const(mCommandProcesses))
2899     {
2900         if (pd->event->id() == eventId)
2901             return pd;
2902     }
2903     return nullptr;
2904 }
2905 
2906 
2907 KAlarmApp::ProcData::ProcData(ShellProcess* p, KAEvent* e, KAAlarm* a, int f)
2908     : process(p)
2909     , event(e)
2910     , alarm(a)
2911     , flags(f)
2912 { }
2913 
2914 KAlarmApp::ProcData::~ProcData()
2915 {
2916     while (!tempFiles.isEmpty())
2917     {
2918         // Delete the temporary file called by the XTerm command
2919         QFile f(tempFiles.constFirst());
2920         f.remove();
2921         tempFiles.removeFirst();
2922     }
2923     delete process;
2924     delete event;
2925     delete alarm;
2926 }
2927 
2928 #include "moc_kalarmapp.cpp"
2929 
2930 // vim: et sw=4: