kalarmapp.cpp 117 KB
Newer Older
1
/*
David Jarvie's avatar
David Jarvie committed
2
 *  kalarmapp.cpp  -  the KAlarm application object
3
 *  Program:  kalarm
4
 *  SPDX-FileCopyrightText: 2001-2020 David Jarvie <djarvie@kde.org>
5
 *
6
 *  SPDX-License-Identifier: GPL-2.0-or-later
7
8
 */

Laurent Montel's avatar
Laurent Montel committed
9
#include "kalarmapp.h"
David Jarvie's avatar
David Jarvie committed
10

David Jarvie's avatar
David Jarvie committed
11
#include "kalarm.h"
12
#include "commandoptions.h"
David Jarvie's avatar
David Jarvie committed
13
#include "dbushandler.h"
14
#include "displaycalendar.h"
15
#include "editdlgtypes.h"
David Jarvie's avatar
David Jarvie committed
16
17
18
#include "functions.h"
#include "kamail.h"
#include "mainwindow.h"
19
#include "messagewindow.h"
20
#include "messagenotification.h"
21
#include "migratekde4files.h"
David Jarvie's avatar
David Jarvie committed
22
23
#include "preferences.h"
#include "prefdlg.h"
24
#include "resourcescalendar.h"
David Jarvie's avatar
David Jarvie committed
25
26
#include "startdaytimer.h"
#include "traywindow.h"
27
#include "resources/datamodel.h"
28
#include "resources/resources.h"
David Jarvie's avatar
David Jarvie committed
29
#include "resources/eventmodel.h"
30
#include "lib/desktop.h"
David Jarvie's avatar
David Jarvie committed
31
32
#include "lib/messagebox.h"
#include "lib/shellprocess.h"
33
34
#include "notifications_interface.h" // DBUS-generated
#include "dbusproperties.h"          // DBUS-generated
David Jarvie's avatar
David Jarvie committed
35
#include "kalarm_debug.h"
36

David Jarvie's avatar
David Jarvie committed
37
38
#include <KAlarmCal/DateTime>
#include <KAlarmCal/KARecurrence>
39

40
#include <KLocalizedString>
41
#include <KConfig>
David Jarvie's avatar
David Jarvie committed
42
43
#include <KConfigGui>
#include <KAboutData>
David Jarvie's avatar
David Jarvie committed
44
#include <KSharedConfig>
45
#include <KStandardGuiItem>
David Jarvie's avatar
David Jarvie committed
46
#include <netwm.h>
47
#include <KShell>
David Jarvie's avatar
David Jarvie committed
48

49
50
51
52
#include <QObject>
#include <QTimer>
#include <QFile>
#include <QTextStream>
David Jarvie's avatar
David Jarvie committed
53
#include <QTemporaryFile>
David Jarvie's avatar
David Jarvie committed
54
#include <QStandardPaths>
55
#include <QSystemTrayIcon>
David Jarvie's avatar
David Jarvie committed
56
#include <QCommandLineParser>
57
58
59
60
61
62

#include <stdlib.h>
#include <ctype.h>
#include <iostream>
#include <climits>

63
64
65
namespace
{
const int RESOURCES_TIMEOUT = 30;   // timeout (seconds) for resources to be populated
66

67
68
69
const char FDO_NOTIFICATIONS_SERVICE[] = "org.freedesktop.Notifications";
const char FDO_NOTIFICATIONS_PATH[]    = "/org/freedesktop/Notifications";

70
/******************************************************************************
71
72
73
74
* Find the maximum number of seconds late which a late-cancel alarm is allowed
* to be. This is calculated as the late cancel interval, plus a few seconds
* leeway to cater for any timing irregularities.
*/
75
inline int maxLateness(int lateCancel)
76
{
77
78
79
    static const int LATENESS_LEEWAY = 5;
    int lc = (lateCancel >= 1) ? (lateCancel - 1)*60 : 0;
    return LATENESS_LEEWAY + lc;
80
81
}

82
83
84
85
86
87
QWidget* mainWidget()
{
    return MainWindow::mainMainWindow();
}
}

88

Laurent Montel's avatar
Laurent Montel committed
89
KAlarmApp*  KAlarmApp::mInstance  = nullptr;
David Jarvie's avatar
David Jarvie committed
90
int         KAlarmApp::mActiveCount = 0;
91
92
int         KAlarmApp::mFatalError  = 0;
QString     KAlarmApp::mFatalMessage;
93
94
95


/******************************************************************************
96
97
* Construct the application.
*/
David Jarvie's avatar
David Jarvie committed
98
KAlarmApp::KAlarmApp(int& argc, char** argv)
David Jarvie's avatar
David Jarvie committed
99
100
    : QApplication(argc, argv)
    , mDBusHandler(new DBusHandler())
101
{
102
    qCDebug(KALARM_LOG) << "KAlarmApp:";
103
104
105
106
107
108
109
110
111
112
113
114
}

/******************************************************************************
*/
KAlarmApp::~KAlarmApp()
{
    while (!mCommandProcesses.isEmpty())
    {
        ProcData* pd = mCommandProcesses.at(0);
        mCommandProcesses.pop_front();
        delete pd;
    }
David Jarvie's avatar
David Jarvie committed
115
116
117
    ResourcesCalendar::terminate();
    DisplayCalendar::terminate();
    DataModel::terminate();
Laurent Montel's avatar
Laurent Montel committed
118
    delete mDBusHandler;
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
}

/******************************************************************************
* Return the one and only KAlarmApp instance.
* If it doesn't already exist, it is created first.
*/
KAlarmApp* KAlarmApp::create(int& argc, char** argv)
{
    if (!mInstance)
    {
        mInstance = new KAlarmApp(argc, argv);

        if (mFatalError)
            mInstance->quitFatal();
    }
    return mInstance;
}

/******************************************************************************
* Perform initialisations which may require KAboutData to have been set up.
*/
void KAlarmApp::initialise()
{
142
143
    // Migrate config and data files from KDE4 locations.
    MigrateKde4Files migrate;
144
145
    migrate.migrate();

David Jarvie's avatar
David Jarvie committed
146
#ifndef NDEBUG
147
    KAlarm::setTestModeConditions();
David Jarvie's avatar
David Jarvie committed
148
#endif
149
150

    setQuitOnLastWindowClosed(false);
151
    Preferences::self();    // read KAlarm configuration
152
153
    if (!Preferences::noAutoStart())
    {
154
155
156
        // Strip out any "OnlyShowIn=KDE" list from kalarm.autostart.desktop
        Preferences::setNoAutoStart(false);
        // Enable kalarm.autostart.desktop to start KAlarm
157
        Preferences::setAutoStart(true);
Laurent Montel's avatar
Laurent Montel committed
158
        Preferences::self()->save();
159
    }
160
161
162
163
164
165
166
    Preferences::connect(&Preferences::startOfDayChanged, this, &KAlarmApp::changeStartOfDay);
    Preferences::connect(&Preferences::workTimeChanged, this, &KAlarmApp::slotWorkTimeChanged);
    Preferences::connect(&Preferences::holidaysChanged, this, &KAlarmApp::slotHolidaysChanged);
    Preferences::connect(&Preferences::feb29TypeChanged, this, &KAlarmApp::slotFeb29TypeChanged);
    Preferences::connect(&Preferences::showInSystemTrayChanged, this, &KAlarmApp::slotShowInSystemTrayChanged);
    Preferences::connect(&Preferences::archivedKeepDaysChanged, this, &KAlarmApp::setArchivePurgeDays);
    Preferences::connect(&Preferences::messageFontChanged, this, &KAlarmApp::slotMessageFontChanged);
167
168
169
170
171
172
173
174
    slotFeb29TypeChanged(Preferences::defaultFeb29Type());

    KAEvent::setStartOfDay(Preferences::startOfDay());
    KAEvent::setWorkTime(Preferences::workDays(), Preferences::workDayStart(), Preferences::workDayEnd());
    KAEvent::setHolidays(Preferences::holidays());
    KAEvent::setDefaultFont(Preferences::messageFont());

    // Check if KOrganizer is installed
175
    const QString korg = QStringLiteral("korganizer");
Volker Krause's avatar
Volker Krause committed
176
    mKOrganizerEnabled = !QStandardPaths::findExecutable(korg).isEmpty();
177
    if (!mKOrganizerEnabled) { qCDebug(KALARM_LOG) << "KAlarmApp: KOrganizer options disabled (KOrganizer not found)"; }
178
    // Check if the window manager can't handle keyboard focus transfer between windows
179
    mWindowFocusBroken = (Desktop::currentIdentity() == Desktop::Unity);
180
    if (mWindowFocusBroken) { qCDebug(KALARM_LOG) << "KAlarmApp: Window keyboard focus broken"; }
181

182
183
    if (initialiseTimerResources())   // initialise calendars and alarm timer
    {
David Jarvie's avatar
David Jarvie committed
184
185
186
187
188
189
190
191
        Resources* resources = Resources::instance();
        connect(resources, &Resources::resourceAdded,
                     this, &KAlarmApp::slotResourceAdded);
        connect(resources, &Resources::resourcePopulated,
                     this, &KAlarmApp::slotResourcePopulated);
        connect(resources, &Resources::resourcePopulated,
                     this, &KAlarmApp::purgeNewArchivedDefault);
        connect(resources, &Resources::resourcesCreated,
David Jarvie's avatar
David Jarvie committed
192
193
194
                     this, &KAlarmApp::slotResourcesCreated);
        connect(resources, &Resources::resourcesPopulated,
                     this, &KAlarmApp::processQueue);
195
196
197
198
199
200
201

        KConfigGroup config(KSharedConfig::openConfig(), "General");
        mNoSystemTray        = config.readEntry("NoSystemTray", false);
        mOldShowInSystemTray = wantShowInSystemTray();
        DateTime::setStartOfDay(Preferences::startOfDay());
        mPrefsArchivedColour = Preferences::archivedColour();
    }
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218

    // Get notified when the Freedesktop notifications properties have changed.
    QDBusConnection conn = QDBusConnection::sessionBus();
    if (conn.interface()->isServiceRegistered(QString::fromLatin1(FDO_NOTIFICATIONS_SERVICE)))
    {
        OrgFreedesktopDBusPropertiesInterface* piface = new OrgFreedesktopDBusPropertiesInterface(
                QString::fromLatin1(FDO_NOTIFICATIONS_SERVICE),
                QString::fromLatin1(FDO_NOTIFICATIONS_PATH),
                conn, this);
        connect(piface, &OrgFreedesktopDBusPropertiesInterface::PropertiesChanged,
                this, &KAlarmApp::slotFDOPropertiesChanged);
        OrgFreedesktopNotificationsInterface niface(
                QString::fromLatin1(FDO_NOTIFICATIONS_SERVICE),
                QString::fromLatin1(FDO_NOTIFICATIONS_PATH),
                conn);
        mNotificationsInhibited = niface.inhibited();
    }
219
220
221
222
}

/******************************************************************************
* Initialise or reinitialise things which are tidied up/closed by quitIf().
223
224
225
226
227
228
* Reinitialisation can be necessary if session restoration finds nothing to
* restore and starts quitting the application, but KAlarm then starts up again
* before the application has exited.
* Reply = true if calendars were initialised successfully,
*         false if they were already initialised, or if initialisation failed.
*/
229
bool KAlarmApp::initialiseTimerResources()
230
231
232
233
234
{
    if (!mAlarmTimer)
    {
        mAlarmTimer = new QTimer(this);
        mAlarmTimer->setSingleShot(true);
Laurent Montel's avatar
Laurent Montel committed
235
        connect(mAlarmTimer, &QTimer::timeout, this, &KAlarmApp::checkNextDueAlarm);
236
    }
237
    if (!ResourcesCalendar::instance())
238
    {
239
        qCDebug(KALARM_LOG) << "KAlarmApp::initialise: initialising calendars";
240
        Desktop::setMainWindowFunc(&mainWidget);
241
242
        // First, initialise calendar resources, which need to be ready to
        // receive signals when resources initialise.
David Jarvie's avatar
David Jarvie committed
243
        ResourcesCalendar::initialise(KALARM_NAME, KALARM_VERSION);
244
245
246
        connect(ResourcesCalendar::instance(), &ResourcesCalendar::earliestAlarmChanged, this, &KAlarmApp::checkNextDueAlarm);
        connect(ResourcesCalendar::instance(), &ResourcesCalendar::atLoginEventAdded, this, &KAlarmApp::atLoginEventAdded);
        DisplayCalendar::initialise();
247
248
        // Finally, initialise the resources which generate signals as they initialise.
        DataModel::initialise();
249
        return true;
250
251
252
253
    }
    return false;
}

David Jarvie's avatar
David Jarvie committed
254
255
256
257
258
/******************************************************************************
* Restore the saved session if required.
*/
bool KAlarmApp::restoreSession()
{
259
260
261
262
263
264
265
266
267
    if (!isSessionRestored())
        return false;
    if (mFatalError)
    {
        quitFatal();
        return false;
    }

    // Process is being restored by session management.
268
    qCDebug(KALARM_LOG) << "KAlarmApp::restoreSession: Restoring";
269
270
271
272
273
    ++mActiveCount;
    // Create the session config object now.
    // This is necessary since if initCheck() below causes calendars to be updated,
    // the session config created after that points to an invalid file, resulting
    // in no windows being restored followed by a later crash.
David Jarvie's avatar
David Jarvie committed
274
    KConfigGui::sessionConfig();
275
276

    // When KAlarm is session restored, automatically set start-at-login to true.
277
    Preferences::self()->load();
278
279
280
    Preferences::setAutoStart(true);
    Preferences::setNoAutoStart(false);
    Preferences::setAskAutoStart(true);  // cancel any start-at-login prompt suppression
Laurent Montel's avatar
Laurent Montel committed
281
    Preferences::self()->save();
282
283
284
285
286
287
288

    if (!initCheck(true))     // open the calendar file (needed for main windows), don't process queue yet
    {
        --mActiveCount;
        quitIf(1, true);    // error opening the main calendar - quit
        return false;
    }
Laurent Montel's avatar
Laurent Montel committed
289
    MainWindow* trayParent = nullptr;
290
291
    for (int i = 1;  KMainWindow::canBeRestored(i);  ++i)
    {
David Jarvie's avatar
David Jarvie committed
292
        const QString type = KMainWindow::classNameOfToplevel(i);
293
        if (type == QLatin1String("MainWindow"))
294
295
296
297
298
299
300
301
        {
            MainWindow* win = MainWindow::create(true);
            win->restore(i, false);
            if (win->isHiddenTrayParent())
                trayParent = win;
            else
                win->show();
        }
302
        else if (type == QLatin1String("MessageWindow"))
303
        {
304
            MessageWindow* win = new MessageWindow;
305
306
            win->restore(i, false);
            if (win->isValid())
307
            {
308
                if (Resources::allCreated())
309
310
                    win->show();
            }
311
312
313
314
315
            else
                delete win;
        }
    }

316
317
    MessageNotification::sessionRestore();

318
319
320
321
    // Try to display the system tray icon if it is configured to be shown
    if (trayParent  ||  wantShowInSystemTray())
    {
        if (!MainWindow::count())
322
            qCWarning(KALARM_LOG) << "KAlarmApp::restoreSession: no main window to be restored!?";
323
324
325
326
327
328
329
330
331
332
333
        else
        {
            displayTrayIcon(true, trayParent);
            // Occasionally for no obvious reason, the main main window is
            // shown when it should be hidden, so hide it just to be sure.
            if (trayParent)
                trayParent->hide();
        }
    }

    --mActiveCount;
334
335
    if (quitIf(0))          // quit if no windows are open
        return false;       // quitIf() can sometimes return, despite calling exit()
336

337
    startProcessQueue();    // start processing the execution queue
338
    return true;
David Jarvie's avatar
David Jarvie committed
339
340
}

341
/******************************************************************************
342
* Called to start a new instance of the unique QApplication.
343
344
* Reply: exit code (>= 0), or -1 to continue execution.
*        If exit code >= 0, 'outputText' holds text to output before terminating.
345
*/
346
int KAlarmApp::activateInstance(const QStringList& args, const QString& workingDirectory, QString* outputText)
347
{
David Jarvie's avatar
David Jarvie committed
348
    Q_UNUSED(workingDirectory)
349
    qCDebug(KALARM_LOG) << "KAlarmApp::activateInstance" << args;
350
351
    if (outputText)
        outputText->clear();
352
353
    if (mFatalError)
    {
354
        Q_EMIT setExitValue(1);
355
        quitFatal();
356
        return 1;
357
    }
David Jarvie's avatar
David Jarvie committed
358

359
360
361
362
363
364
365
366
    // The D-Bus call to activate a subsequent instance of KAlarm may not supply
    // any arguments, but we need one.
    if (!args.isEmpty()  &&  mActivateArg0.isEmpty())
        mActivateArg0 = args[0];
    QStringList fixedArgs(args);
    if (args.isEmpty()  &&  !mActivateArg0.isEmpty())
        fixedArgs << mActivateArg0;

David Jarvie's avatar
David Jarvie committed
367
368
369
370
    // Parse and interpret command line arguments.
    QCommandLineParser parser;
    KAboutData::applicationData().setupCommandLine(&parser);
    parser.setApplicationDescription(QApplication::applicationDisplayName());
371
    CommandOptions* options = new CommandOptions;
372
    const QStringList nonexecArgs = options->setOptions(&parser, fixedArgs);
373
    options->parse();
David Jarvie's avatar
David Jarvie committed
374
375
    KAboutData::applicationData().processCommandLine(&parser);

376
377
378
379
    ++mActiveCount;
    int exitCode = 0;               // default = success
    static bool firstInstance = true;
    bool dontRedisplay = false;
380
    CommandOptions::Command command = CommandOptions::NONE;
David Jarvie's avatar
David Jarvie committed
381
    const bool processOptions = (!firstInstance || !isSessionRestored());
382
    if (processOptions)
383
    {
384
        options->process();
385
#ifndef NDEBUG
David Jarvie's avatar
David Jarvie committed
386
387
        if (options->simulationTime().isValid())
            KAlarm::setSimulatedSystemTime(options->simulationTime());
388
#endif
389
        command = options->command();
David Jarvie's avatar
David Jarvie committed
390
        if (options->disableAll())
391
            setAlarmsEnabled(false);   // disable alarm monitoring
392
393
394
395
396
397
398
399

        // Handle options which exit with a terminal message, before
        // making the application a unique application, since a
        // unique application won't output to the terminal if another
        // instance is already running.
        switch (command)
        {
            case CommandOptions::CMD_ERROR:
400
                Q_EMIT setExitValue(1);
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
                if (outputText)
                {
                    *outputText = options->outputText();
                    delete options;
                    return 1;
                }
                mReadOnly = true;   // don't need write access to calendars
                exitCode = 1;
                break;
            default:
                break;
        }
    }

    if (processOptions)
    {
417
418
419
420
421
422
        switch (command)
        {
            case CommandOptions::TRIGGER_EVENT:
            case CommandOptions::CANCEL_EVENT:
            {
                // Display or delete the event with the specified event ID
423
424
                QueuedAction action = static_cast<QueuedAction>(int((command == CommandOptions::TRIGGER_EVENT) ? QueuedAction::Trigger : QueuedAction::Cancel)
                                                                | int(QueuedAction::Exit));
425
                // Open the calendar, don't start processing execution queue yet,
426
                // and wait for the calendar resources to be populated.
427
                if (!initCheck(true))
428
429
430
                    exitCode = 1;
                else
                {
431
                    mCommandOption = options->commandName();
432
433
434
435
436
437
                    // Get the resource ID string and event UID. Note that if
                    // resources have not been created yet, the numeric
                    // resource ID can't yet be looked up.
                    if (options->resourceId().isEmpty())
                        action = static_cast<QueuedAction>((int)action | int(QueuedAction::FindId));
                    mActionQueue.enqueue(ActionQEntry(action, EventId(options->eventId()), options->resourceId()));
438
439
                    startProcessQueue();      // start processing the execution queue
                    dontRedisplay = true;
440
441
442
                }
                break;
            }
443
            case CommandOptions::LIST:
444
445
                // Output a list of scheduled alarms to stdout.
                // Open the calendar, don't start processing execution queue yet,
446
                // and wait for all calendar resources to be populated.
447
                mReadOnly = true;   // don't need write access to calendars
448
                mAlarmsEnabled = false;   // prevent alarms being processed
449
                if (!initCheck(true))
450
451
452
                    exitCode = 1;
                else
                {
453
454
455
                    const QueuedAction action = static_cast<QueuedAction>(int(QueuedAction::List) | int(QueuedAction::Exit));
                    mActionQueue.enqueue(ActionQEntry(action, EventId()));
                    startProcessQueue();      // start processing the execution queue
456
457
458
                    dontRedisplay = true;
                }
                break;
459

460
            case CommandOptions::EDIT:
461
                // Edit a specified existing alarm.
462
                // Open the calendar and wait for the calendar resources to be populated.
463
                if (!initCheck(false))
464
                    exitCode = 1;
465
                else
466
                {
467
468
469
                    mCommandOption = options->commandName();
                    if (firstInstance)
                        mEditingCmdLineAlarm = 0x10;   // want to redisplay alarms if successful
470
471
472
473
                    // Get the resource ID string and event UID. Note that if
                    // resources have not been created yet, the numeric
                    // resource ID can't yet be looked up.
                    mActionQueue.enqueue(ActionQEntry(QueuedAction::Edit, EventId(options->eventId()), options->resourceId()));
474
475
                    startProcessQueue();      // start processing the execution queue
                    dontRedisplay = true;
476
477
478
479
480
481
482
483
484
485
                }
                break;

            case CommandOptions::EDIT_NEW:
            {
                // Edit a new alarm, and optionally preset selected values
                if (!initCheck())
                    exitCode = 1;
                else
                {
486
                    EditAlarmDlg* editDlg = EditAlarmDlg::create(false, options->editType(), MainWindow::mainMainWindow());
487
488
489
490
491
                    if (!editDlg)
                    {
                        exitCode = 1;
                        break;
                    }
David Jarvie's avatar
David Jarvie committed
492
493
494
495
496
                    if (options->alarmTime().isValid())
                        editDlg->setTime(options->alarmTime());
                    if (options->recurrence())
                        editDlg->setRecurrence(*options->recurrence(), options->subRepeatInterval(), options->subRepeatCount());
                    else if (options->flags() & KAEvent::REPEAT_AT_LOGIN)
497
                        editDlg->setRepeatAtLogin();
David Jarvie's avatar
David Jarvie committed
498
499
500
501
                    editDlg->setAction(options->editAction(), AlarmText(options->text()));
                    if (options->lateCancel())
                        editDlg->setLateCancel(options->lateCancel());
                    if (options->flags() & KAEvent::COPY_KORGANIZER)
502
                        editDlg->setShowInKOrganizer(true);
David Jarvie's avatar
David Jarvie committed
503
                    switch (options->editType())
504
505
506
507
508
                    {
                        case EditAlarmDlg::DISPLAY:
                        {
                            // EditAlarmDlg::create() always returns EditDisplayAlarmDlg for type = DISPLAY
                            EditDisplayAlarmDlg* dlg = qobject_cast<EditDisplayAlarmDlg*>(editDlg);
David Jarvie's avatar
David Jarvie committed
509
510
511
512
513
514
                            if (options->fgColour().isValid())
                                dlg->setFgColour(options->fgColour());
                            if (options->bgColour().isValid())
                                dlg->setBgColour(options->bgColour());
                            if (!options->audioFile().isEmpty()
                            ||  options->flags() & (KAEvent::BEEP | KAEvent::SPEAK))
515
                            {
David Jarvie's avatar
David Jarvie committed
516
517
518
519
                                const KAEvent::Flags flags = options->flags();
                                const Preferences::SoundType type = (flags & KAEvent::BEEP) ? Preferences::Sound_Beep
                                                                  : (flags & KAEvent::SPEAK) ? Preferences::Sound_Speak
                                                                  : Preferences::Sound_File;
David Jarvie's avatar
David Jarvie committed
520
                                dlg->setAudio(type, options->audioFile(), options->audioVolume(), (flags & KAEvent::REPEAT_SOUND ? 0 : -1));
521
                            }
David Jarvie's avatar
David Jarvie committed
522
523
                            if (options->reminderMinutes())
                                dlg->setReminder(options->reminderMinutes(), (options->flags() & KAEvent::REMINDER_ONCE));
524
525
                            if (options->flags() & KAEvent::NOTIFY)
                                dlg->setNotify(true);
David Jarvie's avatar
David Jarvie committed
526
                            if (options->flags() & KAEvent::CONFIRM_ACK)
527
                                dlg->setConfirmAck(true);
David Jarvie's avatar
David Jarvie committed
528
                            if (options->flags() & KAEvent::AUTO_CLOSE)
529
530
531
532
533
534
535
536
537
                                dlg->setAutoClose(true);
                            break;
                        }
                        case EditAlarmDlg::COMMAND:
                            break;
                        case EditAlarmDlg::EMAIL:
                        {
                            // EditAlarmDlg::create() always returns EditEmailAlarmDlg for type = EMAIL
                            EditEmailAlarmDlg* dlg = qobject_cast<EditEmailAlarmDlg*>(editDlg);
David Jarvie's avatar
David Jarvie committed
538
539
540
541
542
543
                            if (options->fromID()
                            ||  !options->addressees().isEmpty()
                            ||  !options->subject().isEmpty()
                            ||  !options->attachments().isEmpty())
                                dlg->setEmailFields(options->fromID(), options->addressees(), options->subject(), options->attachments());
                            if (options->flags() & KAEvent::EMAIL_BCC)
544
545
546
547
548
549
550
                                dlg->setBcc(true);
                            break;
                        }
                        case EditAlarmDlg::AUDIO:
                        {
                            // EditAlarmDlg::create() always returns EditAudioAlarmDlg for type = AUDIO
                            EditAudioAlarmDlg* dlg = qobject_cast<EditAudioAlarmDlg*>(editDlg);
David Jarvie's avatar
David Jarvie committed
551
552
                            if (!options->audioFile().isEmpty()  ||  options->audioVolume() >= 0)
                                dlg->setAudio(options->audioFile(), options->audioVolume());
553
554
555
556
557
                            break;
                        }
                        case EditAlarmDlg::NO_TYPE:
                            break;
                    }
558

559
560
561
562
                    // Execute the edit dialogue. Note that if no other instance of KAlarm is
                    // running, this new instance will not exit after the dialogue is closed.
                    // This is deliberate, since exiting would mean that KAlarm wouldn't
                    // trigger the new alarm.
563
                    KAlarm::execNewAlarmDlg(editDlg);
564
565

                    createOnlyMainWindow();   // prevent the application from quitting
566
567
568
569
570
571
572
573
                }
                break;
            }
            case CommandOptions::EDIT_NEW_PRESET:
                // Edit a new alarm, preset with a template
                if (!initCheck())
                    exitCode = 1;
                else
574
575
576
577
578
                {
                    // Execute the edit dialogue. Note that if no other instance of KAlarm is
                    // running, this new instance will not exit after the dialogue is closed.
                    // This is deliberate, since exiting would mean that KAlarm wouldn't
                    // trigger the new alarm.
David Jarvie's avatar
David Jarvie committed
579
                    KAlarm::editNewAlarm(options->templateName());
580
581

                    createOnlyMainWindow();   // prevent the application from quitting
582
                }
583
584
585
586
                break;

            case CommandOptions::NEW:
                // Display a message or file, execute a command, or send an email
587
                setResourcesTimeout();   // set timeout for resource initialisation
588
                if (!initCheck())
589
                    exitCode = 1;
590
591
592
593
594
595
596
597
598
599
                else
                {
                    if (!scheduleEvent(options->editAction(), options->text(), options->alarmTime(),
                                       options->lateCancel(), options->flags(), options->bgColour(),
                                       options->fgColour(), QFont(), options->audioFile(), options->audioVolume(),
                                       options->reminderMinutes(), (options->recurrence() ? *options->recurrence() : KARecurrence()),
                                       options->subRepeatInterval(), options->subRepeatCount(),
                                       options->fromID(), options->addressees(),
                                       options->subject(), options->attachments()))
                        exitCode = 1;
David Jarvie's avatar
David Jarvie committed
600
601
                    else
                        createOnlyMainWindow();   // prevent the application from quitting
602
                }
603
604
605
606
                break;

            case CommandOptions::TRAY:
                // Display only the system tray icon
607
                if (Preferences::showInSystemTray()  &&  QSystemTrayIcon::isSystemTrayAvailable())
608
                {
609
                    if (!initCheck())   // open the calendar, start processing execution queue
610
                        exitCode = 1;
611
612
613
614
615
                    else
                    {
                        if (!displayTrayIcon(true))
                            exitCode = 1;
                    }
616
617
                    break;
                }
618
                Q_FALLTHROUGH();   // fall through to NONE
619
620
            case CommandOptions::NONE:
                // No arguments - run interactively & display the main window
621
#ifndef NDEBUG
David Jarvie's avatar
David Jarvie committed
622
                if (options->simulationTime().isValid()  &&  !firstInstance)
623
                    break;   // simulating time: don't open main window if already running
624
#endif
625
626
627
628
                if (!initCheck())
                    exitCode = 1;
                else
                {
629
630
631
632
633
634
635
636
637
                    if (mTrayWindow  &&  mTrayWindow->assocMainWindow()  &&  !mTrayWindow->assocMainWindow()->isVisible())
                        mTrayWindow->showAssocMainWindow();
                    else
                    {
                        MainWindow* win = MainWindow::create();
                        if (command == CommandOptions::TRAY)
                            win->setWindowState(win->windowState() | Qt::WindowMinimized);
                        win->show();
                    }
638
639
                }
                break;
640
            default:
641
642
643
                break;
        }
    }
644
645
    if (options != CommandOptions::firstInstance())
        delete options;
646
647
648
649
650
651
652
653
654
655
656

    // If this is the first time through, redisplay any alarm message windows
    // from last time.
    if (firstInstance  &&  !dontRedisplay  &&  !exitCode)
    {
        /* First time through, so redisplay alarm message windows from last time.
         * But it is possible for session restoration in some circumstances to
         * not create any windows, in which case the alarm calendars will have
         * been deleted - if so, don't try to do anything. (This has been known
         * to happen under the Xfce desktop.)
         */
657
        if (ResourcesCalendar::instance())
658
        {
659
            if (Resources::allCreated())
660
661
            {
                mRedisplayAlarms = false;
662
                MessageDisplay::redisplayAlarms();
663
664
665
666
            }
            else
                mRedisplayAlarms = true;
        }
667
668
669
670
671
672
673
674
    }

    --mActiveCount;
    firstInstance = false;

    // Quit the application if this was the last/only running "instance" of the program.
    // Executing 'return' doesn't work very well since the program continues to
    // run if no windows were created.
675
676
    if (quitIf(exitCode >= 0 ? exitCode : 0))
        return exitCode;    // exit this application instance
677

678
    return -1;   // continue executing the application instance
679
680
}

681
682
683
684
685
686
687
688
689
690
691
692
693
694
/******************************************************************************
* Create a minimised main window if none already exists.
* This prevents the application from quitting.
*/
void KAlarmApp::createOnlyMainWindow()
{
    if (!MainWindow::count())
    {
        if (Preferences::showInSystemTray()  &&  QSystemTrayIcon::isSystemTrayAvailable())
        {
            if (displayTrayIcon(true))
                return;
        }
        MainWindow* win = MainWindow::create();
695
696
        win->setWindowState(Qt::WindowMinimized);
        win->show();
697
698
699
    }
}

700
/******************************************************************************
701
* Quit the program, optionally only if there are no more "instances" running.
702
* Reply = true if program exited.
703
*/
704
bool KAlarmApp::quitIf(int exitCode, bool force)
705
{
706
707
708
709
710
    if (force)
    {
        // Quit regardless, except for message windows
        mQuitting = true;
        MainWindow::closeAll();
711
        mQuitting = false;
712
        displayTrayIcon(false);
713
        if (MessageDisplay::instanceCount(true))    // ignore always-hidden displays (e.g. audio alarms)
714
715
716
717
718
719
720
721
            return false;
    }
    else if (mQuitting)
        return false;   // MainWindow::closeAll() causes quitIf() to be called again
    else
    {
        // Quit only if there are no more "instances" running
        mPendingQuit = false;
722
        if (mActiveCount > 0  ||  MessageDisplay::instanceCount(true))  // ignore always-hidden displays (e.g. audio alarms)
723
            return false;
David Jarvie's avatar
David Jarvie committed
724
        const int mwcount = MainWindow::count();
Laurent Montel's avatar
Laurent Montel committed
725
        MainWindow* mw = mwcount ? MainWindow::firstWindow() : nullptr;
726
727
        if (mwcount > 1  ||  (mwcount && (!mw->isHidden() || !mw->isTrayParent())))
            return false;
728
729
        // There are no windows left except perhaps a main window which is a hidden
        // tray icon parent, or an always-hidden message window.
730
731
732
733
734
735
736
        if (mTrayWindow)
        {
            // There is a system tray icon.
            // Don't exit unless the system tray doesn't seem to exist.
            if (checkSystemTray())
                return false;
        }
737
        if (!mActionQueue.isEmpty()  ||  !mCommandProcesses.isEmpty())
738
739
740
741
742
743
744
745
746
        {
            // Don't quit yet if there are outstanding actions on the execution queue
            mPendingQuit = true;
            mPendingQuitCode = exitCode;
            return false;
        }
    }

    // This was the last/only running "instance" of the program, so exit completely.
747
    // NOTE: Everything which is terminated/deleted here must where applicable
748
749
    //       be initialised in the initialiseTimerResources() method, in case
    //       KAlarm is started again before application exit completes!
750
    qCDebug(KALARM_LOG) << "KAlarmApp::quitIf:" << exitCode << ": quitting";
751
    MessageDisplay::stopAudio(true);
752
753
    if (mCancelRtcWake)
    {
Laurent Montel's avatar
Laurent Montel committed
754
        KAlarm::setRtcWakeTime(0, nullptr);
755
756
        KAlarm::deleteRtcWakeConfig();
    }
757
    delete mAlarmTimer;     // prevent checking for alarms after deleting calendars
Laurent Montel's avatar
Laurent Montel committed
758
    mAlarmTimer = nullptr;
759
    mInitialised = false;   // prevent processQueue() from running
760
761
    ResourcesCalendar::terminate();
    DisplayCalendar::terminate();
762
    DataModel::terminate();
763
    Q_EMIT setExitValue(exitCode);
764
765
    exit(exitCode);
    return true;    // sometimes we actually get to here, despite calling exit()
766
767
}

