Updated the Tools menu to reflect current build.
[kdepim.git] / kalarm / kalarmapp.cpp
blob2ec5b0163d66974f0b5094a81ff4489d3ad3fef7
1 /*
2 * kalarmapp.cpp - the KAlarm application object
3 * Program: kalarm
4 * Copyright © 2001-2013 by David Jarvie <djarvie@kde.org>
6 * This program is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; either version 2 of the License, or
9 * (at your option) any later version.
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
16 * You should have received a copy of the GNU General Public License along
17 * with this program; if not, write to the Free Software Foundation, Inc.,
18 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
21 #include "kalarm.h"
22 #include "kalarmapp.moc"
24 #include "alarmcalendar.h"
25 #include "alarmlistview.h"
26 #include "alarmtime.h"
27 #include "autoqpointer.h"
28 #include "commandoptions.h"
29 #include "dbushandler.h"
30 #include "editdlgtypes.h"
31 #ifdef USE_AKONADI
32 #include "collectionmodel.h"
33 #else
34 #include "eventlistmodel.h"
35 #endif
36 #include "functions.h"
37 #include "kamail.h"
38 #include "mainwindow.h"
39 #include "messagebox.h"
40 #include "messagewin.h"
41 #include "preferences.h"
42 #include "prefdlg.h"
43 #include "shellprocess.h"
44 #include "startdaytimer.h"
45 #include "traywindow.h"
47 #include "kspeechinterface.h"
49 #include <kalarmcal/datetime.h>
50 #include <kalarmcal/karecurrence.h>
52 #include <klocale.h>
53 #include <kstandarddirs.h>
54 #include <kconfig.h>
55 #include <kaboutdata.h>
56 #include <ktemporaryfile.h>
57 #include <kfileitem.h>
58 #include <kglobal.h>
59 #include <kstandardguiitem.h>
60 #include <kservicetypetrader.h>
61 #include <kspeech.h>
62 #include <ktoolinvocation.h>
63 #include <netwm.h>
64 #include <kdebug.h>
65 #include <kshell.h>
66 #include <ksystemtrayicon.h>
67 #include <ksystemtimezone.h>
69 #include <QObject>
70 #include <QTimer>
71 #include <QRegExp>
72 #include <QFile>
73 #include <QByteArray>
74 #include <QTextStream>
75 #include <QtDBus/QtDBus>
77 #include <stdlib.h>
78 #include <ctype.h>
79 #include <iostream>
80 #include <climits>
82 static const char* KTTSD_DBUS_SERVICE = "org.kde.kttsd";
83 static const char* KTTDS_DBUS_PATH = "/KSpeech";
85 #ifdef USE_AKONADI
86 static const int AKONADI_TIMEOUT = 30; // timeout (seconds) for Akonadi collections to be populated
87 #endif
89 static void setEventCommandError(const KAEvent&, KAEvent::CmdErrType);
90 static void clearEventCommandError(const KAEvent&, KAEvent::CmdErrType);
92 /******************************************************************************
93 * Find the maximum number of seconds late which a late-cancel alarm is allowed
94 * to be. This is calculated as the late cancel interval, plus a few seconds
95 * leeway to cater for any timing irregularities.
97 static inline int maxLateness(int lateCancel)
99 static const int LATENESS_LEEWAY = 5;
100 int lc = (lateCancel >= 1) ? (lateCancel - 1)*60 : 0;
101 return LATENESS_LEEWAY + lc;
105 KAlarmApp* KAlarmApp::theInstance = 0;
106 int KAlarmApp::mActiveCount = 0;
107 int KAlarmApp::mFatalError = 0;
108 QString KAlarmApp::mFatalMessage;
111 /******************************************************************************
112 * Construct the application.
114 KAlarmApp::KAlarmApp()
115 : KUniqueApplication(),
116 mInitialised(false),
117 mQuitting(false),
118 mReadOnly(false),
119 mLoginAlarmsDone(false),
120 mDBusHandler(new DBusHandler()),
121 mTrayWindow(0),
122 mAlarmTimer(new QTimer(this)),
123 mArchivedPurgeDays(-1), // default to not purging
124 mPurgeDaysQueued(-1),
125 mKSpeech(0),
126 mPendingQuit(false),
127 mCancelRtcWake(false),
128 mProcessingQueue(false),
129 mSessionClosingDown(false),
130 mAlarmsEnabled(true),
131 mSpeechEnabled(false)
133 KGlobal::locale()->insertCatalog("libkdepim");
134 KGlobal::locale()->insertCatalog("libkpimutils");
135 kDebug();
136 #ifndef NDEBUG
137 KAlarm::setTestModeConditions();
138 #endif
139 mAlarmTimer->setSingleShot(true);
140 connect(mAlarmTimer, SIGNAL(timeout()), SLOT(checkNextDueAlarm()));
142 setQuitOnLastWindowClosed(false);
143 Preferences::self(); // read KAlarm configuration
144 if (!Preferences::noAutoStart())
146 Preferences::setAutoStart(true);
147 Preferences::self()->writeConfig();
149 Preferences::connect(SIGNAL(startOfDayChanged(QTime)), this, SLOT(changeStartOfDay()));
150 Preferences::connect(SIGNAL(workTimeChanged(QTime,QTime,QBitArray)), this, SLOT(slotWorkTimeChanged(QTime,QTime,QBitArray)));
151 Preferences::connect(SIGNAL(holidaysChanged(KHolidays::HolidayRegion)), this, SLOT(slotHolidaysChanged(KHolidays::HolidayRegion)));
152 Preferences::connect(SIGNAL(feb29TypeChanged(Feb29Type)), this, SLOT(slotFeb29TypeChanged(Feb29Type)));
153 Preferences::connect(SIGNAL(showInSystemTrayChanged(bool)), this, SLOT(slotShowInSystemTrayChanged()));
154 Preferences::connect(SIGNAL(archivedKeepDaysChanged(int)), this, SLOT(setArchivePurgeDays()));
155 Preferences::connect(SIGNAL(messageFontChanged(QFont)), this, SLOT(slotMessageFontChanged(QFont)));
156 slotFeb29TypeChanged(Preferences::defaultFeb29Type());
158 connect(QDBusConnection::sessionBus().interface(), SIGNAL(serviceUnregistered(QString)),
159 SLOT(slotDBusServiceUnregistered(QString)));
160 KAEvent::setStartOfDay(Preferences::startOfDay());
161 KAEvent::setWorkTime(Preferences::workDays(), Preferences::workDayStart(), Preferences::workDayEnd());
162 KAEvent::setHolidays(Preferences::holidays());
163 KAEvent::setDefaultFont(Preferences::messageFont());
164 if (AlarmCalendar::initialiseCalendars())
166 connect(AlarmCalendar::resources(), SIGNAL(earliestAlarmChanged()), SLOT(checkNextDueAlarm()));
167 #ifdef USE_AKONADI
168 connect(AlarmCalendar::resources(), SIGNAL(atLoginEventAdded(KAEvent)), SLOT(atLoginEventAdded(KAEvent)));
169 connect(AkonadiModel::instance(), SIGNAL(collectionAdded(Akonadi::Collection)),
170 SLOT(purgeNewArchivedDefault(Akonadi::Collection)));
171 connect(AkonadiModel::instance(), SIGNAL(collectionTreeFetched(Akonadi::Collection::List)),
172 SLOT(checkWritableCalendar()));
173 #endif
175 KConfigGroup config(KGlobal::config(), "General");
176 mNoSystemTray = config.readEntry("NoSystemTray", false);
177 mOldShowInSystemTray = wantShowInSystemTray();
178 DateTime::setStartOfDay(Preferences::startOfDay());
179 mPrefsArchivedColour = Preferences::archivedColour();
182 // Check if the speech synthesis daemon is installed
183 mSpeechEnabled = (KServiceTypeTrader::self()->query("DBUS/Text-to-Speech", "Name == 'KTTSD'").count() > 0);
184 if (!mSpeechEnabled) { kDebug() << "Speech synthesis disabled (KTTSD not found)"; }
185 // Check if KOrganizer is installed
186 QString korg = QLatin1String("korganizer");
187 mKOrganizerEnabled = !KStandardDirs::locate("exe", korg).isNull() || !KStandardDirs::findExe(korg).isNull();
188 if (!mKOrganizerEnabled) { kDebug() << "KOrganizer options disabled (KOrganizer not found)"; }
191 /******************************************************************************
193 KAlarmApp::~KAlarmApp()
195 while (!mCommandProcesses.isEmpty())
197 ProcData* pd = mCommandProcesses[0];
198 mCommandProcesses.pop_front();
199 delete pd;
201 AlarmCalendar::terminateCalendars();
204 /******************************************************************************
205 * Return the one and only KAlarmApp instance.
206 * If it doesn't already exist, it is created first.
208 KAlarmApp* KAlarmApp::getInstance()
210 if (!theInstance)
212 theInstance = new KAlarmApp;
214 if (mFatalError)
215 theInstance->quitFatal();
217 return theInstance;
220 /******************************************************************************
221 * Restore the saved session if required.
223 bool KAlarmApp::restoreSession()
225 if (!isSessionRestored())
226 return false;
227 if (mFatalError)
229 quitFatal();
230 return false;
233 // Process is being restored by session management.
234 kDebug() << "Restoring";
235 ++mActiveCount;
236 // Create the session config object now.
237 // This is necessary since if initCheck() below causes calendars to be updated,
238 // the session config created after that points to an invalid file, resulting
239 // in no windows being restored followed by a later crash.
240 kapp->sessionConfig();
242 // When KAlarm is session restored, automatically set start-at-login to true.
243 Preferences::self()->readConfig();
244 Preferences::setAutoStart(true);
245 Preferences::setNoAutoStart(false);
246 Preferences::setAskAutoStart(true); // cancel any start-at-login prompt suppression
247 Preferences::self()->writeConfig();
249 if (!initCheck(true)) // open the calendar file (needed for main windows), don't process queue yet
251 --mActiveCount;
252 quitIf(1, true); // error opening the main calendar - quit
253 return false;
255 MainWindow* trayParent = 0;
256 for (int i = 1; KMainWindow::canBeRestored(i); ++i)
258 QString type = KMainWindow::classNameOfToplevel(i);
259 if (type == QLatin1String("MainWindow"))
261 MainWindow* win = MainWindow::create(true);
262 win->restore(i, false);
263 if (win->isHiddenTrayParent())
264 trayParent = win;
265 else
266 win->show();
268 else if (type == QLatin1String("MessageWin"))
270 MessageWin* win = new MessageWin;
271 win->restore(i, false);
272 if (win->isValid())
273 win->show();
274 else
275 delete win;
279 // Try to display the system tray icon if it is configured to be shown
280 if (trayParent || wantShowInSystemTray())
282 if (!MainWindow::count())
283 kWarning() << "no main window to be restored!?";
284 else
286 displayTrayIcon(true, trayParent);
287 // Occasionally for no obvious reason, the main main window is
288 // shown when it should be hidden, so hide it just to be sure.
289 if (trayParent)
290 trayParent->hide();
294 --mActiveCount;
295 if (quitIf(0)) // quit if no windows are open
296 return false; // quitIf() can sometimes return, despite calling exit()
298 // Check whether the KDE time zone daemon is running (but don't hold up initialisation)
299 QTimer::singleShot(0, this, SLOT(checkKtimezoned()));
301 startProcessQueue(); // start processing the execution queue
302 return true;
305 /******************************************************************************
306 * Called for a KUniqueApplication when a new instance of the application is
307 * started.
309 int KAlarmApp::newInstance()
311 kDebug();
312 if (mFatalError)
314 quitFatal();
315 return 1;
317 ++mActiveCount;
318 int exitCode = 0; // default = success
319 static bool firstInstance = true;
320 bool dontRedisplay = false;
321 if (!firstInstance || !isSessionRestored())
323 CommandOptions options; // fetch and parse command line options
324 #ifndef NDEBUG
325 if (options.simulationTime().isValid())
326 KAlarm::setSimulatedSystemTime(options.simulationTime());
327 #endif
328 CommandOptions::Command command = options.command();
329 if (options.disableAll())
330 setAlarmsEnabled(false); // disable alarm monitoring
331 switch (command)
333 case CommandOptions::TRIGGER_EVENT:
334 case CommandOptions::CANCEL_EVENT:
336 // Display or delete the event with the specified event ID
337 EventFunc function = (command == CommandOptions::TRIGGER_EVENT) ? EVENT_TRIGGER : EVENT_CANCEL;
338 // Open the calendar, don't start processing execution queue yet,
339 // and wait for the Akonadi collection to be populated.
340 #ifdef USE_AKONADI
341 if (!initCheck(true, true, options.eventId().collectionId()))
342 #else
343 if (!initCheck(true))
344 #endif
345 exitCode = 1;
346 else
348 startProcessQueue(); // start processing the execution queue
349 dontRedisplay = true;
350 #ifdef USE_AKONADI
351 if (!handleEvent(options.eventId(), function, true))
352 #else
353 if (!handleEvent(options.eventId(), function))
354 #endif
356 #ifdef USE_AKONADI
357 CommandOptions::printError(i18nc("@info:shell", "%1: Event <resource>%2</resource> not found, or not unique", "--" + options.commandName(), options.eventId().eventId()));
358 #else
359 CommandOptions::printError(i18nc("@info:shell", "%1: Event <resource>%2</resource> not found", "--" + options.commandName(), options.eventId()));
360 #endif
361 exitCode = 1;
364 break;
366 case CommandOptions::LIST:
367 // Output a list of scheduled alarms to stdout.
368 // Open the calendar, don't start processing execution queue yet,
369 // and wait for all Akonadi collections to be populated.
370 mReadOnly = true; // don't need write access to calendars
371 #ifdef USE_AKONADI
372 if (!initCheck(true, true))
373 #else
374 if (!initCheck(true))
375 #endif
376 exitCode = 1;
377 else
379 dontRedisplay = true;
380 QStringList alarms = scheduledAlarmList();
381 for (int i = 0, count = alarms.count(); i < count; ++i)
382 std::cout << alarms[i].toUtf8().constData() << std::endl;
384 break;
385 case CommandOptions::EDIT:
386 // Edit a specified existing alarm.
387 // Open the calendar and wait for the Akonadi collection to be populated.
388 #ifdef USE_AKONADI
389 if (!initCheck(false, true, options.eventId().collectionId()))
390 #else
391 if (!initCheck())
392 #endif
393 exitCode = 1;
394 else if (!KAlarm::editAlarmById(options.eventId()))
396 #ifdef USE_AKONADI
397 CommandOptions::printError(i18nc("@info:shell", "%1: Event <resource>%2</resource> not found, or not editable", "--" + options.commandName(), options.eventId().eventId()));
398 #else
399 CommandOptions::printError(i18nc("@info:shell", "%1: Event <resource>%2</resource> not found, or not editable", "--" + options.commandName(), options.eventId()));
400 #endif
401 exitCode = 1;
403 break;
405 case CommandOptions::EDIT_NEW:
407 // Edit a new alarm, and optionally preset selected values
408 if (!initCheck())
409 exitCode = 1;
410 else
412 // Use AutoQPointer to guard against crash on application exit while
413 // the dialogue is still open. It prevents double deletion (both on
414 // deletion of parent, and on return from this function).
415 AutoQPointer<EditAlarmDlg> editDlg = EditAlarmDlg::create(false, options.editType());
416 if (options.alarmTime().isValid())
417 editDlg->setTime(options.alarmTime());
418 if (options.recurrence())
419 editDlg->setRecurrence(*options.recurrence(), options.subRepeatInterval(), options.subRepeatCount());
420 else if (options.flags() & KAEvent::REPEAT_AT_LOGIN)
421 editDlg->setRepeatAtLogin();
422 editDlg->setAction(options.editAction(), AlarmText(options.text()));
423 if (options.lateCancel())
424 editDlg->setLateCancel(options.lateCancel());
425 if (options.flags() & KAEvent::COPY_KORGANIZER)
426 editDlg->setShowInKOrganizer(true);
427 switch (options.editType())
429 case EditAlarmDlg::DISPLAY:
431 // EditAlarmDlg::create() always returns EditDisplayAlarmDlg for type = DISPLAY
432 EditDisplayAlarmDlg* dlg = qobject_cast<EditDisplayAlarmDlg*>(editDlg);
433 if (options.fgColour().isValid())
434 dlg->setFgColour(options.fgColour());
435 if (options.bgColour().isValid())
436 dlg->setBgColour(options.bgColour());
437 if (!options.audioFile().isEmpty()
438 || options.flags() & (KAEvent::BEEP | KAEvent::SPEAK))
440 KAEvent::Flags flags = options.flags();
441 Preferences::SoundType type = (flags & KAEvent::BEEP) ? Preferences::Sound_Beep
442 : (flags & KAEvent::SPEAK) ? Preferences::Sound_Speak
443 : Preferences::Sound_File;
444 dlg->setAudio(type, options.audioFile(), options.audioVolume(), (flags & KAEvent::REPEAT_SOUND ? 0 : -1));
446 if (options.reminderMinutes())
447 dlg->setReminder(options.reminderMinutes(), (options.flags() & KAEvent::REMINDER_ONCE));
448 if (options.flags() & KAEvent::CONFIRM_ACK)
449 dlg->setConfirmAck(true);
450 if (options.flags() & KAEvent::AUTO_CLOSE)
451 dlg->setAutoClose(true);
452 break;
454 case EditAlarmDlg::COMMAND:
455 break;
456 case EditAlarmDlg::EMAIL:
458 // EditAlarmDlg::create() always returns EditEmailAlarmDlg for type = EMAIL
459 EditEmailAlarmDlg* dlg = qobject_cast<EditEmailAlarmDlg*>(editDlg);
460 if (options.fromID()
461 || !options.addressees().isEmpty()
462 || !options.subject().isEmpty()
463 || !options.attachments().isEmpty())
464 dlg->setEmailFields(options.fromID(), options.addressees(), options.subject(), options.attachments());
465 if (options.flags() & KAEvent::EMAIL_BCC)
466 dlg->setBcc(true);
467 break;
469 case EditAlarmDlg::AUDIO:
471 // EditAlarmDlg::create() always returns EditAudioAlarmDlg for type = AUDIO
472 EditAudioAlarmDlg* dlg = qobject_cast<EditAudioAlarmDlg*>(editDlg);
473 if (!options.audioFile().isEmpty() || options.audioVolume() >= 0)
474 dlg->setAudio(options.audioFile(), options.audioVolume());
475 break;
477 case EditAlarmDlg::NO_TYPE:
478 break;
480 KAlarm::execNewAlarmDlg(editDlg);
482 break;
484 case CommandOptions::EDIT_NEW_PRESET:
485 // Edit a new alarm, preset with a template
486 if (!initCheck())
487 exitCode = 1;
488 else
489 KAlarm::editNewAlarm(options.templateName());
490 break;
492 case CommandOptions::NEW:
493 // Display a message or file, execute a command, or send an email
494 if (!initCheck()
495 || !scheduleEvent(options.editAction(), options.text(), options.alarmTime(),
496 options.lateCancel(), options.flags(), options.bgColour(),
497 options.fgColour(), QFont(), options.audioFile(), options.audioVolume(),
498 options.reminderMinutes(), (options.recurrence() ? *options.recurrence() : KARecurrence()),
499 options.subRepeatInterval(), options.subRepeatCount(),
500 options.fromID(), options.addressees(),
501 options.subject(), options.attachments()))
502 exitCode = 1;
503 break;
505 case CommandOptions::TRAY:
506 // Display only the system tray icon
507 if (Preferences::showInSystemTray() && KSystemTrayIcon::isSystemTrayAvailable())
509 if (!initCheck() // open the calendar, start processing execution queue
510 || !displayTrayIcon(true))
511 exitCode = 1;
512 break;
514 // fall through to NONE
515 case CommandOptions::NONE:
516 // No arguments - run interactively & display the main window
517 #ifndef NDEBUG
518 if (options.simulationTime().isValid() && !firstInstance)
519 break; // simulating time: don't open main window if already running
520 #endif
521 if (!initCheck())
522 exitCode = 1;
523 else
525 MainWindow* win = MainWindow::create();
526 if (command == CommandOptions::TRAY)
527 win->setWindowState(win->windowState() | Qt::WindowMinimized);
528 win->show();
530 break;
532 case CommandOptions::CMD_ERROR:
533 mReadOnly = true; // don't need write access to calendars
534 exitCode = 1;
535 break;
539 // If this is the first time through, redisplay any alarm message windows
540 // from last time.
541 if (firstInstance && !dontRedisplay && !exitCode)
543 /* First time through, so redisplay alarm message windows from last time.
544 * But it is possible for session restoration in some circumstances to
545 * not create any windows, in which case the alarm calendars will have
546 * been deleted - if so, don't try to do anything. (This has been known
547 * to happen under the Xfce desktop.)
549 if (AlarmCalendar::resources())
550 MessageWin::redisplayAlarms();
553 --mActiveCount;
554 firstInstance = false;
556 // Quit the application if this was the last/only running "instance" of the program.
557 // Executing 'return' doesn't work very well since the program continues to
558 // run if no windows were created.
559 quitIf(exitCode);
561 // Check whether the KDE time zone daemon is running (but don't hold up initialisation)
562 QTimer::singleShot(0, this, SLOT(checkKtimezoned()));
564 return exitCode;
567 void KAlarmApp::checkKtimezoned()
569 // Check that the KDE time zone daemon is running
570 static bool done = false;
571 if (done)
572 return;
573 done = true;
574 #if KDE_IS_VERSION(4,5,70)
575 if (!KSystemTimeZones::isTimeZoneDaemonAvailable())
577 kDebug() << "ktimezoned not running: using UTC only";
578 KAMessageBox::information(MainWindow::mainMainWindow(),
579 i18nc("@info", "Time zones are not accessible:<nl/>KAlarm will use the UTC time zone.<nl/><nl/>(The KDE time zone service is not available:<nl/>check that <application>ktimezoned</application> is installed.)"),
580 QString(), QLatin1String("tzunavailable"));
582 #endif
585 /******************************************************************************
586 * Quit the program, optionally only if there are no more "instances" running.
587 * Reply = true if program exited.
589 bool KAlarmApp::quitIf(int exitCode, bool force)
591 if (force)
593 // Quit regardless, except for message windows
594 mQuitting = true;
595 MainWindow::closeAll();
596 mQuitting = false;
597 displayTrayIcon(false);
598 if (MessageWin::instanceCount(true)) // ignore always-hidden windows (e.g. audio alarms)
599 return false;
601 else if (mQuitting)
602 return false; // MainWindow::closeAll() causes quitIf() to be called again
603 else
605 // Quit only if there are no more "instances" running
606 mPendingQuit = false;
607 if (mActiveCount > 0 || MessageWin::instanceCount(true)) // ignore always-hidden windows (e.g. audio alarms)
608 return false;
609 int mwcount = MainWindow::count();
610 MainWindow* mw = mwcount ? MainWindow::firstWindow() : 0;
611 if (mwcount > 1 || (mwcount && (!mw->isHidden() || !mw->isTrayParent())))
612 return false;
613 // There are no windows left except perhaps a main window which is a hidden
614 // tray icon parent, or an always-hidden message window.
615 if (mTrayWindow)
617 // There is a system tray icon.
618 // Don't exit unless the system tray doesn't seem to exist.
619 if (checkSystemTray())
620 return false;
622 if (!mActionQueue.isEmpty() || !mCommandProcesses.isEmpty())
624 // Don't quit yet if there are outstanding actions on the execution queue
625 mPendingQuit = true;
626 mPendingQuitCode = exitCode;
627 return false;
631 // This was the last/only running "instance" of the program, so exit completely.
632 kDebug() << exitCode << ": quitting";
633 MessageWin::stopAudio(true);
634 if (mCancelRtcWake)
636 KAlarm::setRtcWakeTime(0, 0);
637 KAlarm::deleteRtcWakeConfig();
639 delete mAlarmTimer; // prevent checking for alarms after deleting calendars
640 mAlarmTimer = 0;
641 mInitialised = false; // prevent processQueue() from running
642 AlarmCalendar::terminateCalendars();
643 exit(exitCode);
644 return true; // sometimes we actually get to here, despite calling exit()
647 /******************************************************************************
648 * Called when the Quit menu item is selected.
649 * Closes the system tray window and all main windows, but does not exit the
650 * program if other windows are still open.
652 void KAlarmApp::doQuit(QWidget* parent)
654 kDebug();
655 if (KAMessageBox::warningContinueCancel(parent, KMessageBox::Cancel,
656 i18nc("@info", "Quitting will disable alarms (once any alarm message windows are closed)."),
657 QString(), KStandardGuiItem::quit(), Preferences::QUIT_WARN
658 ) != KMessageBox::Yes)
659 return;
660 if (!KAlarm::checkRtcWakeConfig(true).isEmpty())
662 // A wake-on-suspend alarm is set
663 if (KAMessageBox::warningContinueCancel(parent, KMessageBox::Cancel,
664 i18nc("@info", "Quitting will cancel the scheduled Wake from Suspend."),
665 QString(), KStandardGuiItem::quit()
666 ) != KMessageBox::Yes)
667 return;
668 mCancelRtcWake = true;
670 if (!Preferences::autoStart())
672 int option = KMessageBox::No;
673 if (!Preferences::autoStartChangedByUser())
675 option = KAMessageBox::questionYesNoCancel(parent,
676 i18nc("@info", "Do you want to start KAlarm at login?<nl/>"
677 "(Note that alarms will be disabled if KAlarm is not started.)"),
678 QString(), KStandardGuiItem::yes(), KStandardGuiItem::no(),
679 KStandardGuiItem::cancel(), Preferences::ASK_AUTO_START);
681 switch (option)
683 case KMessageBox::Yes:
684 Preferences::setAutoStart(true);
685 Preferences::setNoAutoStart(false);
686 break;
687 case KMessageBox::No:
688 Preferences::setNoAutoStart(true);
689 break;
690 case KMessageBox::Cancel:
691 default:
692 return;
694 Preferences::self()->writeConfig();
696 quitIf(0, true);
699 /******************************************************************************
700 * Called when the session manager is about to close down the application.
702 void KAlarmApp::commitData(QSessionManager& sm)
704 mSessionClosingDown = true;
705 KUniqueApplication::commitData(sm);
706 mSessionClosingDown = false; // reset in case shutdown is cancelled
709 /******************************************************************************
710 * Display an error message for a fatal error. Prevent further actions since
711 * the program state is unsafe.
713 void KAlarmApp::displayFatalError(const QString& message)
715 if (!mFatalError)
717 mFatalError = 1;
718 mFatalMessage = message;
719 if (theInstance)
720 QTimer::singleShot(0, theInstance, SLOT(quitFatal()));
724 /******************************************************************************
725 * Quit the program, once the fatal error message has been acknowledged.
727 void KAlarmApp::quitFatal()
729 switch (mFatalError)
731 case 0:
732 case 2:
733 return;
734 case 1:
735 mFatalError = 2;
736 KMessageBox::error(0, mFatalMessage); // this is an application modal window
737 mFatalError = 3;
738 // fall through to '3'
739 case 3:
740 if (theInstance)
741 theInstance->quitIf(1, true);
742 break;
744 QTimer::singleShot(1000, this, SLOT(quitFatal()));
747 /******************************************************************************
748 * Called by the alarm timer when the next alarm is due.
749 * Also called when the execution queue has finished processing to check for the
750 * next alarm.
752 void KAlarmApp::checkNextDueAlarm()
754 if (!mAlarmsEnabled)
755 return;
756 // Find the first alarm due
757 KAEvent* nextEvent = AlarmCalendar::resources()->earliestAlarm();
758 if (!nextEvent)
759 return; // there are no alarms pending
760 KDateTime nextDt = nextEvent->nextTrigger(KAEvent::ALL_TRIGGER).effectiveKDateTime();
761 KDateTime now = KDateTime::currentDateTime(Preferences::timeZone());
762 qint64 interval = now.secsTo_long(nextDt);
763 kDebug() << "now:" << qPrintable(now.toString("%Y-%m-%d %H:%M %:Z")) << ", next:" << qPrintable(nextDt.toString("%Y-%m-%d %H:%M %:Z")) << ", due:" << interval;
764 if (interval <= 0)
766 // Queue the alarm
767 queueAlarmId(*nextEvent);
768 kDebug() << nextEvent->id() << ": due now";
769 QTimer::singleShot(0, this, SLOT(processQueue()));
771 else
773 // No alarm is due yet, so set timer to wake us when it's due.
774 // Check for integer overflow before setting timer.
775 #ifndef HIBERNATION_SIGNAL
776 /* TODO: REPLACE THIS CODE WHEN A SYSTEM NOTIFICATION SIGNAL BECOMES
777 * AVAILABLE FOR WAKEUP FROM HIBERNATION.
778 * Re-evaluate the next alarm time every minute, in case the
779 * system clock jumps. The most common case when the clock jumps
780 * is when a laptop wakes from hibernation. If timers were left to
781 * run, they would trigger late by the length of time the system
782 * was asleep.
784 if (interval > 60) // 1 minute
785 interval = 60;
786 #endif
787 interval *= 1000;
788 if (interval > INT_MAX)
789 interval = INT_MAX;
790 kDebug() << nextEvent->id() << "wait" << interval/1000 << "seconds";
791 mAlarmTimer->start(static_cast<int>(interval));
795 /******************************************************************************
796 * Called by the alarm timer when the next alarm is due.
797 * Also called when the execution queue has finished processing to check for the
798 * next alarm.
800 void KAlarmApp::queueAlarmId(const KAEvent& event)
802 #ifdef USE_AKONADI
803 EventId id(event);
804 #else
805 const QString id(event.id());
806 #endif
807 for (int i = 0, end = mActionQueue.count(); i < end; ++i)
809 if (mActionQueue[i].function == EVENT_HANDLE && mActionQueue[i].eventId == id)
810 return; // the alarm is already queued
812 mActionQueue.enqueue(ActionQEntry(EVENT_HANDLE, id));
815 /******************************************************************************
816 * Start processing the execution queue.
818 void KAlarmApp::startProcessQueue()
820 if (!mInitialised)
822 kDebug();
823 mInitialised = true;
824 QTimer::singleShot(0, this, SLOT(processQueue())); // process anything already queued
828 /******************************************************************************
829 * The main processing loop for KAlarm.
830 * All KAlarm operations involving opening or updating calendar files are called
831 * from this loop to ensure that only one operation is active at any one time.
832 * This precaution is necessary because KAlarm's activities are mostly
833 * asynchronous, being in response to D-Bus calls from other programs or timer
834 * events, any of which can be received in the middle of performing another
835 * operation. If a calendar file is opened or updated while another calendar
836 * operation is in progress, the program has been observed to hang, or the first
837 * calendar call has failed with data loss - clearly unacceptable!!
839 void KAlarmApp::processQueue()
841 if (mInitialised && !mProcessingQueue)
843 kDebug();
844 mProcessingQueue = true;
846 // Refresh alarms if that's been queued
847 KAlarm::refreshAlarmsIfQueued();
849 if (!mLoginAlarmsDone)
851 // Queue all at-login alarms once only, at program start-up.
852 // First, cancel any scheduled reminders or deferrals for them,
853 // since these will be superseded by the new at-login trigger.
854 KAEvent::List events = AlarmCalendar::resources()->atLoginAlarms();
855 for (int i = 0, end = events.count(); i < end; ++i)
857 KAEvent event = *events[i];
858 if (!cancelReminderAndDeferral(event))
860 if (mAlarmsEnabled)
861 queueAlarmId(event);
864 mLoginAlarmsDone = true;
867 // Process queued events
868 while (!mActionQueue.isEmpty())
870 ActionQEntry& entry = mActionQueue.head();
871 if (entry.eventId.isEmpty())
873 // It's a new alarm
874 switch (entry.function)
876 case EVENT_TRIGGER:
877 execAlarm(entry.event, entry.event.firstAlarm(), false);
878 break;
879 case EVENT_HANDLE:
880 KAlarm::addEvent(entry.event, 0, 0, KAlarm::ALLOW_KORG_UPDATE | KAlarm::NO_RESOURCE_PROMPT);
881 break;
882 case EVENT_CANCEL:
883 break;
886 else
887 handleEvent(entry.eventId, entry.function);
888 mActionQueue.dequeue();
891 // Purge the default archived alarms resource if it's time to do so
892 if (mPurgeDaysQueued >= 0)
894 KAlarm::purgeArchive(mPurgeDaysQueued);
895 mPurgeDaysQueued = -1;
898 // Now that the queue has been processed, quit if a quit was queued
899 if (mPendingQuit)
901 if (quitIf(mPendingQuitCode))
902 return; // quitIf() can sometimes return, despite calling exit()
905 mProcessingQueue = false;
907 // Schedule the application to be woken when the next alarm is due
908 checkNextDueAlarm();
912 #ifdef USE_AKONADI
913 /******************************************************************************
914 * Called when a repeat-at-login alarm has been added externally.
915 * Queues the alarm for triggering.
916 * First, cancel any scheduled reminder or deferral for it, since these will be
917 * superseded by the new at-login trigger.
919 void KAlarmApp::atLoginEventAdded(const KAEvent& event)
921 KAEvent ev = event;
922 if (!cancelReminderAndDeferral(ev))
924 if (mAlarmsEnabled)
926 mActionQueue.enqueue(ActionQEntry(EVENT_HANDLE, EventId(ev)));
927 if (mInitialised)
928 QTimer::singleShot(0, this, SLOT(processQueue()));
932 #endif
934 /******************************************************************************
935 * Called when the system tray main window is closed.
937 void KAlarmApp::removeWindow(TrayWindow*)
939 mTrayWindow = 0;
942 /******************************************************************************
943 * Display or close the system tray icon.
945 bool KAlarmApp::displayTrayIcon(bool show, MainWindow* parent)
947 kDebug();
948 static bool creating = false;
949 if (show)
951 if (!mTrayWindow && !creating)
953 if (!KSystemTrayIcon::isSystemTrayAvailable())
954 return false;
955 if (!MainWindow::count())
957 // We have to have at least one main window to act
958 // as parent to the system tray icon (even if the
959 // window is hidden).
960 creating = true; // prevent main window constructor from creating an additional tray icon
961 parent = MainWindow::create();
962 creating = false;
964 mTrayWindow = new TrayWindow(parent ? parent : MainWindow::firstWindow());
965 connect(mTrayWindow, SIGNAL(deleted()), SIGNAL(trayIconToggled()));
966 emit trayIconToggled();
968 if (!checkSystemTray())
969 quitIf(0); // exit the application if there are no open windows
972 else
974 delete mTrayWindow;
975 mTrayWindow = 0;
977 return true;
980 /******************************************************************************
981 * Check whether the system tray icon has been housed in the system tray.
983 bool KAlarmApp::checkSystemTray()
985 if (!mTrayWindow)
986 return true;
987 if (KSystemTrayIcon::isSystemTrayAvailable() == mNoSystemTray)
989 kDebug() << "changed ->" << mNoSystemTray;
990 mNoSystemTray = !mNoSystemTray;
992 // Store the new setting in the config file, so that if KAlarm exits it will
993 // restart with the correct default.
994 KConfigGroup config(KGlobal::config(), "General");
995 config.writeEntry("NoSystemTray", mNoSystemTray);
996 config.sync();
998 // Update other settings
999 slotShowInSystemTrayChanged();
1001 return !mNoSystemTray;
1004 /******************************************************************************
1005 * Return the main window associated with the system tray icon.
1007 MainWindow* KAlarmApp::trayMainWindow() const
1009 return mTrayWindow ? mTrayWindow->assocMainWindow() : 0;
1012 /******************************************************************************
1013 * Called when the show-in-system-tray preference setting has changed, to show
1014 * or hide the system tray icon.
1016 void KAlarmApp::slotShowInSystemTrayChanged()
1018 bool newShowInSysTray = wantShowInSystemTray();
1019 if (newShowInSysTray != mOldShowInSystemTray)
1021 // The system tray run mode has changed
1022 ++mActiveCount; // prevent the application from quitting
1023 MainWindow* win = mTrayWindow ? mTrayWindow->assocMainWindow() : 0;
1024 delete mTrayWindow; // remove the system tray icon if it is currently shown
1025 mTrayWindow = 0;
1026 mOldShowInSystemTray = newShowInSysTray;
1027 if (newShowInSysTray)
1029 // Show the system tray icon
1030 displayTrayIcon(true);
1032 else
1034 // Stop showing the system tray icon
1035 if (win && win->isHidden())
1037 if (MainWindow::count() > 1)
1038 delete win;
1039 else
1041 win->setWindowState(win->windowState() | Qt::WindowMinimized);
1042 win->show();
1046 --mActiveCount;
1050 /******************************************************************************
1051 * Called when the start-of-day time preference setting has changed.
1052 * Change alarm times for date-only alarms.
1054 void KAlarmApp::changeStartOfDay()
1056 DateTime::setStartOfDay(Preferences::startOfDay());
1057 KAEvent::setStartOfDay(Preferences::startOfDay());
1058 AlarmCalendar::resources()->adjustStartOfDay();
1061 /******************************************************************************
1062 * Called when the default alarm message font preference setting has changed.
1063 * Notify KAEvent.
1065 void KAlarmApp::slotMessageFontChanged(const QFont& font)
1067 KAEvent::setDefaultFont(font);
1070 /******************************************************************************
1071 * Called when the working time preference settings have changed.
1072 * Notify KAEvent.
1074 void KAlarmApp::slotWorkTimeChanged(const QTime& start, const QTime& end, const QBitArray& days)
1076 KAEvent::setWorkTime(days, start, end);
1079 /******************************************************************************
1080 * Called when the holiday region preference setting has changed.
1081 * Notify KAEvent.
1083 void KAlarmApp::slotHolidaysChanged(const KHolidays::HolidayRegion& holidays)
1085 KAEvent::setHolidays(holidays);
1088 /******************************************************************************
1089 * Called when the date for February 29th recurrences has changed in the
1090 * preferences settings.
1092 void KAlarmApp::slotFeb29TypeChanged(Preferences::Feb29Type type)
1094 KARecurrence::Feb29Type rtype;
1095 switch (type)
1097 default:
1098 case Preferences::Feb29_None: rtype = KARecurrence::Feb29_None; break;
1099 case Preferences::Feb29_Feb28: rtype = KARecurrence::Feb29_Feb28; break;
1100 case Preferences::Feb29_Mar1: rtype = KARecurrence::Feb29_Mar1; break;
1102 KARecurrence::setDefaultFeb29Type(rtype);
1105 /******************************************************************************
1106 * Return whether the program is configured to be running in the system tray.
1108 bool KAlarmApp::wantShowInSystemTray() const
1110 return Preferences::showInSystemTray() && KSystemTrayIcon::isSystemTrayAvailable();
1113 /******************************************************************************
1114 * Called when all calendars have been fetched at startup.
1115 * Check whether there are any writable active calendars, and if not, warn the
1116 * user.
1118 void KAlarmApp::checkWritableCalendar()
1120 kDebug();
1121 if (mReadOnly)
1122 return; // don't need write access to calendars
1123 #ifdef USE_AKONADI
1124 if (!AkonadiModel::instance()->isCollectionTreeFetched())
1125 return;
1126 #endif
1127 static bool done = false;
1128 if (done)
1129 return;
1130 done = true;
1131 kDebug()<<"checking";
1132 // Find whether there are any writable active alarm calendars
1133 #ifdef USE_AKONADI
1134 bool active = !CollectionControlModel::enabledCollections(CalEvent::ACTIVE, true).isEmpty();
1135 #else
1136 bool active = AlarmResources::instance()->activeCount(CalEvent::ACTIVE, true);
1137 #endif
1138 if (!active)
1140 kWarning() << "No writable active calendar";
1141 KAMessageBox::information(MainWindow::mainMainWindow(),
1142 i18nc("@info", "Alarms cannot be created or updated, because no writable active alarm calendar is enabled.<nl/><nl/>"
1143 "To fix this, use <interface>View | Show Calendars</interface> to check or change calendar statuses."),
1144 QString(), QLatin1String("noWritableCal"));
1148 #ifdef USE_AKONADI
1149 /******************************************************************************
1150 * Called when a new collection has been added, or when a collection has been
1151 * set as the standard collection for its type.
1152 * If it is the default archived calendar, purge its old alarms if necessary.
1154 void KAlarmApp::purgeNewArchivedDefault(const Akonadi::Collection& collection)
1156 Akonadi::Collection col(collection);
1157 if (CollectionControlModel::isStandard(col, CalEvent::ARCHIVED))
1159 // Allow time (1 minute) for AkonadiModel to be populated with the
1160 // collection's events before purging it.
1161 kDebug() << collection.id() << ": standard archived...";
1162 QTimer::singleShot(60000, this, SLOT(purgeAfterDelay()));
1166 /******************************************************************************
1167 * Called after a delay, after the default archived calendar has been added to
1168 * AkonadiModel.
1169 * Purge old alarms from it if necessary.
1171 void KAlarmApp::purgeAfterDelay()
1173 if (mArchivedPurgeDays >= 0)
1174 purge(mArchivedPurgeDays);
1175 else
1176 setArchivePurgeDays();
1178 #endif
1180 /******************************************************************************
1181 * Called when the length of time to keep archived alarms changes in KAlarm's
1182 * preferences.
1183 * Set the number of days to keep archived alarms.
1184 * Alarms which are older are purged immediately, and at the start of each day.
1186 void KAlarmApp::setArchivePurgeDays()
1188 int newDays = Preferences::archivedKeepDays();
1189 if (newDays != mArchivedPurgeDays)
1191 int oldDays = mArchivedPurgeDays;
1192 mArchivedPurgeDays = newDays;
1193 if (mArchivedPurgeDays <= 0)
1194 StartOfDayTimer::disconnect(this);
1195 if (mArchivedPurgeDays < 0)
1196 return; // keep indefinitely, so don't purge
1197 if (oldDays < 0 || mArchivedPurgeDays < oldDays)
1199 // Alarms are now being kept for less long, so purge them
1200 purge(mArchivedPurgeDays);
1201 if (!mArchivedPurgeDays)
1202 return; // don't archive any alarms
1204 // Start the purge timer to expire at the start of the next day
1205 // (using the user-defined start-of-day time).
1206 StartOfDayTimer::connect(this, SLOT(slotPurge()));
1210 /******************************************************************************
1211 * Purge all archived events from the calendar whose end time is longer ago than
1212 * 'daysToKeep'. All events are deleted if 'daysToKeep' is zero.
1214 void KAlarmApp::purge(int daysToKeep)
1216 if (mPurgeDaysQueued < 0 || daysToKeep < mPurgeDaysQueued)
1217 mPurgeDaysQueued = daysToKeep;
1219 // Do the purge once any other current operations are completed
1220 processQueue();
1224 /******************************************************************************
1225 * Output a list of pending alarms, with their next scheduled occurrence.
1227 QStringList KAlarmApp::scheduledAlarmList()
1229 #ifdef USE_AKONADI
1230 QVector<KAEvent> events = KAlarm::getSortedActiveEvents(this);
1231 #else
1232 KAEvent::List events = KAlarm::getSortedActiveEvents();
1233 #endif
1234 QStringList alarms;
1235 for (int i = 0, count = events.count(); i < count; ++i)
1237 #ifdef USE_AKONADI
1238 KAEvent* event = &events[i];
1239 #else
1240 KAEvent* event = events[i];
1241 #endif
1242 KDateTime dateTime = event->nextTrigger(KAEvent::DISPLAY_TRIGGER).effectiveKDateTime().toLocalZone();
1243 #ifdef USE_AKONADI
1244 Akonadi::Collection c(event->collectionId());
1245 AkonadiModel::instance()->refresh(c);
1246 QString text(c.resource() + ":");
1247 #else
1248 QString text;
1249 #endif
1250 text += event->id() + ' '
1251 + dateTime.toString("%Y%m%dT%H%M ")
1252 + AlarmText::summary(*event, 1);
1253 alarms << text;
1255 return alarms;
1258 /******************************************************************************
1259 * Enable or disable alarm monitoring.
1261 void KAlarmApp::setAlarmsEnabled(bool enabled)
1263 if (enabled != mAlarmsEnabled)
1265 mAlarmsEnabled = enabled;
1266 emit alarmEnabledToggled(enabled);
1267 if (!enabled)
1268 KAlarm::cancelRtcWake(0);
1269 else if (!mProcessingQueue)
1270 checkNextDueAlarm();
1274 /******************************************************************************
1275 * Spread or collect alarm message and error message windows.
1277 void KAlarmApp::spreadWindows(bool spread)
1279 spread = MessageWin::spread(spread);
1280 emit spreadWindowsToggled(spread);
1283 /******************************************************************************
1284 * Called when the spread status of message windows changes.
1285 * Set the 'spread windows' action state.
1287 void KAlarmApp::setSpreadWindowsState(bool spread)
1289 emit spreadWindowsToggled(spread);
1292 /******************************************************************************
1293 * Called to schedule a new alarm, either in response to a DCOP notification or
1294 * to command line options.
1295 * Reply = true unless there was a parameter error or an error opening calendar file.
1297 bool KAlarmApp::scheduleEvent(KAEvent::SubAction action, const QString& text, const KDateTime& dateTime,
1298 int lateCancel, KAEvent::Flags flags, const QColor& bg, const QColor& fg,
1299 const QFont& font, const QString& audioFile, float audioVolume, int reminderMinutes,
1300 const KARecurrence& recurrence, int repeatInterval, int repeatCount,
1301 #ifdef USE_AKONADI
1302 uint mailFromID, const KCalCore::Person::List& mailAddresses,
1303 #else
1304 uint mailFromID, const QList<KCal::Person>& mailAddresses,
1305 #endif
1306 const QString& mailSubject, const QStringList& mailAttachments)
1308 kDebug() << text;
1309 if (!dateTime.isValid())
1310 return false;
1311 KDateTime now = KDateTime::currentUtcDateTime();
1312 if (lateCancel && dateTime < now.addSecs(-maxLateness(lateCancel)))
1313 return true; // alarm time was already archived too long ago
1314 KDateTime alarmTime = dateTime;
1315 // Round down to the nearest minute to avoid scheduling being messed up
1316 if (!dateTime.isDateOnly())
1317 alarmTime.setTime(QTime(alarmTime.time().hour(), alarmTime.time().minute(), 0));
1319 KAEvent event(alarmTime, text, bg, fg, font, action, lateCancel, flags, true);
1320 if (reminderMinutes)
1322 bool onceOnly = flags & KAEvent::REMINDER_ONCE;
1323 event.setReminder(reminderMinutes, onceOnly);
1325 if (!audioFile.isEmpty())
1326 event.setAudioFile(audioFile, audioVolume, -1, 0, (flags & KAEvent::REPEAT_SOUND) ? 0 : -1);
1327 if (!mailAddresses.isEmpty())
1328 event.setEmail(mailFromID, mailAddresses, mailSubject, mailAttachments);
1329 event.setRecurrence(recurrence);
1330 event.setFirstRecurrence();
1331 event.setRepetition(Repetition(repeatInterval, repeatCount - 1));
1332 event.endChanges();
1333 if (alarmTime <= now)
1335 // Alarm is due for display already.
1336 // First execute it once without adding it to the calendar file.
1337 if (!mInitialised)
1338 mActionQueue.enqueue(ActionQEntry(event, EVENT_TRIGGER));
1339 else
1340 execAlarm(event, event.firstAlarm(), false);
1341 // If it's a recurring alarm, reschedule it for its next occurrence
1342 if (!event.recurs()
1343 || event.setNextOccurrence(now) == KAEvent::NO_OCCURRENCE)
1344 return true;
1345 // It has recurrences in the future
1348 // Queue the alarm for insertion into the calendar file
1349 mActionQueue.enqueue(ActionQEntry(event));
1350 if (mInitialised)
1351 QTimer::singleShot(0, this, SLOT(processQueue()));
1352 return true;
1355 /******************************************************************************
1356 * Called in response to a D-Bus request to trigger or cancel an event.
1357 * Optionally display the event. Delete the event from the calendar file and
1358 * from every main window instance.
1360 #ifdef USE_AKONADI
1361 bool KAlarmApp::dbusHandleEvent(const EventId& eventID, EventFunc function)
1362 #else
1363 bool KAlarmApp::dbusHandleEvent(const QString& eventID, EventFunc function)
1364 #endif
1366 kDebug() << eventID;
1367 mActionQueue.append(ActionQEntry(function, eventID));
1368 if (mInitialised)
1369 QTimer::singleShot(0, this, SLOT(processQueue()));
1370 return true;
1373 /******************************************************************************
1374 * Called in response to a D-Bus request to list all pending alarms.
1376 QString KAlarmApp::dbusList()
1378 kDebug();
1379 return scheduledAlarmList().join("\n") + '\n';
1382 /******************************************************************************
1383 * Either:
1384 * a) Display the event and then delete it if it has no outstanding repetitions.
1385 * b) Delete the event.
1386 * c) Reschedule the event for its next repetition. If none remain, delete it.
1387 * If the event is deleted, it is removed from the calendar file and from every
1388 * main window instance.
1389 * Reply = false if event ID not found, or if more than one event with the same
1390 * ID is found.
1392 #ifdef USE_AKONADI
1393 bool KAlarmApp::handleEvent(const EventId& id, EventFunc function, bool checkDuplicates)
1394 #else
1395 bool KAlarmApp::handleEvent(const QString& eventID, EventFunc function)
1396 #endif
1398 // Delete any expired wake-on-suspend config data
1399 KAlarm::checkRtcWakeConfig();
1401 #ifdef USE_AKONADI
1402 const QString eventID(id.eventId());
1403 KAEvent* event = AlarmCalendar::resources()->event(id, checkDuplicates);
1404 if (!event)
1406 if (id.collectionId() != -1)
1407 kWarning() << "Event ID not found, or duplicated:" << eventID;
1408 else
1409 kWarning() << "Event ID not found:" << eventID;
1410 return false;
1412 #else
1413 KAEvent* event = AlarmCalendar::resources()->event(eventID);
1414 if (!event)
1416 kWarning() << "Event ID not found:" << eventID;
1417 return false;
1419 #endif
1420 switch (function)
1422 case EVENT_CANCEL:
1423 kDebug() << eventID << ", CANCEL";
1424 KAlarm::deleteEvent(*event, true);
1425 break;
1427 case EVENT_TRIGGER: // handle it if it's due, else execute it regardless
1428 case EVENT_HANDLE: // handle it if it's due
1430 KDateTime now = KDateTime::currentUtcDateTime();
1431 kDebug() << eventID << "," << (function==EVENT_TRIGGER?"TRIGGER:":"HANDLE:") << qPrintable(now.dateTime().toString("yyyy-MM-dd hh:mm")) << "UTC";
1432 bool updateCalAndDisplay = false;
1433 bool alarmToExecuteValid = false;
1434 KAAlarm alarmToExecute;
1435 bool restart = false;
1436 // Check all the alarms in turn.
1437 // Note that the main alarm is fetched before any other alarms.
1438 for (KAAlarm alarm = event->firstAlarm();
1439 alarm.isValid();
1440 alarm = (restart ? event->firstAlarm() : event->nextAlarm(alarm)), restart = false)
1442 // Check if the alarm is due yet.
1443 KDateTime nextDT = alarm.dateTime(true).effectiveKDateTime();
1444 int secs = nextDT.secsTo(now);
1445 if (secs < 0)
1447 // The alarm appears to be in the future.
1448 // Check if it's an invalid local clock time during a daylight
1449 // saving time shift, which has actually passed.
1450 if (alarm.dateTime().timeSpec() != KDateTime::ClockTime
1451 || nextDT > now.toTimeSpec(KDateTime::ClockTime))
1453 // This alarm is definitely not due yet
1454 kDebug() << "Alarm" << alarm.type() << "at" << nextDT.dateTime() << ": not due";
1455 continue;
1458 bool reschedule = false;
1459 bool rescheduleWork = false;
1460 if ((event->workTimeOnly() || event->holidaysExcluded()) && !alarm.deferred())
1462 // The alarm is restricted to working hours and/or non-holidays
1463 // (apart from deferrals). This needs to be re-evaluated every
1464 // time it triggers, since working hours could change.
1465 if (alarm.dateTime().isDateOnly())
1467 KDateTime dt(nextDT);
1468 dt.setDateOnly(true);
1469 reschedule = !event->isWorkingTime(dt);
1471 else
1472 reschedule = !event->isWorkingTime(nextDT);
1473 rescheduleWork = reschedule;
1474 if (reschedule)
1475 kDebug() << "Alarm" << alarm.type() << "at" << nextDT.dateTime() << ": not during working hours";
1477 if (!reschedule && alarm.repeatAtLogin())
1479 // Alarm is to be displayed at every login.
1480 kDebug() << "REPEAT_AT_LOGIN";
1481 // Check if the main alarm is already being displayed.
1482 // (We don't want to display both at the same time.)
1483 if (alarmToExecute.isValid())
1484 continue;
1486 // Set the time to display if it's a display alarm
1487 alarm.setTime(now);
1489 if (!reschedule && event->lateCancel())
1491 // Alarm is due, and it is to be cancelled if too late.
1492 kDebug() << "LATE_CANCEL";
1493 bool cancel = false;
1494 if (alarm.dateTime().isDateOnly())
1496 // The alarm has no time, so cancel it if its date is too far past
1497 int maxlate = event->lateCancel() / 1440; // maximum lateness in days
1498 KDateTime limit(DateTime(nextDT.addDays(maxlate + 1)).effectiveKDateTime());
1499 if (now >= limit)
1501 // It's too late to display the scheduled occurrence.
1502 // Find the last previous occurrence of the alarm.
1503 DateTime next;
1504 KAEvent::OccurType type = event->previousOccurrence(now, next, true);
1505 switch (type & ~KAEvent::OCCURRENCE_REPEAT)
1507 case KAEvent::FIRST_OR_ONLY_OCCURRENCE:
1508 case KAEvent::RECURRENCE_DATE:
1509 case KAEvent::RECURRENCE_DATE_TIME:
1510 case KAEvent::LAST_RECURRENCE:
1511 limit.setDate(next.date().addDays(maxlate + 1));
1512 if (now >= limit)
1514 if (type == KAEvent::LAST_RECURRENCE
1515 || (type == KAEvent::FIRST_OR_ONLY_OCCURRENCE && !event->recurs()))
1516 cancel = true; // last occurrence (and there are no repetitions)
1517 else
1518 reschedule = true;
1520 break;
1521 case KAEvent::NO_OCCURRENCE:
1522 default:
1523 reschedule = true;
1524 break;
1528 else
1530 // The alarm is timed. Allow it to be the permitted amount late before cancelling it.
1531 int maxlate = maxLateness(event->lateCancel());
1532 if (secs > maxlate)
1534 // It's over the maximum interval late.
1535 // Find the most recent occurrence of the alarm.
1536 DateTime next;
1537 KAEvent::OccurType type = event->previousOccurrence(now, next, true);
1538 switch (type & ~KAEvent::OCCURRENCE_REPEAT)
1540 case KAEvent::FIRST_OR_ONLY_OCCURRENCE:
1541 case KAEvent::RECURRENCE_DATE:
1542 case KAEvent::RECURRENCE_DATE_TIME:
1543 case KAEvent::LAST_RECURRENCE:
1544 if (next.effectiveKDateTime().secsTo(now) > maxlate)
1546 if (type == KAEvent::LAST_RECURRENCE
1547 || (type == KAEvent::FIRST_OR_ONLY_OCCURRENCE && !event->recurs()))
1548 cancel = true; // last occurrence (and there are no repetitions)
1549 else
1550 reschedule = true;
1552 break;
1553 case KAEvent::NO_OCCURRENCE:
1554 default:
1555 reschedule = true;
1556 break;
1561 if (cancel)
1563 // All recurrences are finished, so cancel the event
1564 event->setArchive();
1565 if (cancelAlarm(*event, alarm.type(), false))
1566 return true; // event has been deleted
1567 updateCalAndDisplay = true;
1568 continue;
1571 if (reschedule)
1573 // The latest repetition was too long ago, so schedule the next one
1574 switch (rescheduleAlarm(*event, alarm, false, (rescheduleWork ? nextDT : KDateTime())))
1576 case 1:
1577 // A working-time-only alarm has been rescheduled and the
1578 // rescheduled time is already due. Start processing the
1579 // event again.
1580 alarmToExecuteValid = false;
1581 restart = true;
1582 break;
1583 case -1:
1584 return true; // event has been deleted
1585 default:
1586 break;
1588 updateCalAndDisplay = true;
1589 continue;
1591 if (!alarmToExecuteValid)
1593 kDebug() << "Alarm" << alarm.type() << ": execute";
1594 alarmToExecute = alarm; // note the alarm to be displayed
1595 alarmToExecuteValid = true; // only trigger one alarm for the event
1597 else
1598 kDebug() << "Alarm" << alarm.type() << ": skip";
1601 // If there is an alarm to execute, do this last after rescheduling/cancelling
1602 // any others. This ensures that the updated event is only saved once to the calendar.
1603 if (alarmToExecute.isValid())
1604 execAlarm(*event, alarmToExecute, true, !alarmToExecute.repeatAtLogin());
1605 else
1607 if (function == EVENT_TRIGGER)
1609 // The alarm is to be executed regardless of whether it's due.
1610 // Only trigger one alarm from the event - we don't want multiple
1611 // identical messages, for example.
1612 KAAlarm alarm = event->firstAlarm();
1613 if (alarm.isValid())
1614 execAlarm(*event, alarm, false);
1616 if (updateCalAndDisplay)
1617 KAlarm::updateEvent(*event); // update the window lists and calendar file
1618 else if (function != EVENT_TRIGGER) { kDebug() << "No action"; }
1620 break;
1623 return true;
1626 /******************************************************************************
1627 * Called when an alarm action has completed, to perform any post-alarm actions.
1629 void KAlarmApp::alarmCompleted(const KAEvent& event)
1631 if (!event.postAction().isEmpty())
1633 // doShellCommand() will error if the user is not authorised to run
1634 // shell commands.
1635 QString command = event.postAction();
1636 kDebug() << event.id() << ":" << command;
1637 doShellCommand(command, event, 0, ProcData::POST_ACTION);
1641 /******************************************************************************
1642 * Reschedule the alarm for its next recurrence after now. If none remain,
1643 * delete it. If the alarm is deleted and it is the last alarm for its event,
1644 * the event is removed from the calendar file and from every main window
1645 * instance.
1646 * If 'nextDt' is valid, the event is rescheduled for the next non-working
1647 * time occurrence after that.
1648 * Reply = 1 if 'nextDt' is valid and the rescheduled event is already due
1649 * = -1 if the event has been deleted
1650 * = 0 otherwise.
1652 int KAlarmApp::rescheduleAlarm(KAEvent& event, const KAAlarm& alarm, bool updateCalAndDisplay, const KDateTime& nextDt)
1654 kDebug() << "Alarm type:" << alarm.type();
1655 int reply = 0;
1656 bool update = false;
1657 event.startChanges();
1658 if (alarm.repeatAtLogin())
1660 // Leave an alarm which repeats at every login until its main alarm triggers
1661 if (!event.reminderActive() && event.reminderMinutes() < 0)
1663 // Executing an at-login alarm: first schedule the reminder
1664 // which occurs AFTER the main alarm.
1665 event.activateReminderAfter(KDateTime::currentUtcDateTime());
1666 update = true;
1669 else if (alarm.isReminder() || alarm.deferred())
1671 // It's a reminder alarm or an extra deferred alarm, so delete it
1672 event.removeExpiredAlarm(alarm.type());
1673 update = true;
1675 else
1677 // Reschedule the alarm for its next occurrence.
1678 bool cancelled = false;
1679 DateTime last = event.mainDateTime(false); // note this trigger time
1680 if (last != event.mainDateTime(true))
1681 last = DateTime(); // but ignore sub-repetition triggers
1682 bool next = nextDt.isValid();
1683 KDateTime next_dt = nextDt;
1684 KDateTime now = KDateTime::currentUtcDateTime();
1687 KAEvent::OccurType type = event.setNextOccurrence(next ? next_dt : now);
1688 switch (type)
1690 case KAEvent::NO_OCCURRENCE:
1691 // All repetitions are finished, so cancel the event
1692 kDebug() << "No occurrence";
1693 if (event.reminderMinutes() < 0 && last.isValid()
1694 && alarm.type() != KAAlarm::AT_LOGIN_ALARM && !event.mainExpired())
1696 // Set the reminder which is now due after the last main alarm trigger.
1697 // Note that at-login reminders are scheduled in execAlarm().
1698 event.activateReminderAfter(last);
1699 updateCalAndDisplay = true;
1701 if (cancelAlarm(event, alarm.type(), updateCalAndDisplay))
1702 return -1;
1703 break;
1704 default:
1705 if (!(type & KAEvent::OCCURRENCE_REPEAT))
1706 break;
1707 // Next occurrence is a repeat, so fall through to recurrence handling
1708 case KAEvent::RECURRENCE_DATE:
1709 case KAEvent::RECURRENCE_DATE_TIME:
1710 case KAEvent::LAST_RECURRENCE:
1711 // The event is due by now and repetitions still remain, so rewrite the event
1712 if (updateCalAndDisplay)
1713 update = true;
1714 break;
1715 case KAEvent::FIRST_OR_ONLY_OCCURRENCE:
1716 // The first occurrence is still due?!?, so don't do anything
1717 break;
1719 if (cancelled)
1720 break;
1721 if (event.deferred())
1723 // Just in case there's also a deferred alarm, ensure it's removed
1724 event.removeExpiredAlarm(KAAlarm::DEFERRED_ALARM);
1725 update = true;
1727 if (next)
1729 // The alarm is restricted to working hours and/or non-holidays.
1730 // Check if the calculated next time is valid.
1731 next_dt = event.mainDateTime(true).effectiveKDateTime();
1732 if (event.mainDateTime(false).isDateOnly())
1734 KDateTime dt(next_dt);
1735 dt.setDateOnly(true);
1736 next = !event.isWorkingTime(dt);
1738 else
1739 next = !event.isWorkingTime(next_dt);
1741 } while (next && next_dt <= now);
1742 reply = (!cancelled && next_dt.isValid() && (next_dt <= now)) ? 1 : 0;
1744 if (event.reminderMinutes() < 0 && last.isValid()
1745 && alarm.type() != KAAlarm::AT_LOGIN_ALARM)
1747 // Set the reminder which is now due after the last main alarm trigger.
1748 // Note that at-login reminders are scheduled in execAlarm().
1749 event.activateReminderAfter(last);
1752 event.endChanges();
1753 if (update)
1754 KAlarm::updateEvent(event); // update the window lists and calendar file
1755 return reply;
1758 /******************************************************************************
1759 * Delete the alarm. If it is the last alarm for its event, the event is removed
1760 * from the calendar file and from every main window instance.
1761 * Reply = true if event has been deleted.
1763 bool KAlarmApp::cancelAlarm(KAEvent& event, KAAlarm::Type alarmType, bool updateCalAndDisplay)
1765 kDebug();
1766 if (alarmType == KAAlarm::MAIN_ALARM && !event.displaying() && event.toBeArchived())
1768 // The event is being deleted. Save it in the archived resources first.
1769 KAEvent ev(event);
1770 KAlarm::addArchivedEvent(ev);
1772 event.removeExpiredAlarm(alarmType);
1773 if (!event.alarmCount())
1775 KAlarm::deleteEvent(event, false);
1776 return true;
1778 if (updateCalAndDisplay)
1779 KAlarm::updateEvent(event); // update the window lists and calendar file
1780 return false;
1783 /******************************************************************************
1784 * Cancel any reminder or deferred alarms in an repeat-at-login event.
1785 * This should be called when the event is first loaded.
1786 * If there are no more alarms left in the event, the event is removed from the
1787 * calendar file and from every main window instance.
1788 * Reply = true if event has been deleted.
1790 bool KAlarmApp::cancelReminderAndDeferral(KAEvent& event)
1792 return cancelAlarm(event, KAAlarm::REMINDER_ALARM, false)
1793 || cancelAlarm(event, KAAlarm::DEFERRED_REMINDER_ALARM, false)
1794 || cancelAlarm(event, KAAlarm::DEFERRED_ALARM, true);
1797 /******************************************************************************
1798 * Execute an alarm by displaying its message or file, or executing its command.
1799 * Reply = ShellProcess instance if a command alarm
1800 * = MessageWin if an audio alarm
1801 * != 0 if successful
1802 * = -1 if execution has not completed
1803 * = 0 if the alarm is disabled, or if an error message was output.
1805 void* KAlarmApp::execAlarm(KAEvent& event, const KAAlarm& alarm, bool reschedule, bool allowDefer, bool noPreAction)
1807 if (!mAlarmsEnabled || !event.enabled())
1809 // The event (or all events) is disabled
1810 kDebug() << event.id() << ": disabled";
1811 if (reschedule)
1812 rescheduleAlarm(event, alarm, true);
1813 return 0;
1816 void* result = (void*)1;
1817 event.setArchive();
1819 switch (alarm.action())
1821 case KAAlarm::COMMAND:
1822 if (!event.commandDisplay())
1824 // execCommandAlarm() will error if the user is not authorised
1825 // to run shell commands.
1826 result = execCommandAlarm(event, alarm);
1827 if (reschedule)
1828 rescheduleAlarm(event, alarm, true);
1829 break;
1831 // fall through to MESSAGE
1832 case KAAlarm::MESSAGE:
1833 case KAAlarm::FILE:
1835 // Display a message, file or command output, provided that the same event
1836 // isn't already being displayed
1837 #ifdef USE_AKONADI
1838 MessageWin* win = MessageWin::findEvent(EventId(event));
1839 #else
1840 MessageWin* win = MessageWin::findEvent(event.id());
1841 #endif
1842 // Find if we're changing a reminder message to the real message
1843 bool reminder = (alarm.type() & KAAlarm::REMINDER_ALARM);
1844 bool replaceReminder = !reminder && win && (win->alarmType() & KAAlarm::REMINDER_ALARM);
1845 if (!reminder
1846 && (!event.deferred() || (event.extraActionOptions() & KAEvent::ExecPreActOnDeferral))
1847 && (replaceReminder || !win) && !noPreAction
1848 && !event.preAction().isEmpty())
1850 // It's not a reminder alarm, and it's not a deferred alarm unless the
1851 // pre-alarm action applies to deferred alarms, and there is no message
1852 // window (other than a reminder window) currently displayed for this
1853 // alarm, and we need to execute a command before displaying the new window.
1855 // NOTE: The pre-action is not executed for a recurring alarm if an
1856 // alarm message window for a previous occurrence is still visible.
1857 // Check whether the command is already being executed for this alarm.
1858 for (int i = 0, end = mCommandProcesses.count(); i < end; ++i)
1860 ProcData* pd = mCommandProcesses[i];
1861 if (pd->event->id() == event.id() && (pd->flags & ProcData::PRE_ACTION))
1863 kDebug() << "Already executing pre-DISPLAY command";
1864 return pd->process; // already executing - don't duplicate the action
1868 // doShellCommand() will error if the user is not authorised to run
1869 // shell commands.
1870 QString command = event.preAction();
1871 kDebug() << "Pre-DISPLAY command:" << command;
1872 int flags = (reschedule ? ProcData::RESCHEDULE : 0) | (allowDefer ? ProcData::ALLOW_DEFER : 0);
1873 if (doShellCommand(command, event, &alarm, (flags | ProcData::PRE_ACTION)))
1875 AlarmCalendar::resources()->setAlarmPending(&event);
1876 return result; // display the message after the command completes
1878 // Error executing command
1879 if (event.cancelOnPreActionError())
1881 // Cancel the rest of the alarm execution
1882 kDebug() << event.id() << ": pre-action failed: cancelled";
1883 if (reschedule)
1884 rescheduleAlarm(event, alarm, true);
1885 return 0;
1887 // Display the message even though it failed
1890 if (!win)
1892 // There isn't already a message for this event
1893 int flags = (reschedule ? 0 : MessageWin::NO_RESCHEDULE) | (allowDefer ? 0 : MessageWin::NO_DEFER);
1894 (new MessageWin(&event, alarm, flags))->show();
1896 else if (replaceReminder)
1898 // The caption needs to be changed from "Reminder" to "Message"
1899 win->cancelReminder(event, alarm);
1901 else if (!win->hasDefer() && !alarm.repeatAtLogin())
1903 // It's a repeat-at-login message with no Defer button,
1904 // which has now reached its final trigger time and needs
1905 // to be replaced with a new message.
1906 win->showDefer();
1907 win->showDateTime(event, alarm);
1909 else
1911 // Use the existing message window
1913 if (win)
1915 // Raise the existing message window and replay any sound
1916 win->repeat(alarm); // N.B. this reschedules the alarm
1918 break;
1920 case KAAlarm::EMAIL:
1922 kDebug() << "EMAIL to:" << event.emailAddresses(",");
1923 QStringList errmsgs;
1924 KAMail::JobData data(event, alarm, reschedule, (reschedule || allowDefer));
1925 data.queued = true;
1926 int ans = KAMail::send(data, errmsgs);
1927 if (ans)
1929 // The email has either been sent or failed - not queued
1930 if (ans < 0)
1931 result = 0; // failure
1932 data.queued = false;
1933 emailSent(data, errmsgs, (ans > 0));
1935 else
1937 result = (void*)-1; // email has been queued
1939 if (reschedule)
1940 rescheduleAlarm(event, alarm, true);
1941 break;
1943 case KAAlarm::AUDIO:
1945 // Play the sound, provided that the same event
1946 // isn't already playing
1947 #ifdef USE_AKONADI
1948 MessageWin* win = MessageWin::findEvent(EventId(event));
1949 #else
1950 MessageWin* win = MessageWin::findEvent(event.id());
1951 #endif
1952 if (!win)
1954 // There isn't already a message for this event.
1955 int flags = (reschedule ? 0 : MessageWin::NO_RESCHEDULE) | MessageWin::ALWAYS_HIDE;
1956 win = new MessageWin(&event, alarm, flags);
1958 else
1960 // There's an existing message window: replay the sound
1961 win->repeat(alarm); // N.B. this reschedules the alarm
1963 return win;
1965 default:
1966 return 0;
1968 return result;
1971 /******************************************************************************
1972 * Called when sending an email has completed.
1974 void KAlarmApp::emailSent(KAMail::JobData& data, const QStringList& errmsgs, bool copyerr)
1976 if (!errmsgs.isEmpty())
1978 // Some error occurred, although the email may have been sent successfully
1979 if (errmsgs.count() > 1)
1980 kDebug() << (copyerr ? "Copy error:" : "Failed:") << errmsgs[1];
1981 MessageWin::showError(data.event, data.alarm.dateTime(), errmsgs);
1983 else if (data.queued)
1984 emit execAlarmSuccess();
1987 /******************************************************************************
1988 * Execute the command specified in a command alarm.
1989 * To connect to the output ready signals of the process, specify a slot to be
1990 * called by supplying 'receiver' and 'slot' parameters.
1992 ShellProcess* KAlarmApp::execCommandAlarm(const KAEvent& event, const KAAlarm& alarm, const QObject* receiver, const char* slot)
1994 // doShellCommand() will error if the user is not authorised to run
1995 // shell commands.
1996 int flags = (event.commandXterm() ? ProcData::EXEC_IN_XTERM : 0)
1997 | (event.commandDisplay() ? ProcData::DISP_OUTPUT : 0);
1998 QString command = event.cleanText();
1999 if (event.commandScript())
2001 // Store the command script in a temporary file for execution
2002 kDebug() << "Script";
2003 QString tmpfile = createTempScriptFile(command, false, event, alarm);
2004 if (tmpfile.isEmpty())
2006 setEventCommandError(event, KAEvent::CMD_ERROR);
2007 return 0;
2009 return doShellCommand(tmpfile, event, &alarm, (flags | ProcData::TEMP_FILE), receiver, slot);
2011 else
2013 kDebug() << command;
2014 return doShellCommand(command, event, &alarm, flags, receiver, slot);
2018 /******************************************************************************
2019 * Execute a shell command line specified by an alarm.
2020 * If the PRE_ACTION bit of 'flags' is set, the alarm will be executed via
2021 * execAlarm() once the command completes, the execAlarm() parameters being
2022 * derived from the remaining bits in 'flags'.
2023 * 'flags' must contain the bit PRE_ACTION or POST_ACTION if and only if it is
2024 * a pre- or post-alarm action respectively.
2025 * To connect to the output ready signals of the process, specify a slot to be
2026 * called by supplying 'receiver' and 'slot' parameters.
2028 * Note that if shell access is not authorised, the attempt to run the command
2029 * will be errored.
2031 ShellProcess* KAlarmApp::doShellCommand(const QString& command, const KAEvent& event, const KAAlarm* alarm, int flags, const QObject* receiver, const char* slot)
2033 kDebug() << command << "," << event.id();
2034 QIODevice::OpenMode mode = QIODevice::WriteOnly;
2035 QString cmd;
2036 QString tmpXtermFile;
2037 if (flags & ProcData::EXEC_IN_XTERM)
2039 // Execute the command in a terminal window.
2040 cmd = composeXTermCommand(command, event, alarm, flags, tmpXtermFile);
2042 else
2044 cmd = command;
2045 mode = QIODevice::ReadWrite;
2048 ProcData* pd = 0;
2049 ShellProcess* proc = 0;
2050 if (!cmd.isEmpty())
2052 // Use ShellProcess, which automatically checks whether the user is
2053 // authorised to run shell commands.
2054 proc = new ShellProcess(cmd);
2055 proc->setEnv(QLatin1String("KALARM_UID"), event.id(), true);
2056 proc->setOutputChannelMode(KProcess::MergedChannels); // combine stdout & stderr
2057 connect(proc, SIGNAL(shellExited(ShellProcess*)), SLOT(slotCommandExited(ShellProcess*)));
2058 if ((flags & ProcData::DISP_OUTPUT) && receiver && slot)
2060 connect(proc, SIGNAL(receivedStdout(ShellProcess*)), receiver, slot);
2061 connect(proc, SIGNAL(receivedStderr(ShellProcess*)), receiver, slot);
2063 if (mode == QIODevice::ReadWrite && !event.logFile().isEmpty())
2065 // Output is to be appended to a log file.
2066 // Set up a logging process to write the command's output to.
2067 QString heading;
2068 if (alarm && alarm->dateTime().isValid())
2070 QString dateTime = alarm->dateTime().formatLocale();
2071 heading.sprintf("\n******* KAlarm %s *******\n", dateTime.toLatin1().data());
2073 else
2074 heading = QLatin1String("\n******* KAlarm *******\n");
2075 QFile logfile(event.logFile());
2076 if (logfile.open(QIODevice::Append | QIODevice::Text))
2078 QTextStream out(&logfile);
2079 out << heading;
2080 logfile.close();
2082 proc->setStandardOutputFile(event.logFile(), QIODevice::Append);
2084 pd = new ProcData(proc, new KAEvent(event), (alarm ? new KAAlarm(*alarm) : 0), flags);
2085 if (flags & ProcData::TEMP_FILE)
2086 pd->tempFiles += command;
2087 if (!tmpXtermFile.isEmpty())
2088 pd->tempFiles += tmpXtermFile;
2089 mCommandProcesses.append(pd);
2090 if (proc->start(mode))
2091 return proc;
2094 // Error executing command - report it
2095 kWarning() << "Command failed to start";
2096 commandErrorMsg(proc, event, alarm, flags);
2097 if (pd)
2099 mCommandProcesses.removeAt(mCommandProcesses.indexOf(pd));
2100 delete pd;
2102 return 0;
2105 /******************************************************************************
2106 * Compose a command line to execute the given command in a terminal window.
2107 * 'tempScriptFile' receives the name of a temporary script file which is
2108 * invoked by the command line, if applicable.
2109 * Reply = command line, or empty string if error.
2111 QString KAlarmApp::composeXTermCommand(const QString& command, const KAEvent& event, const KAAlarm* alarm, int flags, QString& tempScriptFile) const
2113 kDebug() << command << "," << event.id();
2114 tempScriptFile.clear();
2115 QString cmd = Preferences::cmdXTermCommand();
2116 cmd.replace("%t", KGlobal::mainComponent().aboutData()->programName()); // set the terminal window title
2117 if (cmd.indexOf("%C") >= 0)
2119 // Execute the command from a temporary script file
2120 if (flags & ProcData::TEMP_FILE)
2121 cmd.replace("%C", command); // the command is already calling a temporary file
2122 else
2124 tempScriptFile = createTempScriptFile(command, true, event, *alarm);
2125 if (tempScriptFile.isEmpty())
2126 return QString();
2127 cmd.replace("%C", tempScriptFile); // %C indicates where to insert the command
2130 else if (cmd.indexOf("%W") >= 0)
2132 // Execute the command from a temporary script file,
2133 // with a sleep after the command is executed
2134 tempScriptFile = createTempScriptFile(command + QLatin1String("\nsleep 86400\n"), true, event, *alarm);
2135 if (tempScriptFile.isEmpty())
2136 return QString();
2137 cmd.replace("%W", tempScriptFile); // %w indicates where to insert the command
2139 else if (cmd.indexOf("%w") >= 0)
2141 // Append a sleep to the command.
2142 // Quote the command in case it contains characters such as [>|;].
2143 QString exec = KShell::quoteArg(command + QLatin1String("; sleep 86400"));
2144 cmd.replace("%w", exec); // %w indicates where to insert the command string
2146 else
2148 // Set the command to execute.
2149 // Put it in quotes in case it contains characters such as [>|;].
2150 QString exec = KShell::quoteArg(command);
2151 if (cmd.indexOf("%c") >= 0)
2152 cmd.replace("%c", exec); // %c indicates where to insert the command string
2153 else
2154 cmd.append(exec); // otherwise, simply append the command string
2156 return cmd;
2159 /******************************************************************************
2160 * Create a temporary script file containing the specified command string.
2161 * Reply = path of temporary file, or null string if error.
2163 QString KAlarmApp::createTempScriptFile(const QString& command, bool insertShell, const KAEvent& event, const KAAlarm& alarm) const
2165 KTemporaryFile tmpFile;
2166 tmpFile.setAutoRemove(false); // don't delete file when it is destructed
2167 if (!tmpFile.open())
2168 kError() << "Unable to create a temporary script file";
2169 else
2171 tmpFile.setPermissions(QFile::ReadUser | QFile::WriteUser | QFile::ExeUser);
2172 QTextStream stream(&tmpFile);
2173 if (insertShell)
2174 stream << "#!" << ShellProcess::shellPath() << "\n";
2175 stream << command;
2176 stream.flush();
2177 if (tmpFile.error() != QFile::NoError)
2178 kError() << "Error" << tmpFile.errorString() << " writing to temporary script file";
2179 else
2180 return tmpFile.fileName();
2183 QStringList errmsgs(i18nc("@info", "Error creating temporary script file"));
2184 MessageWin::showError(event, alarm.dateTime(), errmsgs, QLatin1String("Script"));
2185 return QString();
2188 /******************************************************************************
2189 * Called when a command alarm's execution completes.
2191 void KAlarmApp::slotCommandExited(ShellProcess* proc)
2193 kDebug();
2194 // Find this command in the command list
2195 for (int i = 0, end = mCommandProcesses.count(); i < end; ++i)
2197 ProcData* pd = mCommandProcesses[i];
2198 if (pd->process == proc)
2200 // Found the command. Check its exit status.
2201 bool executeAlarm = pd->preAction();
2202 ShellProcess::Status status = proc->status();
2203 if (status == ShellProcess::SUCCESS && !proc->exitCode())
2205 kDebug() << pd->event->id() << ": SUCCESS";
2206 clearEventCommandError(*pd->event, pd->preAction() ? KAEvent::CMD_ERROR_PRE
2207 : pd->postAction() ? KAEvent::CMD_ERROR_POST
2208 : KAEvent::CMD_ERROR);
2210 else
2212 QString errmsg = proc->errorMessage();
2213 if (status == ShellProcess::SUCCESS || status == ShellProcess::NOT_FOUND)
2214 kWarning() << pd->event->id() << ":" << errmsg << "exit status =" << status << ", code =" << proc->exitCode();
2215 else
2216 kWarning() << pd->event->id() << ":" << errmsg << "exit status =" << status;
2217 if (pd->messageBoxParent)
2219 // Close the existing informational KMessageBox for this process
2220 QList<KDialog*> dialogs = pd->messageBoxParent->findChildren<KDialog*>();
2221 if (!dialogs.isEmpty())
2222 delete dialogs[0];
2223 setEventCommandError(*pd->event, pd->preAction() ? KAEvent::CMD_ERROR_PRE
2224 : pd->postAction() ? KAEvent::CMD_ERROR_POST
2225 : KAEvent::CMD_ERROR);
2226 if (!pd->tempFile())
2228 errmsg += '\n';
2229 errmsg += proc->command();
2231 KAMessageBox::error(pd->messageBoxParent, errmsg);
2233 else
2234 commandErrorMsg(proc, *pd->event, pd->alarm, pd->flags);
2236 if (executeAlarm && pd->event->cancelOnPreActionError())
2238 kDebug() << pd->event->id() << ": pre-action failed: cancelled";
2239 if (pd->reschedule())
2240 rescheduleAlarm(*pd->event, *pd->alarm, true);
2241 executeAlarm = false;
2244 if (pd->preAction())
2245 AlarmCalendar::resources()->setAlarmPending(pd->event, false);
2246 if (executeAlarm)
2247 execAlarm(*pd->event, *pd->alarm, pd->reschedule(), pd->allowDefer(), true);
2248 mCommandProcesses.removeAt(i);
2249 delete pd;
2250 break;
2254 // If there are now no executing shell commands, quit if a quit was queued
2255 if (mPendingQuit && mCommandProcesses.isEmpty())
2256 quitIf(mPendingQuitCode);
2259 /******************************************************************************
2260 * Output an error message for a shell command, and record the alarm's error status.
2262 void KAlarmApp::commandErrorMsg(const ShellProcess* proc, const KAEvent& event, const KAAlarm* alarm, int flags)
2264 KAEvent::CmdErrType cmderr;
2265 QStringList errmsgs;
2266 QString dontShowAgain;
2267 if (flags & ProcData::PRE_ACTION)
2269 if (event.dontShowPreActionError())
2270 return; // don't notify user of any errors for the alarm
2271 errmsgs += i18nc("@info", "Pre-alarm action:");
2272 dontShowAgain = QLatin1String("Pre");
2273 cmderr = KAEvent::CMD_ERROR_PRE;
2275 else if (flags & ProcData::POST_ACTION)
2277 errmsgs += i18nc("@info", "Post-alarm action:");
2278 dontShowAgain = QLatin1String("Post");
2279 cmderr = (event.commandError() == KAEvent::CMD_ERROR_PRE)
2280 ? KAEvent::CMD_ERROR_PRE_POST : KAEvent::CMD_ERROR_POST;
2282 else
2284 dontShowAgain = QLatin1String("Exec");
2285 cmderr = KAEvent::CMD_ERROR;
2288 // Record the alarm's error status
2289 setEventCommandError(event, cmderr);
2290 // Display an error message
2291 if (proc)
2293 errmsgs += proc->errorMessage();
2294 if (!(flags & ProcData::TEMP_FILE))
2295 errmsgs += proc->command();
2296 dontShowAgain += QString::number(proc->status());
2298 MessageWin::showError(event, (alarm ? alarm->dateTime() : DateTime()), errmsgs, dontShowAgain);
2301 /******************************************************************************
2302 * Notes that an informational KMessageBox is displayed for this process.
2304 void KAlarmApp::commandMessage(ShellProcess* proc, QWidget* parent)
2306 // Find this command in the command list
2307 for (int i = 0, end = mCommandProcesses.count(); i < end; ++i)
2309 ProcData* pd = mCommandProcesses[i];
2310 if (pd->process == proc)
2312 pd->messageBoxParent = parent;
2313 break;
2318 /******************************************************************************
2319 * Return a D-Bus interface object for KSpeech.
2320 * The KTTSD D-Bus service is started if necessary.
2321 * If the service cannot be started, 'error' is set to an error text.
2323 OrgKdeKSpeechInterface* KAlarmApp::kspeechInterface(QString& error) const
2325 error.clear();
2326 QDBusConnection client = QDBusConnection::sessionBus();
2327 if (!client.interface()->isServiceRegistered(KTTSD_DBUS_SERVICE))
2329 // kttsd is not running, so start it
2330 delete mKSpeech;
2331 mKSpeech = 0;
2332 if (KToolInvocation::startServiceByDesktopName(QLatin1String("kttsd"), QStringList(), &error))
2334 kDebug() << "Failed to start kttsd:" << error;
2335 return 0;
2338 if (!mKSpeech)
2340 mKSpeech = new OrgKdeKSpeechInterface(KTTSD_DBUS_SERVICE, KTTDS_DBUS_PATH, QDBusConnection::sessionBus());
2341 mKSpeech->setParent(theApp());
2342 mKSpeech->setApplicationName(KGlobal::mainComponent().aboutData()->programName());
2343 mKSpeech->setDefaultPriority(KSpeech::jpMessage);
2345 return mKSpeech;
2348 /******************************************************************************
2349 * Called when a D-Bus service unregisters.
2350 * If it's the KTTSD service, delete the KSpeech interface object.
2352 void KAlarmApp::slotDBusServiceUnregistered(const QString& serviceName)
2354 if (serviceName == KTTSD_DBUS_SERVICE)
2356 delete mKSpeech;
2357 mKSpeech = 0;
2361 /******************************************************************************
2362 * If this is the first time through, open the calendar file, and start
2363 * processing the execution queue.
2365 #ifdef USE_AKONADI
2366 bool KAlarmApp::initCheck(bool calendarOnly, bool waitForCollection, Akonadi::Collection::Id collectionId)
2367 #else
2368 bool KAlarmApp::initCheck(bool calendarOnly)
2369 #endif
2371 static bool firstTime = true;
2372 if (firstTime)
2374 kDebug() << "first time";
2376 /* Need to open the display calendar now, since otherwise if display
2377 * alarms are immediately due, they will often be processed while
2378 * MessageWin::redisplayAlarms() is executing open() (but before open()
2379 * completes), which causes problems!!
2381 AlarmCalendar::displayCalendar()->open();
2383 if (!AlarmCalendar::resources()->open())
2384 return false;
2385 setArchivePurgeDays();
2387 // Warn the user if there are no writable active alarm calendars
2388 checkWritableCalendar();
2390 firstTime = false;
2393 if (!calendarOnly)
2394 startProcessQueue(); // start processing the execution queue
2396 #ifdef USE_AKONADI
2397 if (waitForCollection)
2399 #if KDE_IS_VERSION(4,9,80)
2400 // Wait for one or all Akonadi collections to be populated
2401 if (!CollectionControlModel::instance()->waitUntilPopulated(collectionId, AKONADI_TIMEOUT))
2402 return false;
2403 #endif
2405 #endif
2406 return true;
2409 /******************************************************************************
2410 * Called when an audio thread starts or stops.
2412 void KAlarmApp::notifyAudioPlaying(bool playing)
2414 emit audioPlaying(playing);
2417 /******************************************************************************
2418 * Stop audio play.
2420 void KAlarmApp::stopAudio()
2422 MessageWin::stopAudio();
2426 void setEventCommandError(const KAEvent& event, KAEvent::CmdErrType err)
2428 if (err == KAEvent::CMD_ERROR_POST && event.commandError() == KAEvent::CMD_ERROR_PRE)
2429 err = KAEvent::CMD_ERROR_PRE_POST;
2430 event.setCommandError(err);
2431 #ifdef USE_AKONADI
2432 KAEvent* ev = AlarmCalendar::resources()->event(EventId(event));
2433 #else
2434 KAEvent* ev = AlarmCalendar::resources()->event(event.id());
2435 #endif
2436 if (ev && ev->commandError() != err)
2437 ev->setCommandError(err);
2438 #ifdef USE_AKONADI
2439 AkonadiModel::instance()->updateCommandError(event);
2440 #else
2441 EventListModel::alarms()->updateCommandError(event.id());
2442 #endif
2445 void clearEventCommandError(const KAEvent& event, KAEvent::CmdErrType err)
2447 KAEvent::CmdErrType newerr = static_cast<KAEvent::CmdErrType>(event.commandError() & ~err);
2448 event.setCommandError(newerr);
2449 #ifdef USE_AKONADI
2450 KAEvent* ev = AlarmCalendar::resources()->event(EventId(event));
2451 #else
2452 KAEvent* ev = AlarmCalendar::resources()->event(event.id());
2453 #endif
2454 if (ev)
2456 newerr = static_cast<KAEvent::CmdErrType>(ev->commandError() & ~err);
2457 ev->setCommandError(newerr);
2459 #ifdef USE_AKONADI
2460 AkonadiModel::instance()->updateCommandError(event);
2461 #else
2462 EventListModel::alarms()->updateCommandError(event.id());
2463 #endif
2467 KAlarmApp::ProcData::ProcData(ShellProcess* p, KAEvent* e, KAAlarm* a, int f)
2468 : process(p),
2469 event(e),
2470 alarm(a),
2471 messageBoxParent(0),
2472 flags(f)
2475 KAlarmApp::ProcData::~ProcData()
2477 while (!tempFiles.isEmpty())
2479 // Delete the temporary file called by the XTerm command
2480 QFile f(tempFiles.first());
2481 f.remove();
2482 tempFiles.removeFirst();
2484 delete process;
2485 delete event;
2486 delete alarm;
2489 // vim: et sw=4: