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

David Jarvie's avatar
David Jarvie committed
9
#include "functions.h"
10
#include "functions_p.h"
David Jarvie's avatar
David Jarvie committed
11

12
#include "akonadicollectionsearch.h"
13
#include "displaycalendar.h"
David Jarvie's avatar
David Jarvie committed
14
15
16
17
#include "editdlg.h"
#include "kalarmapp.h"
#include "kamail.h"
#include "mainwindow.h"
18
#include "messagewindow.h"
David Jarvie's avatar
David Jarvie committed
19
#include "preferences.h"
20
#include "resourcescalendar.h"
David Jarvie's avatar
David Jarvie committed
21
22
#include "templatelistview.h"
#include "templatemenuaction.h"
23
#include "resources/calendarfunctions.h"
24
#include "resources/datamodel.h"
25
#include "resources/resources.h"
David Jarvie's avatar
David Jarvie committed
26
#include "resources/eventmodel.h"
David Jarvie's avatar
David Jarvie committed
27
#include "lib/autoqpointer.h"
28
#include "lib/filedialog.h"
David Jarvie's avatar
David Jarvie committed
29
30
#include "lib/messagebox.h"
#include "lib/shellprocess.h"
Laurent Montel's avatar
Laurent Montel committed
31
#include "config-kalarm.h"
32
#include "kalarm_debug.h"
David Jarvie's avatar
Spacing    
David Jarvie committed
33

David Jarvie's avatar
David Jarvie committed
34
#include <kalarmcal_version.h>
David Jarvie's avatar
David Jarvie committed
35
36
#include <KAlarmCal/Identities>
#include <KAlarmCal/KAEvent>
37

38
39
40
41
#include <KCalendarCore/Event>
#include <KCalendarCore/ICalFormat>
#include <KCalendarCore/Person>
#include <KCalendarCore/Duration>
42
#include <KCalendarCore/MemoryCalendar>
Laurent Montel's avatar
Laurent Montel committed
43
using namespace KCalendarCore;
David Jarvie's avatar
David Jarvie committed
44
45
#include <KIdentityManagement/IdentityManager>
#include <KIdentityManagement/Identity>
John Layt's avatar
John Layt committed
46
#include <KHolidays/HolidayRegion>
47

48
#include <KConfigGroup>
David Jarvie's avatar
Spacing    
David Jarvie committed
49
#include <KSharedConfig>
50
#include <KToggleAction>
51
#include <KLocalizedString>
David Jarvie's avatar
David Jarvie committed
52
#include <KAuth>
53
#include <KStandardShortcut>
54
55
56
57
#include <KFileItem>
#include <KJobWidgets>
#include <KIO/StatJob>
#include <KIO/StoredTransferJob>
58
#include <KFileCustomDialog>
59
#include <KWindowInfo>
Laurent Montel's avatar
Laurent Montel committed
60
#include <KWindowSystem>
David Jarvie's avatar
David Jarvie committed
61

David Jarvie's avatar
Tidy up    
David Jarvie committed
62
#include <QAction>
David Jarvie's avatar
David Jarvie committed
63
64
#include <QDBusConnectionInterface>
#include <QDBusInterface>
David Jarvie's avatar
David Jarvie committed
65
#include <QTimer>
66
#include <qglobal.h>
Laurent Montel's avatar
Laurent Montel committed
67
#include <QStandardPaths>
68
69
#include <QPushButton>
#include <QTemporaryFile>
70

71
//clazy:excludeall=non-pod-global-static
David Jarvie's avatar
David Jarvie committed
72
73
74

namespace
{
75
bool refreshAlarmsQueued = false;
76
QUrl lastImportUrl;     // last URL for Import Alarms file dialogue
77
QUrl lastExportUrl;     // last URL for Export Alarms file dialogue
David Jarvie's avatar
David Jarvie committed
78

79
80
struct UpdateStatusData
{
81
    KAlarm::UpdateResult status;   // status code and error message if any
82
83
84
85
    int                  warnErr;
    int                  warnKOrg;

    explicit UpdateStatusData(KAlarm::UpdateStatus s = KAlarm::UPDATE_OK) : status(s), warnErr(0), warnKOrg(0) {}
86
87
    int  failedCount() const      { return status.failed.count(); }
    void appendFailed(int index)  { status.failed.append(index); }
88
    // Set an error status and increment to number of errors to warn about
89
    void setError(KAlarm::UpdateStatus st, int errorCount = -1, const QString& msg = QString())
90
    {
91
92
93
94
95
96
97
98
99
100
101
        status.set(st, msg);
        if (errorCount < 0)
            ++warnErr;
        else
            warnErr = errorCount;
    }
    // Set an error status and increment to number of errors to warn about,
    // without changing the error message
    void updateError(KAlarm::UpdateStatus st, int errorCount = -1)
    {
        status.status = st;
102
103
104
105
106
107
        if (errorCount < 0)
            ++warnErr;
        else
            warnErr = errorCount;
    }
    // Update the error status with a KOrganizer related status
David Jarvie's avatar
David Jarvie committed
108
    void korgUpdate(const KAlarm::UpdateResult& result)
109
110
111
112
113
114
115
116
117
118
    {
        if (result.status != KAlarm::UPDATE_OK)
        {
            ++warnKOrg;
            if (result.status > status.status)
                status = result;
        }
    }
};

David Jarvie's avatar
David Jarvie committed
119
120
121
122
123
const QLatin1String KMAIL_DBUS_SERVICE("org.kde.kmail");
//const QLatin1String KMAIL_DBUS_IFACE("org.kde.kmail.kmail");
//const QLatin1String KMAIL_DBUS_WINDOW_PATH("/kmail/kmail_mainwindow_1");
const QLatin1String KORG_DBUS_SERVICE("org.kde.korganizer");
const QLatin1String KORG_DBUS_IFACE("org.kde.korganizer.Korganizer");
124
125
126
// D-Bus object path of KOrganizer's notification interface
#define       KORG_DBUS_PATH            "/Korganizer"
#define       KORG_DBUS_LOAD_PATH       "/korganizer_PimApplication"
David Jarvie's avatar
David Jarvie committed
127
//const QLatin1String KORG_DBUS_WINDOW_PATH("/korganizer/MainWindow_1");
128
const QLatin1String KORG_MIME_TYPE("application/x-vnd.akonadi.calendar.event");
129
const QLatin1String KORGANIZER_UID("korg-");
130

David Jarvie's avatar
David Jarvie committed
131
132
const QLatin1String ALARM_OPTS_FILE("alarmopts");
const char*         DONT_SHOW_ERRORS_GROUP = "DontShowErrors";
133

134
KAlarm::UpdateResult updateEvent(KAEvent&, KAlarm::UpdateError, QWidget* msgParent);
135
void editNewTemplate(EditAlarmDlg::Type, const KAEvent* preset, QWidget* parent);
136
137
138
139
void displayUpdateError(QWidget* parent, KAlarm::UpdateError, const UpdateStatusData&, bool showKOrgError = true);
KAlarm::UpdateResult sendToKOrganizer(const KAEvent&);
KAlarm::UpdateResult deleteFromKOrganizer(const QString& eventID);
KAlarm::UpdateResult runKOrganizer();
140
QString uidKOrganizer(const QString& eventID);
David Jarvie's avatar
David Jarvie committed
141
142
143
144
145
146
}


namespace KAlarm
{

Laurent Montel's avatar
Laurent Montel committed
147
Private* Private::mInstance = nullptr;
148

David Jarvie's avatar
David Jarvie committed
149
/******************************************************************************
David Jarvie's avatar
David Jarvie committed
150
* Display a main window with the specified event selected.
David Jarvie's avatar
David Jarvie committed
151
*/
152
MainWindow* displayMainWindowSelected(const QString& eventId)
David Jarvie's avatar
David Jarvie committed
153
{
154
155
156
157
158
159
160
161
162
163
164
165
    MainWindow* win = MainWindow::firstWindow();
    if (!win)
    {
        if (theApp()->checkCalendar())    // ensure calendar is open
        {
            win = MainWindow::create();
            win->show();
        }
    }
    else
    {
        // There is already a main window, so make it the active window
166
167
168
169
170
171
172
173
174
        KWindowInfo wi(win->winId(), NET::WMDesktop);
        if (!wi.valid()  ||  !wi.isOnDesktop(KWindowSystem::currentDesktop()))
        {
            // The main window isn't on the current desktop. Hide it first so
            // that it will be shown on the current desktop when it is shown
            // again. Note that this shifts the window's position, so don't
            // hide it if it's already on the current desktop.
            win->hide();
        }
175
176
        win->setWindowState(win->windowState() & ~Qt::WindowMinimized);
        win->show();
177
178
179
        win->raise();
        win->activateWindow();
    }
180
    if (win)
181
182
        win->selectEvent(eventId);
    return win;
David Jarvie's avatar
David Jarvie committed
183
184
}

185
186
187
188
189
/******************************************************************************
* Create an "Alarms Enabled/Enable Alarms" action.
*/
KToggleAction* createAlarmEnableAction(QObject* parent)
{
190
191
    KToggleAction* action = new KToggleAction(i18nc("@action", "Enable &Alarms"), parent);
    action->setChecked(theApp()->alarmsEnabled());
192
    QObject::connect(action, &QAction::toggled, theApp(), &KAlarmApp::setAlarmsEnabled);
193
    // The following line ensures that all instances are kept in the same state
194
    QObject::connect(theApp(), &KAlarmApp::alarmEnabledToggled, action, &QAction::setChecked);
195
    return action;
196
197
}

198
199
200
/******************************************************************************
* Create a "Stop Play" action.
*/
David Jarvie's avatar
Spacing    
David Jarvie committed
201
QAction* createStopPlayAction(QObject* parent)
202
{
Laurent Montel's avatar
Laurent Montel committed
203
    QAction* action = new QAction(QIcon::fromTheme(QStringLiteral("media-playback-stop")), i18nc("@action", "Stop Play"), parent);
204
    action->setEnabled(MessageDisplay::isAudioPlaying());
205
    QObject::connect(action, &QAction::triggered, theApp(), &KAlarmApp::stopAudio);
206
    // The following line ensures that all instances are kept in the same state
207
    QObject::connect(theApp(), &KAlarmApp::audioPlaying, action, &QAction::setEnabled);
208
    return action;
209
210
}

211
212
213
214
215
/******************************************************************************
* Create a "Spread Windows" action.
*/
KToggleAction* createSpreadWindowsAction(QObject* parent)
{
216
    KToggleAction* action = new KToggleAction(i18nc("@action", "Spread Windows"), parent);
217
    QObject::connect(action, &QAction::triggered, theApp(), &KAlarmApp::spreadWindows);
218
    // The following line ensures that all instances are kept in the same state
219
    QObject::connect(theApp(), &KAlarmApp::spreadWindowsToggled, action, &QAction::setChecked);
220
    return action;
221
222
}

David Jarvie's avatar
David Jarvie committed
223
/******************************************************************************
224
* Add a new active (non-archived) alarm, and save it in its resource.
225
226
227
* Parameters: msgParent = parent widget for any calendar selection prompt or
*                         error message.
*             event - is updated with the actual event ID.
David Jarvie's avatar
David Jarvie committed
228
*/
229
UpdateResult addEvent(KAEvent& event, Resource& resource, QWidget* msgParent, int options, bool showKOrgErr)
David Jarvie's avatar
David Jarvie committed
230
{
David Jarvie's avatar
David Jarvie committed
231
    qCDebug(KALARM_LOG) << "KAlarm::addEvent:" << event.id();
232
    bool cancelled = false;
233
    UpdateStatusData status;
234
    if (!theApp()->checkCalendar())    // ensure calendar is open
235
        status.status = UPDATE_FAILED;
236
237
238
    else
    {
        // Save the event details in the calendar file, and get the new event ID
239
240
        // Note that ResourcesCalendar::addEvent() updates 'event'.
        if (!ResourcesCalendar::addEvent(event, resource, msgParent, (options & USE_EVENT_ID), (options & NO_RESOURCE_PROMPT), &cancelled))
241
        {
242
            status.status = UPDATE_FAILED;
243
244
245
        }
        else
        {
246
            if (!resource.save(&status.status.message))
247
248
249
250
            {
                resource.reload(true);   // discard the new event
                status.status.status = SAVE_FAILED;
            }
251
        }
252
        if (status.status == UPDATE_OK)
253
254
255
        {
            if ((options & ALLOW_KORG_UPDATE)  &&  event.copyToKOrganizer())
            {
256
257
                UpdateResult st = sendToKOrganizer(event);    // tell KOrganizer to show the event
                status.korgUpdate(st);
258
259
260
            }
        }
    }
261

262
263
264
    if (status.status != UPDATE_OK  &&  !cancelled  &&  msgParent)
        displayUpdateError(msgParent, ERR_ADD, status, showKOrgErr);
    return status.status;
265
266
267
}

/******************************************************************************
268
* Add a list of new active (non-archived) alarms, and save them in the resource.
269
270
* The events are updated with their actual event IDs.
*/
271
UpdateResult addEvents(QVector<KAEvent>& events, Resource& resource, QWidget* msgParent, bool allowKOrgUpdate, bool showKOrgErr)
272
{
David Jarvie's avatar
David Jarvie committed
273
    qCDebug(KALARM_LOG) << "KAlarm::addEvents:" << events.count();
274
    if (events.isEmpty())
275
276
        return UpdateResult(UPDATE_OK);
    UpdateStatusData status;
277
    if (!theApp()->checkCalendar())    // ensure calendar is open
278
        status.status = UPDATE_FAILED;
279
280
    else
    {
281
282
        if (!resource.isValid())
            resource = Resources::destination(CalEvent::ACTIVE, msgParent);
283
        if (!resource.isValid())
284
        {
David Jarvie's avatar
David Jarvie committed
285
            qCDebug(KALARM_LOG) << "KAlarm::addEvents: No calendar";
286
            status.status = UPDATE_FAILED;
287
        }
288
        else
289
        {
290
            for (int i = 0, end = events.count();  i < end;  ++i)
291
            {
292
293
                // Save the event details in the calendar file, and get the new event ID
                KAEvent& event = events[i];
294
                if (!ResourcesCalendar::addEvent(event, resource, msgParent, false))
295
                {
296
                    status.appendFailed(i);
297
298
299
300
301
302
303
304
                    status.setError(UPDATE_ERROR);
                    continue;
                }
                if (allowKOrgUpdate  &&  event.copyToKOrganizer())
                {
                    UpdateResult st = sendToKOrganizer(event);    // tell KOrganizer to show the event
                    status.korgUpdate(st);
                }
305

306
            }
307
            if (status.failedCount() == events.count())
308
                status.status = UPDATE_FAILED;
309
            else if (!resource.save(&status.status.message))
310
311
312
313
            {
                resource.reload(true);   // discard the new events
                status.updateError(SAVE_FAILED, events.count());  // everything failed
            }
314
315
316
        }
    }

317
318
319
    if (status.status != UPDATE_OK  &&  msgParent)
        displayUpdateError(msgParent, ERR_ADD, status, showKOrgErr);
    return status.status;
David Jarvie's avatar
David Jarvie committed
320
321
}

322
/******************************************************************************
323
* Save the event in the archived calendar.
324
* The event's ID is changed to an archived ID if necessary.
325
*/
326
bool addArchivedEvent(KAEvent& event, Resource& resource)
327
{
David Jarvie's avatar
David Jarvie committed
328
    qCDebug(KALARM_LOG) << "KAlarm::addArchivedEvent:" << event.id();
329
    bool archiving = (event.category() == CalEvent::ACTIVE);
330
331
332
333
334
335
    if (archiving  &&  !Preferences::archivedKeepDays())
        return false;   // expired alarms aren't being kept
    KAEvent newevent(event);
    KAEvent* const newev = &newevent;
    if (archiving)
    {
336
        newev->setCategory(CalEvent::ARCHIVED);    // this changes the event ID
337
        newev->setCreatedDateTime(KADateTime::currentUtcDateTime());   // time stamp to control purging
338
    }
339
340
341
    // Add the event from the archived resource. It's not too important whether
    // the resource is saved successfully after the deletion, so allow it to be
    // saved automatically.
342
    if (!ResourcesCalendar::addEvent(newevent, resource, nullptr, false))
343
344
        return false;
    event = *newev;   // update event ID etc.
345

346
    return true;
347
348
349
350
351
}

/******************************************************************************
* Add a new template.
* 'event' is updated with the actual event ID.
352
* Parameters: promptParent = parent widget for any calendar selection prompt.
353
*/
354
UpdateResult addTemplate(KAEvent& event, Resource& resource, QWidget* msgParent)
355
{
David Jarvie's avatar
David Jarvie committed
356
    qCDebug(KALARM_LOG) << "KAlarm::addTemplate:" << event.id();
357
    UpdateStatusData status;
358

359
360
    // Add the template to the calendar file
    KAEvent newev(event);
361
    if (!ResourcesCalendar::addEvent(newev, resource, msgParent, false))
362
        status.status = UPDATE_FAILED;
363
364
365
    else
    {
        event = newev;   // update event ID etc.
366
        if (!resource.save(&status.status.message))
367
        {
368
369
            resource.reload(true);   // discard the new template
            status.status.status = SAVE_FAILED;
370
        }
371
372
        else
            return UpdateResult(UPDATE_OK);
373
    }
374

375
    if (msgParent)
376
377
        displayUpdateError(msgParent, ERR_TEMPLATE, status);
    return status.status;
378
379
}

David Jarvie's avatar
David Jarvie committed
380
/******************************************************************************
381
* Modify an active (non-archived) alarm in a resource.
382
* The new event must have a different event ID from the old one.
David Jarvie's avatar
David Jarvie committed
383
*/
384
UpdateResult modifyEvent(KAEvent& oldEvent, KAEvent& newEvent, QWidget* msgParent, bool showKOrgErr)
David Jarvie's avatar
David Jarvie committed
385
{
David Jarvie's avatar
David Jarvie committed
386
    qCDebug(KALARM_LOG) << "KAlarm::modifyEvent:" << oldEvent.id();
387

388
    UpdateStatusData status;
389
    Resource resource = Resources::resource(oldEvent.resourceId());
390
391
    if (!newEvent.isValid())
    {
392
        deleteEvent(oldEvent, resource, true);
393
        status.status = UPDATE_FAILED;
394
395
396
    }
    else
    {
397
        EventId oldId(oldEvent);
398
399
400
401
402
        if (oldEvent.copyToKOrganizer())
        {
            // Tell KOrganizer to delete its old event.
            // But ignore errors, because the user could have manually
            // deleted it since KAlarm asked KOrganizer to set it up.
403
            deleteFromKOrganizer(oldId.eventId());
404
        }
405
        // Update the event in the calendar file, and get the new event ID
406
        if (!ResourcesCalendar::modifyEvent(oldId, newEvent))
407
            status.status = UPDATE_FAILED;
408
409
        else
        {
410
            if (!resource.save(&status.status.message))
411
412
413
414
            {
                resource.reload(true);   // retrieve the pre-update version of the event
                status.status.status = SAVE_FAILED;
            }
415
            if (status.status == UPDATE_OK)
416
            {
417
                if (newEvent.copyToKOrganizer())
418
                {
419
420
                    UpdateResult st = sendToKOrganizer(newEvent);    // tell KOrganizer to show the new event
                    status.korgUpdate(st);
421
422
423
424
425
426
427
                }

                // Remove "Don't show error messages again" for the old alarm
                setDontShowErrors(oldId);
            }
        }
    }
428

429
430
431
    if (status.status != UPDATE_OK  &&  msgParent)
        displayUpdateError(msgParent, ERR_MODIFY, status, showKOrgErr);
    return status.status;
David Jarvie's avatar
David Jarvie committed
432
433
434
}

/******************************************************************************
435
* Update an active (non-archived) alarm in its resource.
David Jarvie's avatar
David Jarvie committed
436
* The new event will have the same event ID as the old one.
437
438
* The event is not updated in KOrganizer, since this function is called when an
* existing alarm is rescheduled (due to recurrence or deferral).
David Jarvie's avatar
David Jarvie committed
439
*/
440
UpdateResult updateEvent(KAEvent& event, QWidget* msgParent, bool archiveOnDelete)
David Jarvie's avatar
David Jarvie committed
441
{
David Jarvie's avatar
David Jarvie committed
442
    qCDebug(KALARM_LOG) << "KAlarm::updateEvent:" << event.id();
David Jarvie's avatar
David Jarvie committed
443

444
445
    if (!event.isValid())
    {
446
447
448
        Resource resource;
        deleteEvent(event, resource, archiveOnDelete);
        return UpdateResult(UPDATE_OK);
449
    }
450
451
452

    // Update the event in the resource.
    return ::updateEvent(event, ERR_MODIFY, msgParent);
David Jarvie's avatar
David Jarvie committed
453
454
}

455
/******************************************************************************
456
* Update a template in its resource.
457
*/
458
UpdateResult updateTemplate(KAEvent& event, QWidget* msgParent)
459
{
460
    return ::updateEvent(event, ERR_TEMPLATE, msgParent);
461
462
}

David Jarvie's avatar
David Jarvie committed
463
/******************************************************************************
464
* Delete alarms from a resource.
465
* If the events are archived, the events' IDs are changed to archived IDs if necessary.
David Jarvie's avatar
David Jarvie committed
466
*/
467
UpdateResult deleteEvent(KAEvent& event, Resource& resource, bool archive, QWidget* msgParent, bool showKOrgErr)
David Jarvie's avatar
David Jarvie committed
468
{
469
    QVector<KAEvent> events(1, event);
470
    return deleteEvents(events, resource, archive, msgParent, showKOrgErr);
471
}
David Jarvie's avatar
David Jarvie committed
472

473
UpdateResult deleteEvents(QVector<KAEvent>& events, Resource& resource, bool archive, QWidget* msgParent, bool showKOrgErr)
474
{
David Jarvie's avatar
David Jarvie committed
475
    qCDebug(KALARM_LOG) << "KAlarm::deleteEvents:" << events.count();
476
    if (events.isEmpty())
477
478
        return UpdateResult(UPDATE_OK);
    UpdateStatusData status;
David Jarvie's avatar
David Jarvie committed
479
    bool deleteWakeFromSuspendAlarm = false;
David Jarvie's avatar
David Jarvie committed
480
    const QString wakeFromSuspendId = checkRtcWakeConfig().value(0);
481
    QSet<Resource> resources;   // resources which events have been deleted from
482
483
484
485
    for (int i = 0, end = events.count();  i < end;  ++i)
    {
        // Save the event details in the calendar file, and get the new event ID
        KAEvent* event = &events[i];
David Jarvie's avatar
David Jarvie committed
486
        const QString id = event->id();
487

488
        // Delete the event from the calendar file
489
        if (event->category() != CalEvent::ARCHIVED)
490
491
492
493
494
495
        {
            if (event->copyToKOrganizer())
            {
                // The event was shown in KOrganizer, so tell KOrganizer to
                // delete it. But ignore errors, because the user could have
                // manually deleted it from KOrganizer since it was set up.
496
497
                UpdateResult st = deleteFromKOrganizer(id);
                status.korgUpdate(st);
498
499
500
            }
            if (archive  &&  event->toBeArchived())
            {
501
                Resource archResource;
502
                KAEvent ev(*event);
503
                addArchivedEvent(ev, archResource);  // this changes the event ID to an archived ID
504
505
            }
        }
506
        Resource res = resource;
507
        if (ResourcesCalendar::deleteEvent(*event, res, false))   // don't save calendar after deleting
508
509
510
            resources.insert(res);
        else
            status.appendFailed(i);
511

David Jarvie's avatar
David Jarvie committed
512
513
        if (id == wakeFromSuspendId)
            deleteWakeFromSuspendAlarm = true;
514

515
        // Remove "Don't show error messages again" for this alarm
516
        setDontShowErrors(EventId(*event));
517
518
    }

519
520
521
522
523
524
525
526
527
    if (!resource.isValid()  &&  resources.size() == 1)
        resource = *resources.constBegin();

    if (status.failedCount())
        status.setError(status.failedCount() == events.count() ? UPDATE_FAILED : UPDATE_ERROR, status.failedCount());
    if (status.failedCount() < events.count())
    {
        QString msg;
        for (Resource res : resources)
528
            if (!res.save(&msg))
529
530
531
532
533
            {
                res.reload(true);    // retrieve the events which couldn't be deleted
                status.setError(SAVE_FAILED, status.failedCount(), msg);
            }
    }
534
535
    if (status.status != UPDATE_OK  &&  msgParent)
        displayUpdateError(msgParent, ERR_DELETE, status, showKOrgErr);
David Jarvie's avatar
David Jarvie committed
536
537
538
539
540

    // Remove any wake-from-suspend scheduled for a deleted alarm
    if (deleteWakeFromSuspendAlarm  &&  !wakeFromSuspendId.isEmpty())
        cancelRtcWake(msgParent, wakeFromSuspendId);

541
    return status.status;
David Jarvie's avatar
David Jarvie committed
542
543
}

544
/******************************************************************************
545
* Delete templates from the calendar file.
546
*/
547
UpdateResult deleteTemplates(const KAEvent::List& events, QWidget* msgParent)
548
{
549
    int count = events.count();
David Jarvie's avatar
David Jarvie committed
550
    qCDebug(KALARM_LOG) << "KAlarm::deleteTemplates:" << count;
551
    if (!count)
552
553
        return UpdateResult(UPDATE_OK);
    UpdateStatusData status;
554
555
    QSet<Resource> resources;   // resources which events have been deleted from
    for (int i = 0, end = events.count();  i < end;  ++i)
556
557
558
    {
        // Update the window lists
        // Delete the template from the calendar file
559
560
        const KAEvent* event = events[i];
        Resource resource;
561
        if (ResourcesCalendar::deleteEvent(*event, resource, false))   // don't save calendar after deleting
562
563
564
565
566
567
568
569
570
571
572
            resources.insert(resource);
        else
            status.appendFailed(i);
    }

    if (status.failedCount())
        status.setError(status.failedCount() == events.count() ? UPDATE_FAILED : UPDATE_ERROR, status.failedCount());
    if (status.failedCount() < events.count())
    {
        QString msg;
        for (Resource res : resources)
573
            if (!res.save(&msg))
574
575
576
577
578
            {
                res.reload(true);    // retrieve the templates which couldn't be deleted
                status.status.message = msg;
                status.setError(SAVE_FAILED, status.failedCount(), msg);
            }
579
580
    }

581
582
583
    if (status.status != UPDATE_OK  &&  msgParent)
        displayUpdateError(msgParent, ERR_TEMPLATE, status);
    return status.status;
584
585
}

David Jarvie's avatar
David Jarvie committed
586
587
588
589
590
/******************************************************************************
* Delete an alarm from the display calendar.
*/
void deleteDisplayEvent(const QString& eventID)
{
David Jarvie's avatar
David Jarvie committed
591
    qCDebug(KALARM_LOG) << "KAlarm::deleteDisplayEvent:" << eventID;
592
593
    if (DisplayCalendar::open())
        DisplayCalendar::deleteEvent(eventID, true);   // save calendar after deleting
David Jarvie's avatar
David Jarvie committed
594
595
596
}

/******************************************************************************
597
* Undelete archived alarms.
598
* The archive bit is set to ensure that they get re-archived if deleted again.
599
600
601
* Parameters:
*   calendar - the active alarms calendar to restore the alarms into, or null
*              to use the default way of determining the active alarm calendar.
602
*   ineligibleIndexes - will be filled in with the indexes to any ineligible events.
David Jarvie's avatar
David Jarvie committed
603
*/
604
UpdateResult reactivateEvent(KAEvent& event, Resource& resource, QWidget* msgParent, bool showKOrgErr)
David Jarvie's avatar
David Jarvie committed
605
{
606
    QVector<int> ids;
607
    QVector<KAEvent> events(1, event);
608
    return reactivateEvents(events, ids, resource, msgParent, showKOrgErr);
609
}
David Jarvie's avatar
David Jarvie committed
610

611
UpdateResult reactivateEvents(QVector<KAEvent>& events, QVector<int>& ineligibleIndexes, Resource& resource, QWidget* msgParent, bool showKOrgErr)
612
{
David Jarvie's avatar
David Jarvie committed
613
    qCDebug(KALARM_LOG) << "KAlarm::reactivateEvents:" << events.count();
614
    ineligibleIndexes.clear();
615
    if (events.isEmpty())
616
617
        return UpdateResult(UPDATE_OK);
    UpdateStatusData status;
618
    if (!resource.isValid())
619
        resource = Resources::destination(CalEvent::ACTIVE, msgParent);
620
    if (!resource.isValid())
621
    {
David Jarvie's avatar
David Jarvie committed
622
        qCDebug(KALARM_LOG) << "KAlarm::reactivateEvents: No calendar";
623
        status.setError(UPDATE_FAILED, events.count());
624
625
626
627
    }
    else
    {
        int count = 0;
628
        const KADateTime now = KADateTime::currentUtcDateTime();
629
        QVector<KAEvent> eventsToDelete;
630
631
632
633
        for (int i = 0, end = events.count();  i < end;  ++i)
        {
            // Delete the event from the archived resource
            KAEvent* event = &events[i];
634
            if (event->category() != CalEvent::ARCHIVED
635
636
            ||  !event->occursAfter(now, true))
            {
637
                ineligibleIndexes += i;
638
639
640
                continue;
            }
            ++count;
641

642
643
            KAEvent newevent(*event);
            KAEvent* const newev = &newevent;
644
            newev->setCategory(CalEvent::ACTIVE);    // this changes the event ID
645
646
647
            if (newev->recurs()  ||  newev->repetition())
                newev->setNextOccurrence(now);   // skip any recurrences in the past
            newev->setArchive();    // ensure that it gets re-archived if it is deleted
David Jarvie's avatar
David Jarvie committed
648

649
650
            // Save the event details in the calendar file.
            // This converts the event ID.
651
            if (!ResourcesCalendar::addEvent(newevent, resource, msgParent, true))
652
            {
653
                status.appendFailed(i);
654
655
656
657
                continue;
            }
            if (newev->copyToKOrganizer())
            {
658
659
                UpdateResult st = sendToKOrganizer(*newev);    // tell KOrganizer to show the event
                status.korgUpdate(st);
660
            }
661

662
            if (ResourcesCalendar::event(EventId(*event)).isValid())  // no error if event doesn't exist in archived resource
663
                eventsToDelete.append(*event);
664
665
666
            events[i] = newevent;
        }

667
668
669
670
        if (status.failedCount())
            status.setError(status.failedCount() == events.count() ? UPDATE_FAILED : UPDATE_ERROR, status.failedCount());
        if (status.failedCount() < events.count())
        {
671
            if (!resource.save(&status.status.message))
672
673
674
675
676
677
678
679
680
681
682
683
            {
                resource.reload(true);
                status.updateError(SAVE_FAILED, count);
            }
            else
            {
                // Delete the events from the archived resources. It's not too
                // important whether the resources are saved successfully after
                // the deletion, so allow them to be saved automatically.
                for (const KAEvent& event : eventsToDelete)
                {
                    Resource res = Resources::resource(event.resourceId());
684
                    if (!ResourcesCalendar::deleteEvent(event, res, false))
685
686
687
688
                        status.setError(UPDATE_ERROR);
                }
            }
        }
689
    }
690
691
692
    if (status.status != UPDATE_OK  &&  msgParent)
        displayUpdateError(msgParent, ERR_REACTIVATE, status, showKOrgErr);
    return status.status;
David Jarvie's avatar
David Jarvie committed
693
694
}

695
/******************************************************************************
696
* Enable or disable alarms.
697
* The new events will have the same event IDs as the old ones.
698
*/
699
UpdateResult enableEvents(QVector<KAEvent>& events, bool enable, QWidget* msgParent)
700
{
David Jarvie's avatar
David Jarvie committed
701
    qCDebug(KALARM_LOG) << "KAlarm::enableEvents:" << events.count();
702
    if (events.isEmpty())
703
704
        return UpdateResult(UPDATE_OK);
    UpdateStatusData status;
David Jarvie's avatar
David Jarvie committed
705
    bool deleteWakeFromSuspendAlarm = false;
David Jarvie's avatar
David Jarvie committed
706
    const QString wakeFromSuspendId = checkRtcWakeConfig().value(0);
707
    QSet<ResourceId> resourceIds;   // resources whose events have been updated
708
709
710
    for (int i = 0, end = events.count();  i < end;  ++i)
    {
        KAEvent* event = &events[i];
711
        if (event->category() == CalEvent::ACTIVE
712
713
714
715
        &&  enable != event->enabled())
        {
            event->setEnabled(enable);

David Jarvie's avatar
David Jarvie committed
716
717
718
            if (!enable  &&  event->id() == wakeFromSuspendId)
                deleteWakeFromSuspendAlarm = true;

719
            // Update the event in the calendar file
720
            const KAEvent newev = ResourcesCalendar::updateEvent(*event);
721
            if (!newev.isValid())
722
            {
David Jarvie's avatar
David Jarvie committed
723
                qCCritical(KALARM_LOG) << "KAlarm::enableEvents: Error updating event in calendar:" << event->id();
724
725
                status.appendFailed(i);
            }
726
727
            else
            {
728
                resourceIds.insert(event->resourceId());
729
                ResourcesCalendar::disabledChanged(newev);
730

731
                // If we're disabling a display alarm, close any message display
732
                if (!enable  &&  (event->actionTypes() & KAEvent::ACT_DISPLAY))
733
                {
734
                    MessageDisplay* win = MessageDisplay::findEvent(EventId(*event));
735
736
737
738
739
740
                    delete win;
                }
            }
        }
    }

741
742
743
744
745
746
747
748
    if (status.failedCount())
        status.setError(status.failedCount() == events.count() ? UPDATE_FAILED : UPDATE_ERROR, status.failedCount());
    if (status.failedCount() < events.count())
    {
        QString msg;
        for (ResourceId id : resourceIds)
        {
            Resource res = Resources::resource(id);
749
            if (!res.save(&msg))
750
751
752
753
754
755
756
757
            {
                // Don't reload resource after failed save. It's better to
                // keep the new enabled status of the alarms at least until
                // KAlarm is restarted.
                status.setError(SAVE_FAILED, status.failedCount(), msg);
            }
        }
    }
758
759
    if (status.status != UPDATE_OK  &&  msgParent)
        displayUpdateError(msgParent, ERR_ADD, status);
David Jarvie's avatar
David Jarvie committed
760
761
762
763
764

    // Remove any wake-from-suspend scheduled for a disabled alarm
    if (deleteWakeFromSuspendAlarm  &&  !wakeFromSuspendId.isEmpty())
        cancelRtcWake(msgParent, wakeFromSuspendId);

765
    return status.status;
766
767
768
}

/******************************************************************************
769
770
771
772
773
774
775
776
777
* This method must only be called from the main KAlarm queue processing loop,
* to prevent asynchronous calendar operations interfering with one another.
*
* Purge all archived events from the default archived alarm resource whose end
* time is longer ago than 'purgeDays'. All events are deleted if 'purgeDays' is
* zero.
*/
void purgeArchive(int purgeDays)
{
778
779
    if (purgeDays < 0)
        return;
David Jarvie's avatar
David Jarvie committed
780
    qCDebug(KALARM_LOG) << "KAlarm::purgeArchive:" << purgeDays;
781
    const QDate cutoff = KADateTime::currentLocalDate().addDays(-purgeDays);
782
    const Resource resource = Resources::getStandard(CalEvent::ARCHIVED);
783
    if (!resource.isValid())
784
        return;
785
    QVector<KAEvent> events = ResourcesCalendar::events(resource);
786
787
    for (int i = 0;  i < events.count();  )
    {
788
        if (purgeDays  &&  events.at(i).createdDateTime().date() >= cutoff)
789
            events.remove(i);
790
791
792
793
        else
            ++i;
    }
    if (!events.isEmpty())
794
795
796
797
798
799
800
801
802
803
804
805
        ResourcesCalendar::purgeEvents(events);   // delete the events and save the calendar
}

/******************************************************************************
* Return whether an event is read-only.
*/
bool eventReadOnly(const QString& eventId)
{
    KAEvent event;
    const Resource resource = Resources::resourceForEvent(eventId, event);
    return !event.isValid()  ||  event.isReadOnly()
       ||  !resource.isWritable(event.category());
806
807
}

808
809
810
811
812
813
814
/******************************************************************************
* Display an error message about an error when saving an event.
* If 'model' is non-null, the AlarmListModel* which it points to is used; if
* that is null, it is created.
*/
QVector<KAEvent> getSortedActiveEvents(QObject* parent, AlarmListModel** model)
{
Laurent Montel's avatar
Laurent Montel committed
815
    AlarmListModel* mdl = nullptr;
816
817
818
819
    if (!model)
        model = &mdl;
    if (!*model)
    {
820
        *model = DataModel::createAlarmListModel(parent);
821
822
823
824
825
826
        (*model)->setEventTypeFilter(CalEvent::ACTIVE);
        (*model)->sort(AlarmListModel::TimeColumn);
    }
    QVector<KAEvent> result;
    for (int i = 0, count = (*model)->rowCount();  i < count;  ++i)
    {
David Jarvie's avatar
David Jarvie committed
827
        const KAEvent event = (*model)->event(i);
828
829
830
831
832
833
        if (event.enabled()  &&  !event.expired())
            result += event;
    }
    return result;
}

834
835
836
837
838
839
840
841
842
843
/******************************************************************************
* Import alarms from an external calendar and merge them into KAlarm's calendar.
* The alarms are given new unique event IDs.
* Parameters: parent = parent widget for error message boxes
* Reply = true if all alarms in the calendar were successfully imported
*       = false if any alarms failed to be imported.
*/
bool importAlarms(Resource& resource, QWidget* parent)
{
    qCDebug(KALARM_LOG) << "KAlarm::importAlarms" << resource.displayId();
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
    // Use KFileCustomDialog to allow files' mime types to be determined by
    // both file name and content, instead of QFileDialog which only looks at
    // files' names. This is needed in particular when importing an old KAlarm
    // calendar directory, in order to list the calendar files within it, since
    // each calendar file name is simply the UID of the event within it, without
    // a .ics extension.
    AutoQPointer<KFileCustomDialog> dlg = new KFileCustomDialog(lastImportUrl, parent);
    dlg->setWindowTitle(i18nc("@title:window", "Import Calendar Files"));
    KFileWidget* widget = dlg->fileWidget();
    widget->setOperationMode(KFileWidget::Opening);
    widget->setMode(KFile::Files | KFile::ExistingOnly);
    widget->setMimeFilter({QStringLiteral("text/calendar")});
    dlg->setWindowModality(Qt::WindowModal);
    dlg->exec();
    if (!dlg)
        return false;
    const QList<QUrl> urls = widget->selectedUrls();
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
    if (urls.isEmpty())
        return false;
    lastImportUrl = urls[0].adjusted(QUrl::RemoveFilename);

    const CalEvent::Types alarmTypes = resource.isValid() ? resource.alarmTypes() : CalEvent::ACTIVE | CalEvent::ARCHIVED | CalEvent::TEMPLATE;

    // Read all the selected calendar files and extract their alarms.
    QHash<CalEvent::Type, QVector<KAEvent>> events;
    for (const QUrl& url : urls)
    {
        if (!url.isValid())
        {
            qCDebug(KALARM_LOG) << "KAlarm::importAlarms: Invalid URL";