768
769
770
771
772
773
774
/******************************************************************************
* Called when the Quit menu item is selected.
* Closes the system tray window and all main windows, but does not exit the
* program if other windows are still open.
*/
void KAlarmApp::doQuit(QWidget* parent)
{
775
    qCDebug(KALARM_LOG) << "KAlarmApp::doQuit";
776
    if (KAMessageBox::warningCancelContinue(parent,
777
                                            i18nc("@info", "Quitting will disable alarms (once any alarm message windows are closed)."),
778
779
780
                                            QString(), KStandardGuiItem::quit(),
                                            KStandardGuiItem::cancel(), Preferences::QUIT_WARN
                                           ) != KMessageBox::Continue)
781
        return;
782
783
784
    if (!KAlarm::checkRtcWakeConfig(true).isEmpty())
    {
        // A wake-on-suspend alarm is set
785
        if (KAMessageBox::warningCancelContinue(parent,
786
787
                                                i18nc("@info", "Quitting will cancel the scheduled Wake from Suspend."),
                                                QString(), KStandardGuiItem::quit()
788
                                               ) != KMessageBox::Continue)
789
790
791
            return;
        mCancelRtcWake = true;
    }
792
793
794
795
796
    if (!Preferences::autoStart())
    {
        int option = KMessageBox::No;
        if (!Preferences::autoStartChangedByUser())
        {
797
            option = KAMessageBox::questionYesNoCancel(parent,
Laurent Montel's avatar
Laurent Montel committed
798
                                         xi18nc("@info", "Do you want to start KAlarm at login?<nl/>"
799
800
                                                        "(Note that alarms will be disabled if KAlarm is not started.)"),
                                         QString(), KStandardGuiItem::yes(), KStandardGuiItem::no(),
David Jarvie's avatar
David Jarvie committed
801
                                         KStandardGuiItem::cancel(), Preferences::ASK_AUTO_START);
802
803
804
805
806
807
808
809
810
811
812
813
814
815
        }
        switch (option)
        {
            case KMessageBox::Yes:
                Preferences::setAutoStart(true);
                Preferences::setNoAutoStart(false);
                break;
            case KMessageBox::No:
                Preferences::setNoAutoStart(true);
                break;
            case KMessageBox::Cancel:
            default:
                return;
        }
Laurent Montel's avatar
Laurent Montel committed
816
        Preferences::self()->save();
817
818
    }
    quitIf(0, true);
819
820
}

821
822
823
824
825
826
/******************************************************************************
* Display an error message for a fatal error. Prevent further actions since
* the program state is unsafe.
*/
void KAlarmApp::displayFatalError(const QString& message)
{
827
828
829
830
    if (!mFatalError)
    {
        mFatalError = 1;
        mFatalMessage = message;
David Jarvie's avatar
David Jarvie committed
831
832
        if (mInstance)
            QTimer::singleShot(0, mInstance, &KAlarmApp::quitFatal);
833
    }
834
835
836
837
838
839
840
}

/******************************************************************************
* Quit the program, once the fatal error message has been acknowledged.
*/
void KAlarmApp::quitFatal()
{
841
842
843
844
845
846
847
    switch (mFatalError)
    {
        case 0:
        case 2:
            return;
        case 1:
            mFatalError = 2;
Laurent Montel's avatar
Laurent Montel committed
848
            KMessageBox::error(nullptr, mFatalMessage);   // this is an application modal window
849
            mFatalError = 3;
850
            Q_FALLTHROUGH();   // fall through to '3'
851
        case 3:
David Jarvie's avatar
David Jarvie committed
852
853
            if (mInstance)
                mInstance->quitIf(1, true);
854
855
            break;
    }
856
    QTimer::singleShot(1000, this, &KAlarmApp::quitFatal);
857
858
}

859
860
861
862
863
864
865
/******************************************************************************
* Called by the alarm timer when the next alarm is due.
* Also called when the execution queue has finished processing to check for the
* next alarm.
*/
void KAlarmApp::checkNextDueAlarm()
{
866
867
868
    if (!mAlarmsEnabled)
        return;
    // Find the first alarm due
869
    KADateTime nextDt;
870
    const KAEvent nextEvent = ResourcesCalendar::earliestAlarm(nextDt, mNotificationsInhibited);
871
    if (!nextEvent.isValid())
872
        return;   // there are no alarms pending
873
    const KADateTime now = KADateTime::currentDateTime(Preferences::timeSpec());
874
    qint64 interval = now.msecsTo(nextDt);
875
    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;
876
877
878
    if (interval <= 0)
    {
        // Queue the alarm
879
880
        queueAlarmId(nextEvent);
        qCDebug(KALARM_LOG) << "KAlarmApp::checkNextDueAlarm:" << nextEvent.id() << ": due now";
881
        QTimer::singleShot(0, this, &KAlarmApp::processQueue);
882
883
884
885
886
    }
    else
    {
        // No alarm is due yet, so set timer to wake us when it's due.
        // Check for integer overflow before setting timer.
887
#ifndef HIBERNATION_SIGNAL
888
889
890
        /* TODO: Use hibernation wakeup signal:
         *   #include <Solid/Power>
         *   connect(Solid::Power::self(), &Solid::Power::resumeFromSuspend, ...)
891
         *   (or resumingFromSuspend?)
892
893
894
895
         * to be notified when wakeup from hibernation occurs. But can't use it
         * unless we know that this notification is supported by the system!
         */
        /* Re-evaluate the next alarm time every minute, in case the
896
897
898
899
900
         * system clock jumps. The most common case when the clock jumps
         * is when a laptop wakes from hibernation. If timers were left to
         * run, they would trigger late by the length of time the system
         * was asleep.
         */
901
902
        if (interval > 60000)    // 1 minute
            interval = 60000;
903
#endif
904
        ++interval;    // ensure we don't trigger just before the minute boundary
905
906
        if (interval > INT_MAX)
            interval = INT_MAX;
907
        qCDebug(KALARM_LOG) << "KAlarmApp::checkNextDueAlarm:" << nextEvent.id() << "wait" << interval/1000 << "seconds";
908
909
        mAlarmTimer->start(static_cast<int>(interval));
    }
910
911
912
913
914
915
916
}

/******************************************************************************
* Called by the alarm timer when the next alarm is due.
* Also called when the execution queue has finished processing to check for the
* next alarm.
*/
917
void KAlarmApp::queueAlarmId(const KAEvent& event)
918
{
David Jarvie's avatar
David Jarvie committed
919
    const EventId id(event);
David Jarvie's avatar
David Jarvie committed
920
    for (const ActionQEntry& entry : qAsConst(mActionQueue))
921
    {
922
        if (entry.action == QueuedAction::Handle  &&  entry.eventId == id)
923
924
            return;  // the alarm is already queued
    }
925
    mActionQueue.enqueue(ActionQEntry(QueuedAction::Handle, id));
926
927
928
929
930
931
932
}

/******************************************************************************
* Start processing the execution queue.
*/
void KAlarmApp::startProcessQueue()
{
933
934
    if (!mInitialised)
    {
935
        qCDebug(KALARM_LOG) << "KAlarmApp::startProcessQueue";
936
        mInitialised = true;
937
        QTimer::singleShot(0, this, &KAlarmApp::processQueue);    // process anything already queued
938
    }
939
940
}

David Jarvie's avatar
David Jarvie committed
941
942
943
944
945
/******************************************************************************
* The main processing loop for KAlarm.
* All KAlarm operations involving opening or updating calendar files are called
* from this loop to ensure that only one operation is active at any one time.
* This precaution is necessary because KAlarm's activities are mostly
946
947
948
949
950
* asynchronous, being in response to D-Bus calls from other programs or timer
* events, any of which can be received in the middle of performing another
* operation. If a calendar file is opened or updated while another calendar
* operation is in progress, the program has been observed to hang, or the first
* calendar call has failed with data loss - clearly unacceptable!!
David Jarvie's avatar
David Jarvie committed
951
952
953
*/
void KAlarmApp::processQueue()
{
954
955
    if (mInitialised  &&  !mProcessingQueue)
    {
956
        qCDebug(KALARM_LOG) << "KAlarmApp::processQueue";
957
958
959
960
961
962
        mProcessingQueue = true;

        // Refresh alarms if that's been queued
        KAlarm::refreshAlarmsIfQueued();

        // Process queued events
963
        while (!mActionQueue.isEmpty())
964
        {
965
966
            ActionQEntry& entry = mActionQueue.head();

967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
            // If the first action's resource ID is a string, can't process it
            // until its numeric resource ID can be found.
            if (!entry.resourceId.isEmpty())
            {
                if (!Resources::allCreated())
                {
                    // If resource population has timed out, discard all queued events.
                    if (mResourcesTimedOut)
                    {
                        qCCritical(KALARM_LOG) << "Error! Timeout creating calendars";
                        mActionQueue.clear();
                    }
                    break;
                }
                // Convert the resource ID string to the numeric resource ID.
                entry.eventId.setResourceId(EventId::getResourceId(entry.resourceId));
                entry.resourceId.clear();
            }

986
987
988
989
            // Can't process the first action until its resource has been populated.
            const ResourceId id = entry.eventId.resourceId();
            if ((id <  0 && !Resources::allPopulated())
            ||  (id >= 0 && !Resources::resource(id).isPopulated()))
990
991
992
993
994
995
996
997
998
999
            {
                // If resource population has timed out, discard all queued events.
                if (mResourcesTimedOut)
                {
                    qCCritical(KALARM_LOG) << "Error! Timeout reading calendars";
                    mActionQueue.clear();
                }
                break;
            }

1000
            // Process the first action in the queue.