diff --git a/src/kalarmapp.cpp b/src/kalarmapp.cpp index 48c6de76..12857304 100644 --- a/src/kalarmapp.cpp +++ b/src/kalarmapp.cpp @@ -1,2658 +1,2682 @@ /* * kalarmapp.cpp - the KAlarm application object * Program: kalarm * Copyright © 2001-2020 David Jarvie * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kalarmapp.h" #include "alarmcalendar.h" #include "alarmtime.h" #include "commandoptions.h" #include "dbushandler.h" #include "editdlgtypes.h" #include "functions.h" #include "kamail.h" #include "mainwindow.h" #include "messagewin.h" #include "kalarmmigrateapplication.h" #include "preferences.h" #include "prefdlg.h" #include "startdaytimer.h" #include "traywindow.h" #include "resources/datamodel.h" #include "resources/resources.h" #include "resources/eventmodel.h" #include "lib/desktop.h" #include "lib/messagebox.h" #include "lib/shellprocess.h" #include "kalarm_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include static const int AKONADI_TIMEOUT = 30; // timeout (seconds) for Akonadi collections to be populated /****************************************************************************** * Find the maximum number of seconds late which a late-cancel alarm is allowed * to be. This is calculated as the late cancel interval, plus a few seconds * leeway to cater for any timing irregularities. */ static inline int maxLateness(int lateCancel) { static const int LATENESS_LEEWAY = 5; int lc = (lateCancel >= 1) ? (lateCancel - 1)*60 : 0; return LATENESS_LEEWAY + lc; } KAlarmApp* KAlarmApp::mInstance = nullptr; int KAlarmApp::mActiveCount = 0; int KAlarmApp::mFatalError = 0; QString KAlarmApp::mFatalMessage; /****************************************************************************** * Construct the application. */ KAlarmApp::KAlarmApp(int& argc, char** argv) : QApplication(argc, argv) , mDBusHandler(new DBusHandler()) { qCDebug(KALARM_LOG) << "KAlarmApp:"; KAlarmMigrateApplication migrate; migrate.migrate(); #ifndef NDEBUG KAlarm::setTestModeConditions(); #endif setQuitOnLastWindowClosed(false); Preferences::self(); // read KAlarm configuration if (!Preferences::noAutoStart()) { // Strip out any "OnlyShowIn=KDE" list from kalarm.autostart.desktop Preferences::setNoAutoStart(false); // Enable kalarm.autostart.desktop to start KAlarm Preferences::setAutoStart(true); Preferences::self()->save(); } Preferences::connect(SIGNAL(startOfDayChanged(QTime)), this, SLOT(changeStartOfDay())); Preferences::connect(SIGNAL(workTimeChanged(QTime,QTime,QBitArray)), this, SLOT(slotWorkTimeChanged(QTime,QTime,QBitArray))); Preferences::connect(SIGNAL(holidaysChanged(KHolidays::HolidayRegion)), this, SLOT(slotHolidaysChanged(KHolidays::HolidayRegion))); Preferences::connect(SIGNAL(feb29TypeChanged(Feb29Type)), this, SLOT(slotFeb29TypeChanged(Feb29Type))); Preferences::connect(SIGNAL(showInSystemTrayChanged(bool)), this, SLOT(slotShowInSystemTrayChanged())); Preferences::connect(SIGNAL(archivedKeepDaysChanged(int)), this, SLOT(setArchivePurgeDays())); Preferences::connect(SIGNAL(messageFontChanged(QFont)), this, SLOT(slotMessageFontChanged(QFont))); slotFeb29TypeChanged(Preferences::defaultFeb29Type()); KAEvent::setStartOfDay(Preferences::startOfDay()); KAEvent::setWorkTime(Preferences::workDays(), Preferences::workDayStart(), Preferences::workDayEnd()); KAEvent::setHolidays(Preferences::holidays()); KAEvent::setDefaultFont(Preferences::messageFont()); // Check if KOrganizer is installed const QString korg = QStringLiteral("korganizer"); mKOrganizerEnabled = !QStandardPaths::findExecutable(korg).isEmpty(); if (!mKOrganizerEnabled) { qCDebug(KALARM_LOG) << "KAlarmApp: KOrganizer options disabled (KOrganizer not found)"; } // Check if the window manager can't handle keyboard focus transfer between windows mWindowFocusBroken = (Desktop::currentIdentity() == Desktop::Unity); if (mWindowFocusBroken) { qCDebug(KALARM_LOG) << "KAlarmApp: Window keyboard focus broken"; } } /****************************************************************************** */ KAlarmApp::~KAlarmApp() { while (!mCommandProcesses.isEmpty()) { ProcData* pd = mCommandProcesses.at(0); mCommandProcesses.pop_front(); delete pd; } AlarmCalendar::terminateCalendars(); } /****************************************************************************** * Return the one and only KAlarmApp instance. * If it doesn't already exist, it is created first. */ KAlarmApp* KAlarmApp::create(int& argc, char** argv) { if (!mInstance) { mInstance = new KAlarmApp(argc, argv); if (mFatalError) mInstance->quitFatal(); } return mInstance; } /****************************************************************************** * Perform initialisations which may require the constructor to have completed * and KAboutData to have been set up. */ void KAlarmApp::initialise() { if (initialiseTimerResources()) // initialise calendars and alarm timer { Resources* resources = Resources::instance(); connect(resources, &Resources::resourceAdded, this, &KAlarmApp::slotResourceAdded); connect(resources, &Resources::resourcePopulated, this, &KAlarmApp::slotResourcePopulated); connect(resources, &Resources::resourcePopulated, this, &KAlarmApp::purgeNewArchivedDefault); connect(resources, &Resources::resourcesCreated, this, &KAlarmApp::slotResourcesCreated); connect(resources, &Resources::migrationCompleted, this, &KAlarmApp::checkWritableCalendar); connect(resources, &Resources::resourcesPopulated, this, &KAlarmApp::processQueue); KConfigGroup config(KSharedConfig::openConfig(), "General"); mNoSystemTray = config.readEntry("NoSystemTray", false); mOldShowInSystemTray = wantShowInSystemTray(); DateTime::setStartOfDay(Preferences::startOfDay()); mPrefsArchivedColour = Preferences::archivedColour(); } } /****************************************************************************** * Initialise or reinitialise things which are tidied up/closed by quitIf(). * Reinitialisation can be necessary if session restoration finds nothing to * restore and starts quitting the application, but KAlarm then starts up again * before the application has exited. * Reply = true if calendars were initialised successfully, * false if they were already initialised, or if initialisation failed. */ bool KAlarmApp::initialiseTimerResources() { if (!mAlarmTimer) { mAlarmTimer = new QTimer(this); mAlarmTimer->setSingleShot(true); connect(mAlarmTimer, &QTimer::timeout, this, &KAlarmApp::checkNextDueAlarm); } if (!AlarmCalendar::resources()) { qCDebug(KALARM_LOG) << "KAlarmApp::initialise: initialising calendars"; if (AlarmCalendar::initialiseCalendars()) { connect(AlarmCalendar::resources(), &AlarmCalendar::earliestAlarmChanged, this, &KAlarmApp::checkNextDueAlarm); connect(AlarmCalendar::resources(), &AlarmCalendar::atLoginEventAdded, this, &KAlarmApp::atLoginEventAdded); return true; } } return false; } /****************************************************************************** * Restore the saved session if required. */ bool KAlarmApp::restoreSession() { if (!isSessionRestored()) return false; if (mFatalError) { quitFatal(); return false; } // Process is being restored by session management. qCDebug(KALARM_LOG) << "KAlarmApp::restoreSession: Restoring"; ++mActiveCount; // Create the session config object now. // This is necessary since if initCheck() below causes calendars to be updated, // the session config created after that points to an invalid file, resulting // in no windows being restored followed by a later crash. KConfigGui::sessionConfig(); // When KAlarm is session restored, automatically set start-at-login to true. Preferences::self()->load(); Preferences::setAutoStart(true); Preferences::setNoAutoStart(false); Preferences::setAskAutoStart(true); // cancel any start-at-login prompt suppression Preferences::self()->save(); if (!initCheck(true)) // open the calendar file (needed for main windows), don't process queue yet { --mActiveCount; quitIf(1, true); // error opening the main calendar - quit return false; } MainWindow* trayParent = nullptr; for (int i = 1; KMainWindow::canBeRestored(i); ++i) { const QString type = KMainWindow::classNameOfToplevel(i); if (type == QLatin1String("MainWindow")) { MainWindow* win = MainWindow::create(true); win->restore(i, false); if (win->isHiddenTrayParent()) trayParent = win; else win->show(); } else if (type == QLatin1String("MessageWin")) { MessageWin* win = new MessageWin; win->restore(i, false); if (win->isValid()) { if (Resources::allCreated()) win->show(); } else delete win; } } // Try to display the system tray icon if it is configured to be shown if (trayParent || wantShowInSystemTray()) { if (!MainWindow::count()) qCWarning(KALARM_LOG) << "KAlarmApp::restoreSession: no main window to be restored!?"; else { displayTrayIcon(true, trayParent); // Occasionally for no obvious reason, the main main window is // shown when it should be hidden, so hide it just to be sure. if (trayParent) trayParent->hide(); } } --mActiveCount; if (quitIf(0)) // quit if no windows are open return false; // quitIf() can sometimes return, despite calling exit() startProcessQueue(); // start processing the execution queue return true; } /****************************************************************************** * Called for a unique QApplication when a new instance of the application is * started. * Reply: exit code (>= 0), or -1 to continue execution. * If exit code >= 0, 'outputText' holds text to output before terminating. */ void KAlarmApp::activateByDBus(const QStringList& args, const QString& workingDirectory) { activateInstance(args, workingDirectory, nullptr); } /****************************************************************************** * Called to start a new instance of the application. * Reply: exit code (>= 0), or -1 to continue execution. * If exit code >= 0, 'outputText' holds text to output before terminating. */ int KAlarmApp::activateInstance(const QStringList& args, const QString& workingDirectory, QString* outputText) { Q_UNUSED(workingDirectory) qCDebug(KALARM_LOG) << "KAlarmApp::activateInstance"; if (outputText) outputText->clear(); if (mFatalError) { quitFatal(); return 1; } // The D-Bus call to activate a subsequent instance of KAlarm may not supply // any arguments, but we need one. if (!args.isEmpty() && mActivateArg0.isEmpty()) mActivateArg0 = args[0]; QStringList fixedArgs(args); if (args.isEmpty() && !mActivateArg0.isEmpty()) fixedArgs << mActivateArg0; // Parse and interpret command line arguments. QCommandLineParser parser; KAboutData::applicationData().setupCommandLine(&parser); parser.setApplicationDescription(QApplication::applicationDisplayName()); CommandOptions* options = new CommandOptions; const QStringList newArgs = options->setOptions(&parser, fixedArgs); options->parse(); KAboutData::applicationData().processCommandLine(&parser); ++mActiveCount; int exitCode = 0; // default = success static bool firstInstance = true; bool dontRedisplay = false; CommandOptions::Command command = CommandOptions::NONE; const bool processOptions = (!firstInstance || !isSessionRestored()); if (processOptions) { options->process(); #ifndef NDEBUG if (options->simulationTime().isValid()) KAlarm::setSimulatedSystemTime(options->simulationTime()); #endif command = options->command(); if (options->disableAll()) setAlarmsEnabled(false); // disable alarm monitoring // Handle options which exit with a terminal message, before // making the application a unique application, since a // unique application won't output to the terminal if another // instance is already running. switch (command) { case CommandOptions::CMD_ERROR: if (outputText) { *outputText = options->outputText(); delete options; return 1; } mReadOnly = true; // don't need write access to calendars exitCode = 1; break; case CommandOptions::EXIT: if (outputText) { *outputText = options->outputText(); delete options; return 0; } exitCode = -1; break; default: break; } } // Make this a unique application. KDBusService* s = new KDBusService(KDBusService::Unique, this); connect(this, &KAlarmApp::aboutToQuit, s, &KDBusService::deleteLater); connect(s, &KDBusService::activateRequested, this, &KAlarmApp::activateByDBus); if (processOptions) { switch (command) { case CommandOptions::TRIGGER_EVENT: case CommandOptions::CANCEL_EVENT: { // Display or delete the event with the specified event ID - const EventFunc function = (command == CommandOptions::TRIGGER_EVENT) ? EVENT_TRIGGER : EVENT_CANCEL; + const QueuedAction action = static_cast(int((command == CommandOptions::TRIGGER_EVENT) ? QueuedAction::Trigger : QueuedAction::Cancel) + | int(QueuedAction::FindId) | int(QueuedAction::Exit)); // Open the calendar, don't start processing execution queue yet, // and wait for the calendar resources to be populated. - if (!initCheck(true) - || !waitUntilPopulated(options->eventId().resourceId(), AKONADI_TIMEOUT)) + if (!initCheck(true)) exitCode = 1; else { + mCommandOption = options->commandName(); + mActionQueue.enqueue(ActionQEntry(action, options->eventId())); startProcessQueue(); // start processing the execution queue dontRedisplay = true; - if (!handleEvent(options->eventId(), function, true)) - { - CommandOptions::printError(xi18nc("@info:shell", "%1: Event %2 not found, or not unique", QStringLiteral("--") + options->commandName(), options->eventId().eventId())); - exitCode = 1; - } - else - createOnlyMainWindow(); // prevent the application from quitting } break; } case CommandOptions::LIST: // Output a list of scheduled alarms to stdout. // Open the calendar, don't start processing execution queue yet, // and wait for all calendar resources to be populated. mReadOnly = true; // don't need write access to calendars mAlarmsEnabled = false; // prevent alarms being processed - if (!initCheck(true) - || !waitUntilPopulated(-1, AKONADI_TIMEOUT)) + if (!initCheck(true)) exitCode = 1; else { + const QueuedAction action = static_cast(int(QueuedAction::List) | int(QueuedAction::Exit)); + mActionQueue.enqueue(ActionQEntry(action, EventId())); + startProcessQueue(); // start processing the execution queue dontRedisplay = true; - const QStringList alarms = scheduledAlarmList(); - for (const QString& alarm : alarms) - std::cout << alarm.toUtf8().constData() << std::endl; } break; case CommandOptions::EDIT: // Edit a specified existing alarm. // Open the calendar and wait for the calendar resources to be populated. if (!initCheck(false) || !waitUntilPopulated(options->eventId().resourceId(), AKONADI_TIMEOUT)) exitCode = 1; else { if (!KAlarm::editAlarmById(options->eventId())) { - CommandOptions::printError(xi18nc("@info:shell", "%1: Event %2 not found, or not editable", QStringLiteral("--") + options->commandName(), options->eventId().eventId())); + CommandOptions::printError(xi18nc("@info:shell", "%1: Event %2 not found, or not editable", options->commandName(), options->eventId().eventId())); exitCode = 1; } else createOnlyMainWindow(); // prevent the application from quitting } break; case CommandOptions::EDIT_NEW: { // Edit a new alarm, and optionally preset selected values if (!initCheck()) exitCode = 1; else { EditAlarmDlg* editDlg = EditAlarmDlg::create(false, options->editType(), MainWindow::mainMainWindow()); if (options->alarmTime().isValid()) editDlg->setTime(options->alarmTime()); if (options->recurrence()) editDlg->setRecurrence(*options->recurrence(), options->subRepeatInterval(), options->subRepeatCount()); else if (options->flags() & KAEvent::REPEAT_AT_LOGIN) editDlg->setRepeatAtLogin(); editDlg->setAction(options->editAction(), AlarmText(options->text())); if (options->lateCancel()) editDlg->setLateCancel(options->lateCancel()); if (options->flags() & KAEvent::COPY_KORGANIZER) editDlg->setShowInKOrganizer(true); switch (options->editType()) { case EditAlarmDlg::DISPLAY: { // EditAlarmDlg::create() always returns EditDisplayAlarmDlg for type = DISPLAY EditDisplayAlarmDlg* dlg = qobject_cast(editDlg); if (options->fgColour().isValid()) dlg->setFgColour(options->fgColour()); if (options->bgColour().isValid()) dlg->setBgColour(options->bgColour()); if (!options->audioFile().isEmpty() || options->flags() & (KAEvent::BEEP | KAEvent::SPEAK)) { const KAEvent::Flags flags = options->flags(); const Preferences::SoundType type = (flags & KAEvent::BEEP) ? Preferences::Sound_Beep : (flags & KAEvent::SPEAK) ? Preferences::Sound_Speak : Preferences::Sound_File; dlg->setAudio(type, options->audioFile(), options->audioVolume(), (flags & KAEvent::REPEAT_SOUND ? 0 : -1)); } if (options->reminderMinutes()) dlg->setReminder(options->reminderMinutes(), (options->flags() & KAEvent::REMINDER_ONCE)); if (options->flags() & KAEvent::CONFIRM_ACK) dlg->setConfirmAck(true); if (options->flags() & KAEvent::AUTO_CLOSE) dlg->setAutoClose(true); break; } case EditAlarmDlg::COMMAND: break; case EditAlarmDlg::EMAIL: { // EditAlarmDlg::create() always returns EditEmailAlarmDlg for type = EMAIL EditEmailAlarmDlg* dlg = qobject_cast(editDlg); if (options->fromID() || !options->addressees().isEmpty() || !options->subject().isEmpty() || !options->attachments().isEmpty()) dlg->setEmailFields(options->fromID(), options->addressees(), options->subject(), options->attachments()); if (options->flags() & KAEvent::EMAIL_BCC) dlg->setBcc(true); break; } case EditAlarmDlg::AUDIO: { // EditAlarmDlg::create() always returns EditAudioAlarmDlg for type = AUDIO EditAudioAlarmDlg* dlg = qobject_cast(editDlg); if (!options->audioFile().isEmpty() || options->audioVolume() >= 0) dlg->setAudio(options->audioFile(), options->audioVolume()); break; } case EditAlarmDlg::NO_TYPE: break; } createOnlyMainWindow(); // prevent the application from quitting // Execute the edit dialogue. Note that if no other instance of KAlarm is // running, this new instance will not exit after the dialogue is closed. // This is deliberate, since exiting would mean that KAlarm wouldn't // trigger the new alarm. KAlarm::execNewAlarmDlg(editDlg); } break; } case CommandOptions::EDIT_NEW_PRESET: // Edit a new alarm, preset with a template if (!initCheck()) exitCode = 1; else { createOnlyMainWindow(); // prevent the application from quitting // Execute the edit dialogue. Note that if no other instance of KAlarm is // running, this new instance will not exit after the dialogue is closed. // This is deliberate, since exiting would mean that KAlarm wouldn't // trigger the new alarm. KAlarm::editNewAlarm(options->templateName()); } break; case CommandOptions::NEW: // Display a message or file, execute a command, or send an email setResourcesTimeout(); // set timeout for resource initialisation if (!initCheck()) exitCode = 1; else { if (!scheduleEvent(options->editAction(), options->text(), options->alarmTime(), options->lateCancel(), options->flags(), options->bgColour(), options->fgColour(), QFont(), options->audioFile(), options->audioVolume(), options->reminderMinutes(), (options->recurrence() ? *options->recurrence() : KARecurrence()), options->subRepeatInterval(), options->subRepeatCount(), options->fromID(), options->addressees(), options->subject(), options->attachments())) exitCode = 1; else createOnlyMainWindow(); // prevent the application from quitting } break; case CommandOptions::TRAY: // Display only the system tray icon if (Preferences::showInSystemTray() && QSystemTrayIcon::isSystemTrayAvailable()) { if (!initCheck()) // open the calendar, start processing execution queue exitCode = 1; else { if (!displayTrayIcon(true)) exitCode = 1; } break; } Q_FALLTHROUGH(); // fall through to NONE case CommandOptions::NONE: // No arguments - run interactively & display the main window #ifndef NDEBUG if (options->simulationTime().isValid() && !firstInstance) break; // simulating time: don't open main window if already running #endif if (!initCheck()) exitCode = 1; else { if (mTrayWindow && mTrayWindow->assocMainWindow() && !mTrayWindow->assocMainWindow()->isVisible()) mTrayWindow->showAssocMainWindow(); else { MainWindow* win = MainWindow::create(); if (command == CommandOptions::TRAY) win->setWindowState(win->windowState() | Qt::WindowMinimized); win->show(); } } break; default: break; } } if (options != CommandOptions::firstInstance()) delete options; // If this is the first time through, redisplay any alarm message windows // from last time. if (firstInstance && !dontRedisplay && !exitCode) { /* First time through, so redisplay alarm message windows from last time. * But it is possible for session restoration in some circumstances to * not create any windows, in which case the alarm calendars will have * been deleted - if so, don't try to do anything. (This has been known * to happen under the Xfce desktop.) */ if (AlarmCalendar::resources()) { if (Resources::allCreated()) { mRedisplayAlarms = false; MessageWin::redisplayAlarms(); } else mRedisplayAlarms = true; } } --mActiveCount; firstInstance = false; // Quit the application if this was the last/only running "instance" of the program. // Executing 'return' doesn't work very well since the program continues to // run if no windows were created. if (quitIf(exitCode >= 0 ? exitCode : 0)) return exitCode; // exit this application instance return -1; // continue executing the application instance } /****************************************************************************** * Create a minimised main window if none already exists. * This prevents the application from quitting. */ void KAlarmApp::createOnlyMainWindow() { if (!MainWindow::count()) { if (Preferences::showInSystemTray() && QSystemTrayIcon::isSystemTrayAvailable()) { if (displayTrayIcon(true)) return; } MainWindow* win = MainWindow::create(); win->showMinimized(); } } /****************************************************************************** * Quit the program, optionally only if there are no more "instances" running. * Reply = true if program exited. */ bool KAlarmApp::quitIf(int exitCode, bool force) { if (force) { // Quit regardless, except for message windows mQuitting = true; MainWindow::closeAll(); mQuitting = false; displayTrayIcon(false); if (MessageWin::instanceCount(true)) // ignore always-hidden windows (e.g. audio alarms) return false; } else if (mQuitting) return false; // MainWindow::closeAll() causes quitIf() to be called again else { // Quit only if there are no more "instances" running mPendingQuit = false; if (mActiveCount > 0 || MessageWin::instanceCount(true)) // ignore always-hidden windows (e.g. audio alarms) return false; const int mwcount = MainWindow::count(); MainWindow* mw = mwcount ? MainWindow::firstWindow() : nullptr; if (mwcount > 1 || (mwcount && (!mw->isHidden() || !mw->isTrayParent()))) return false; // There are no windows left except perhaps a main window which is a hidden // tray icon parent, or an always-hidden message window. if (mTrayWindow) { // There is a system tray icon. // Don't exit unless the system tray doesn't seem to exist. if (checkSystemTray()) return false; } if (!mActionQueue.isEmpty() || !mCommandProcesses.isEmpty()) { // Don't quit yet if there are outstanding actions on the execution queue mPendingQuit = true; mPendingQuitCode = exitCode; return false; } } // This was the last/only running "instance" of the program, so exit completely. // NOTE: Everything which is terminated/deleted here must where applicable // be initialised in the initialiseTimerResources() method, in case // KAlarm is started again before application exit completes! qCDebug(KALARM_LOG) << "KAlarmApp::quitIf:" << exitCode << ": quitting"; MessageWin::stopAudio(true); if (mCancelRtcWake) { KAlarm::setRtcWakeTime(0, nullptr); KAlarm::deleteRtcWakeConfig(); } delete mAlarmTimer; // prevent checking for alarms after deleting calendars mAlarmTimer = nullptr; mInitialised = false; // prevent processQueue() from running AlarmCalendar::terminateCalendars(); + DataModel::terminate(); exit(exitCode); return true; // sometimes we actually get to here, despite calling exit() } /****************************************************************************** * Called when the Quit menu item is selected. * Closes the system tray window and all main windows, but does not exit the * program if other windows are still open. */ void KAlarmApp::doQuit(QWidget* parent) { qCDebug(KALARM_LOG) << "KAlarmApp::doQuit"; if (KAMessageBox::warningCancelContinue(parent, i18nc("@info", "Quitting will disable alarms (once any alarm message windows are closed)."), QString(), KStandardGuiItem::quit(), KStandardGuiItem::cancel(), Preferences::QUIT_WARN ) != KMessageBox::Continue) return; if (!KAlarm::checkRtcWakeConfig(true).isEmpty()) { // A wake-on-suspend alarm is set if (KAMessageBox::warningCancelContinue(parent, i18nc("@info", "Quitting will cancel the scheduled Wake from Suspend."), QString(), KStandardGuiItem::quit() ) != KMessageBox::Continue) return; mCancelRtcWake = true; } if (!Preferences::autoStart()) { int option = KMessageBox::No; if (!Preferences::autoStartChangedByUser()) { option = KAMessageBox::questionYesNoCancel(parent, xi18nc("@info", "Do you want to start KAlarm at login?" "(Note that alarms will be disabled if KAlarm is not started.)"), QString(), KStandardGuiItem::yes(), KStandardGuiItem::no(), KStandardGuiItem::cancel(), Preferences::ASK_AUTO_START); } switch (option) { case KMessageBox::Yes: Preferences::setAutoStart(true); Preferences::setNoAutoStart(false); break; case KMessageBox::No: Preferences::setNoAutoStart(true); break; case KMessageBox::Cancel: default: return; } Preferences::self()->save(); } quitIf(0, true); } /****************************************************************************** * Display an error message for a fatal error. Prevent further actions since * the program state is unsafe. */ void KAlarmApp::displayFatalError(const QString& message) { if (!mFatalError) { mFatalError = 1; mFatalMessage = message; if (mInstance) QTimer::singleShot(0, mInstance, &KAlarmApp::quitFatal); } } /****************************************************************************** * Quit the program, once the fatal error message has been acknowledged. */ void KAlarmApp::quitFatal() { switch (mFatalError) { case 0: case 2: return; case 1: mFatalError = 2; KMessageBox::error(nullptr, mFatalMessage); // this is an application modal window mFatalError = 3; Q_FALLTHROUGH(); // fall through to '3' case 3: if (mInstance) mInstance->quitIf(1, true); break; } QTimer::singleShot(1000, this, &KAlarmApp::quitFatal); } /****************************************************************************** * Called by the alarm timer when the next alarm is due. * Also called when the execution queue has finished processing to check for the * next alarm. */ void KAlarmApp::checkNextDueAlarm() { if (!mAlarmsEnabled) return; // Find the first alarm due const KAEvent* nextEvent = AlarmCalendar::resources()->earliestAlarm(); if (!nextEvent) return; // there are no alarms pending const KADateTime nextDt = nextEvent->nextTrigger(KAEvent::ALL_TRIGGER).effectiveKDateTime(); const KADateTime now = KADateTime::currentDateTime(Preferences::timeSpec()); qint64 interval = now.msecsTo(nextDt); qCDebug(KALARM_LOG) << "KAlarmApp::checkNextDueAlarm: now:" << qPrintable(now.toString(QStringLiteral("%Y-%m-%d %H:%M %:Z"))) << ", next:" << qPrintable(nextDt.toString(QStringLiteral("%Y-%m-%d %H:%M %:Z"))) << ", due:" << interval; if (interval <= 0) { // Queue the alarm queueAlarmId(*nextEvent); qCDebug(KALARM_LOG) << "KAlarmApp::checkNextDueAlarm:" << nextEvent->id() << ": due now"; QTimer::singleShot(0, this, &KAlarmApp::processQueue); } else { // No alarm is due yet, so set timer to wake us when it's due. // Check for integer overflow before setting timer. #pragma message("TODO: use hibernation wakeup signal") #ifndef HIBERNATION_SIGNAL /* TODO: REPLACE THIS CODE WHEN A SYSTEM NOTIFICATION SIGNAL BECOMES * AVAILABLE FOR WAKEUP FROM HIBERNATION. * Re-evaluate the next alarm time every minute, in case the * system clock jumps. The most common case when the clock jumps * is when a laptop wakes from hibernation. If timers were left to * run, they would trigger late by the length of time the system * was asleep. */ if (interval > 60000) // 1 minute interval = 60000; #endif ++interval; // ensure we don't trigger just before the minute boundary if (interval > INT_MAX) interval = INT_MAX; qCDebug(KALARM_LOG) << "KAlarmApp::checkNextDueAlarm:" << nextEvent->id() << "wait" << interval/1000 << "seconds"; mAlarmTimer->start(static_cast(interval)); } } /****************************************************************************** * Called by the alarm timer when the next alarm is due. * Also called when the execution queue has finished processing to check for the * next alarm. */ void KAlarmApp::queueAlarmId(const KAEvent& event) { const EventId id(event); for (const ActionQEntry& entry : qAsConst(mActionQueue)) { - if (entry.function == EVENT_HANDLE && entry.eventId == id) + if (entry.action == QueuedAction::Handle && entry.eventId == id) return; // the alarm is already queued } - mActionQueue.enqueue(ActionQEntry(EVENT_HANDLE, id)); + mActionQueue.enqueue(ActionQEntry(QueuedAction::Handle, id)); } /****************************************************************************** * Start processing the execution queue. */ void KAlarmApp::startProcessQueue() { if (!mInitialised) { qCDebug(KALARM_LOG) << "KAlarmApp::startProcessQueue"; mInitialised = true; QTimer::singleShot(0, this, &KAlarmApp::processQueue); // process anything already queued } } /****************************************************************************** * The main processing loop for KAlarm. * All KAlarm operations involving opening or updating calendar files are called * from this loop to ensure that only one operation is active at any one time. * This precaution is necessary because KAlarm's activities are mostly * asynchronous, being in response to D-Bus calls from other programs or timer * events, any of which can be received in the middle of performing another * operation. If a calendar file is opened or updated while another calendar * operation is in progress, the program has been observed to hang, or the first * calendar call has failed with data loss - clearly unacceptable!! */ void KAlarmApp::processQueue() { if (mInitialised && !mProcessingQueue) { qCDebug(KALARM_LOG) << "KAlarmApp::processQueue"; mProcessingQueue = true; // Refresh alarms if that's been queued KAlarm::refreshAlarmsIfQueued(); // Process queued events while (!mActionQueue.isEmpty()) { - bool removeFromQueue = true; + // Can't add a new event until resources have been populated. + if (!Resources::allPopulated()) + { + // If resource population has timed out, discard all queued events. + if (mResourcesTimedOut) + { + qCCritical(KALARM_LOG) << "Error! Timeout reading calendars"; + mActionQueue.clear(); + } + break; + } + ActionQEntry& entry = mActionQueue.head(); + const bool findUniqueId = int(entry.action) & int(QueuedAction::FindId); + const bool exitAfter = int(entry.action) & int(QueuedAction::Exit); + const QueuedAction action = static_cast(int(entry.action) & int(QueuedAction::ActionMask)); + + bool ok = true; if (entry.eventId.isEmpty()) { // It's a new alarm - switch (entry.function) + switch (action) { - case EVENT_TRIGGER: + case QueuedAction::Trigger: execAlarm(entry.event, entry.event.firstAlarm(), false); break; - case EVENT_HANDLE: - // Can't add a new event until resources have been populated. - if (!Resources::allPopulated()) - { - // Keep the queued item unless resource population has timed out. - if (!mResourcesTimedOut) - removeFromQueue = false; - } - else - KAlarm::addEvent(entry.event, nullptr, nullptr, KAlarm::ALLOW_KORG_UPDATE | KAlarm::NO_RESOURCE_PROMPT); + case QueuedAction::Handle: + KAlarm::addEvent(entry.event, nullptr, nullptr, KAlarm::ALLOW_KORG_UPDATE | KAlarm::NO_RESOURCE_PROMPT); + break; + case QueuedAction::List: + { + const QStringList alarms = scheduledAlarmList(); + for (const QString& alarm : alarms) + std::cout << alarm.toUtf8().constData() << std::endl; break; - case EVENT_CANCEL: + } + default: break; } } else - handleEvent(entry.eventId, entry.function); - if (!removeFromQueue) - break; + { + ok = handleEvent(entry.eventId, action, findUniqueId); + if (!ok && exitAfter) + CommandOptions::printError(xi18nc("@info:shell", "%1: Event %2 not found, or not unique", mCommandOption, entry.eventId.eventId())); + } + + if (exitAfter) + { + mActionQueue.clear(); // ensure that quitIf() actually exits the program + quitIf(ok ? 0 : 1); + return; // quitIf() can sometimes return, despite calling exit() + } + mActionQueue.dequeue(); } // Purge the default archived alarms resource if it's time to do so if (mPurgeDaysQueued >= 0) { KAlarm::purgeArchive(mPurgeDaysQueued); mPurgeDaysQueued = -1; } // Now that the queue has been processed, quit if a quit was queued if (mPendingQuit) { if (quitIf(mPendingQuitCode)) return; // quitIf() can sometimes return, despite calling exit() } mProcessingQueue = false; // Schedule the application to be woken when the next alarm is due checkNextDueAlarm(); } } /****************************************************************************** * Called when a repeat-at-login alarm has been added externally. * Queues the alarm for triggering. * First, cancel any scheduled reminder or deferral for it, since these will be * superseded by the new at-login trigger. */ void KAlarmApp::atLoginEventAdded(const KAEvent& event) { KAEvent ev = event; if (!cancelReminderAndDeferral(ev)) { if (mAlarmsEnabled) { - mActionQueue.enqueue(ActionQEntry(EVENT_HANDLE, EventId(ev))); + mActionQueue.enqueue(ActionQEntry(QueuedAction::Handle, EventId(ev))); if (mInitialised) QTimer::singleShot(0, this, &KAlarmApp::processQueue); } } } /****************************************************************************** * Called when the system tray main window is closed. */ void KAlarmApp::removeWindow(TrayWindow*) { mTrayWindow = nullptr; } /****************************************************************************** * Display or close the system tray icon. */ bool KAlarmApp::displayTrayIcon(bool show, MainWindow* parent) { qCDebug(KALARM_LOG) << "KAlarmApp::displayTrayIcon"; static bool creating = false; if (show) { if (!mTrayWindow && !creating) { if (!QSystemTrayIcon::isSystemTrayAvailable()) return false; if (!MainWindow::count()) { // We have to have at least one main window to act // as parent to the system tray icon (even if the // window is hidden). creating = true; // prevent main window constructor from creating an additional tray icon parent = MainWindow::create(); creating = false; } mTrayWindow = new TrayWindow(parent ? parent : MainWindow::firstWindow()); connect(mTrayWindow, &TrayWindow::deleted, this, &KAlarmApp::trayIconToggled); Q_EMIT trayIconToggled(); if (!checkSystemTray()) quitIf(0); // exit the application if there are no open windows } } else { delete mTrayWindow; mTrayWindow = nullptr; } return true; } /****************************************************************************** * Check whether the system tray icon has been housed in the system tray. */ bool KAlarmApp::checkSystemTray() { if (!mTrayWindow) return true; if (QSystemTrayIcon::isSystemTrayAvailable() == mNoSystemTray) { qCDebug(KALARM_LOG) << "KAlarmApp::checkSystemTray: changed ->" << mNoSystemTray; mNoSystemTray = !mNoSystemTray; // Store the new setting in the config file, so that if KAlarm exits it will // restart with the correct default. KConfigGroup config(KSharedConfig::openConfig(), "General"); config.writeEntry("NoSystemTray", mNoSystemTray); config.sync(); // Update other settings slotShowInSystemTrayChanged(); } return !mNoSystemTray; } /****************************************************************************** * Return the main window associated with the system tray icon. */ MainWindow* KAlarmApp::trayMainWindow() const { return mTrayWindow ? mTrayWindow->assocMainWindow() : nullptr; } /****************************************************************************** * Called when the show-in-system-tray preference setting has changed, to show * or hide the system tray icon. */ void KAlarmApp::slotShowInSystemTrayChanged() { const bool newShowInSysTray = wantShowInSystemTray(); if (newShowInSysTray != mOldShowInSystemTray) { // The system tray run mode has changed ++mActiveCount; // prevent the application from quitting MainWindow* win = mTrayWindow ? mTrayWindow->assocMainWindow() : nullptr; delete mTrayWindow; // remove the system tray icon if it is currently shown mTrayWindow = nullptr; mOldShowInSystemTray = newShowInSysTray; if (newShowInSysTray) { // Show the system tray icon displayTrayIcon(true); } else { // Stop showing the system tray icon if (win && win->isHidden()) { if (MainWindow::count() > 1) delete win; else { win->setWindowState(win->windowState() | Qt::WindowMinimized); win->show(); } } } --mActiveCount; } } /****************************************************************************** * Called when the start-of-day time preference setting has changed. * Change alarm times for date-only alarms. */ void KAlarmApp::changeStartOfDay() { DateTime::setStartOfDay(Preferences::startOfDay()); KAEvent::setStartOfDay(Preferences::startOfDay()); AlarmCalendar::resources()->adjustStartOfDay(); } /****************************************************************************** * Called when the default alarm message font preference setting has changed. * Notify KAEvent. */ void KAlarmApp::slotMessageFontChanged(const QFont& font) { KAEvent::setDefaultFont(font); } /****************************************************************************** * Called when the working time preference settings have changed. * Notify KAEvent. */ void KAlarmApp::slotWorkTimeChanged(const QTime& start, const QTime& end, const QBitArray& days) { KAEvent::setWorkTime(days, start, end); } /****************************************************************************** * Called when the holiday region preference setting has changed. * Notify KAEvent. */ void KAlarmApp::slotHolidaysChanged(const KHolidays::HolidayRegion& holidays) { KAEvent::setHolidays(holidays); } /****************************************************************************** * Called when the date for February 29th recurrences has changed in the * preferences settings. */ void KAlarmApp::slotFeb29TypeChanged(Preferences::Feb29Type type) { KARecurrence::Feb29Type rtype; switch (type) { default: case Preferences::Feb29_None: rtype = KARecurrence::Feb29_None; break; case Preferences::Feb29_Feb28: rtype = KARecurrence::Feb29_Feb28; break; case Preferences::Feb29_Mar1: rtype = KARecurrence::Feb29_Mar1; break; } KARecurrence::setDefaultFeb29Type(rtype); } /****************************************************************************** * Return whether the program is configured to be running in the system tray. */ bool KAlarmApp::wantShowInSystemTray() const { return Preferences::showInSystemTray() && QSystemTrayIcon::isSystemTrayAvailable(); } /****************************************************************************** * Set a timeout for populating resources. */ void KAlarmApp::setResourcesTimeout() { QTimer::singleShot(AKONADI_TIMEOUT * 1000, this, &KAlarmApp::slotResourcesTimeout); } /****************************************************************************** * Called on a timeout to check whether resources have been populated. * If not, exit the program with code 1. */ void KAlarmApp::slotResourcesTimeout() { if (!Resources::allPopulated()) { // Resource population has timed out. mResourcesTimedOut = true; quitIf(1); } } /****************************************************************************** * Called when all resources have been created at startup. * Check whether there are any writable active calendars, and if not, warn the * user. * If alarms are being archived, check whether there is a default archived * calendar, and if not, warn the user. */ void KAlarmApp::slotResourcesCreated() { if (mRedisplayAlarms) { mRedisplayAlarms = false; MessageWin::redisplayAlarms(); } checkWritableCalendar(); checkArchivedCalendar(); } /****************************************************************************** * Called when all calendars have been fetched at startup, or calendar migration * has completed. * Check whether there are any writable active calendars, and if not, warn the * user. */ void KAlarmApp::checkWritableCalendar() { if (mReadOnly) return; // don't need write access to calendars if (!Resources::allCreated() || !DataModel::isMigrationComplete()) return; static bool done = false; if (done) return; done = true; qCDebug(KALARM_LOG) << "KAlarmApp::checkWritableCalendar"; // Check for, and remove, any duplicate resources, i.e. those which use the // same calendar file/directory. DataModel::removeDuplicateResources(); // Find whether there are any writable active alarm calendars const bool active = !Resources::enabledResources(CalEvent::ACTIVE, true).isEmpty(); if (!active) { qCWarning(KALARM_LOG) << "KAlarmApp::checkWritableCalendar: No writable active calendar"; KAMessageBox::information(MainWindow::mainMainWindow(), xi18nc("@info", "Alarms cannot be created or updated, because no writable active alarm calendar is enabled." "To fix this, use View | Show Calendars to check or change calendar statuses."), QString(), QStringLiteral("noWritableCal")); } } /****************************************************************************** * If alarms are being archived, check whether there is a default archived * calendar, and if not, warn the user. */ void KAlarmApp::checkArchivedCalendar() { static bool done = false; if (done) return; done = true; // If alarms are to be archived, check that the default archived alarm // calendar is writable. if (Preferences::archivedKeepDays()) { Resource standard = Resources::getStandard(CalEvent::ARCHIVED); if (!standard.isValid()) { // Schedule the display of a user prompt, without holding up // other processing. QTimer::singleShot(0, this, &KAlarmApp::promptArchivedCalendar); } } } /****************************************************************************** * If alarms are being archived, check whether there is a default archived * calendar, and if not, warn the user. */ void KAlarmApp::promptArchivedCalendar() { const bool archived = !Resources::enabledResources(CalEvent::ARCHIVED, true).isEmpty(); if (archived) { qCWarning(KALARM_LOG) << "KAlarmApp::checkArchivedCalendar: Archiving, but no writable archived calendar"; KAMessageBox::information(MainWindow::mainMainWindow(), xi18nc("@info", "Alarms are configured to be archived, but this is not possible because no writable archived alarm calendar is enabled." "To fix this, use View | Show Calendars to check or change calendar statuses."), QString(), QStringLiteral("noWritableArch")); } else { qCWarning(KALARM_LOG) << "KAlarmApp::checkArchivedCalendar: Archiving, but no standard archived calendar"; KAMessageBox::information(MainWindow::mainMainWindow(), xi18nc("@info", "Alarms are configured to be archived, but this is not possible because no archived alarm calendar is set as default." "To fix this, use View | Show Calendars, select an archived alarms calendar, and check Use as Default for Archived Alarms."), QString(), QStringLiteral("noStandardArch")); } } /****************************************************************************** * Called when a new resource has been added, to note the possible need to purge * its old alarms if it is the default archived calendar. */ void KAlarmApp::slotResourceAdded(const Resource& resource) { if (resource.alarmTypes() & CalEvent::ARCHIVED) mPendingPurges += resource.id(); } /****************************************************************************** * Called when a resource has been populated, to purge its old alarms if it is * the default archived calendar. */ void KAlarmApp::slotResourcePopulated(const Resource& resource) { if (mPendingPurges.removeAll(resource.id()) > 0) purgeNewArchivedDefault(resource); } /****************************************************************************** * Called when a new resource has been populated, or when a resource has been * set as the standard resource for its type. * If it is the default archived calendar, purge its old alarms if necessary. */ void KAlarmApp::purgeNewArchivedDefault(const Resource& resource) { if (Resources::isStandard(resource, CalEvent::ARCHIVED)) { qCDebug(KALARM_LOG) << "KAlarmApp::purgeNewArchivedDefault:" << resource.displayId() << ": standard archived..."; if (mArchivedPurgeDays >= 0) purge(mArchivedPurgeDays); else setArchivePurgeDays(); } } /****************************************************************************** * Called when the length of time to keep archived alarms changes in KAlarm's * preferences. * Set the number of days to keep archived alarms. * Alarms which are older are purged immediately, and at the start of each day. */ void KAlarmApp::setArchivePurgeDays() { const int newDays = Preferences::archivedKeepDays(); if (newDays != mArchivedPurgeDays) { const int oldDays = mArchivedPurgeDays; mArchivedPurgeDays = newDays; if (mArchivedPurgeDays <= 0) StartOfDayTimer::disconnect(this); if (mArchivedPurgeDays < 0) return; // keep indefinitely, so don't purge if (oldDays < 0 || mArchivedPurgeDays < oldDays) { // Alarms are now being kept for less long, so purge them purge(mArchivedPurgeDays); if (!mArchivedPurgeDays) return; // don't archive any alarms } // Start the purge timer to expire at the start of the next day // (using the user-defined start-of-day time). StartOfDayTimer::connect(this, SLOT(slotPurge())); } } /****************************************************************************** * Purge all archived events from the calendar whose end time is longer ago than * 'daysToKeep'. All events are deleted if 'daysToKeep' is zero. */ void KAlarmApp::purge(int daysToKeep) { if (mPurgeDaysQueued < 0 || daysToKeep < mPurgeDaysQueued) mPurgeDaysQueued = daysToKeep; // Do the purge once any other current operations are completed processQueue(); } /****************************************************************************** * Output a list of pending alarms, with their next scheduled occurrence. */ QStringList KAlarmApp::scheduledAlarmList() { QStringList alarms; const QVector events = KAlarm::getSortedActiveEvents(this); for (const KAEvent& event : events) { const KADateTime dateTime = event.nextTrigger(KAEvent::DISPLAY_TRIGGER).effectiveKDateTime().toLocalZone(); const Resource resource = Resources::resource(event.resourceId()); QString text(resource.configName() + QLatin1String(":")); text += event.id() + QLatin1Char(' ') + dateTime.toString(QStringLiteral("%Y%m%dT%H%M ")) + AlarmText::summary(event, 1); alarms << text; } return alarms; } /****************************************************************************** * Enable or disable alarm monitoring. */ void KAlarmApp::setAlarmsEnabled(bool enabled) { if (enabled != mAlarmsEnabled) { mAlarmsEnabled = enabled; Q_EMIT alarmEnabledToggled(enabled); if (!enabled) KAlarm::cancelRtcWake(nullptr); else if (!mProcessingQueue) checkNextDueAlarm(); } } /****************************************************************************** * Spread or collect alarm message and error message windows. */ void KAlarmApp::spreadWindows(bool spread) { spread = MessageWin::spread(spread); Q_EMIT spreadWindowsToggled(spread); } /****************************************************************************** * Called when the spread status of message windows changes. * Set the 'spread windows' action state. */ void KAlarmApp::setSpreadWindowsState(bool spread) { Q_EMIT spreadWindowsToggled(spread); } /****************************************************************************** * Check whether the window manager's handling of keyboard focus transfer * between application windows is broken. This is true for Ubuntu's Unity * desktop, where MessageWin windows steal keyboard focus from EditAlarmDlg * windows. */ bool KAlarmApp::windowFocusBroken() const { return mWindowFocusBroken; } /****************************************************************************** * Check whether window/keyboard focus currently needs to be fixed manually due * to the window manager not handling it correctly. This will occur if there are * both EditAlarmDlg and MessageWin windows currently active. */ bool KAlarmApp::needWindowFocusFix() const { return mWindowFocusBroken && MessageWin::instanceCount(true) && EditAlarmDlg::instanceCount(); } /****************************************************************************** * Called to schedule a new alarm, either in response to a DCOP notification or * to command line options. * Reply = true unless there was a parameter error or an error opening calendar file. */ bool KAlarmApp::scheduleEvent(KAEvent::SubAction action, const QString& text, const KADateTime& dateTime, int lateCancel, KAEvent::Flags flags, const QColor& bg, const QColor& fg, const QFont& font, const QString& audioFile, float audioVolume, int reminderMinutes, const KARecurrence& recurrence, const KCalendarCore::Duration& repeatInterval, int repeatCount, uint mailFromID, const KCalendarCore::Person::List& mailAddresses, const QString& mailSubject, const QStringList& mailAttachments) { qCDebug(KALARM_LOG) << "KAlarmApp::scheduleEvent:" << text; if (!dateTime.isValid()) return false; const KADateTime now = KADateTime::currentUtcDateTime(); if (lateCancel && dateTime < now.addSecs(-maxLateness(lateCancel))) return true; // alarm time was already archived too long ago KADateTime alarmTime = dateTime; // Round down to the nearest minute to avoid scheduling being messed up if (!dateTime.isDateOnly()) alarmTime.setTime(QTime(alarmTime.time().hour(), alarmTime.time().minute(), 0)); KAEvent event(alarmTime, text, bg, fg, font, action, lateCancel, flags, true); if (reminderMinutes) { const bool onceOnly = flags & KAEvent::REMINDER_ONCE; event.setReminder(reminderMinutes, onceOnly); } if (!audioFile.isEmpty()) event.setAudioFile(audioFile, audioVolume, -1, 0, (flags & KAEvent::REPEAT_SOUND) ? 0 : -1); if (!mailAddresses.isEmpty()) event.setEmail(mailFromID, mailAddresses, mailSubject, mailAttachments); event.setRecurrence(recurrence); event.setFirstRecurrence(); event.setRepetition(Repetition(repeatInterval, repeatCount - 1)); event.endChanges(); if (alarmTime <= now) { // Alarm is due for display already. // First execute it once without adding it to the calendar file. if (!mInitialised) - mActionQueue.enqueue(ActionQEntry(event, EVENT_TRIGGER)); + mActionQueue.enqueue(ActionQEntry(event, QueuedAction::Trigger)); else execAlarm(event, event.firstAlarm(), false); // If it's a recurring alarm, reschedule it for its next occurrence if (!event.recurs() || event.setNextOccurrence(now) == KAEvent::NO_OCCURRENCE) return true; // It has recurrences in the future } // Queue the alarm for insertion into the calendar file mActionQueue.enqueue(ActionQEntry(event)); if (mInitialised) QTimer::singleShot(0, this, &KAlarmApp::processQueue); return true; } /****************************************************************************** * Called in response to a D-Bus request to trigger or cancel an event. * Optionally display the event. Delete the event from the calendar file and * from every main window instance. */ -bool KAlarmApp::dbusHandleEvent(const EventId& eventID, EventFunc function) +bool KAlarmApp::dbusHandleEvent(const EventId& eventID, QueuedAction action) { qCDebug(KALARM_LOG) << "KAlarmApp::dbusHandleEvent:" << eventID; - mActionQueue.append(ActionQEntry(function, eventID)); + mActionQueue.append(ActionQEntry(action, eventID)); if (mInitialised) QTimer::singleShot(0, this, &KAlarmApp::processQueue); return true; } /****************************************************************************** * Called in response to a D-Bus request to list all pending alarms. */ QString KAlarmApp::dbusList() { qCDebug(KALARM_LOG) << "KAlarmApp::dbusList"; return scheduledAlarmList().join(QLatin1Char('\n')) + QLatin1Char('\n'); } /****************************************************************************** * Either: * a) Display the event and then delete it if it has no outstanding repetitions. * b) Delete the event. * c) Reschedule the event for its next repetition. If none remain, delete it. * If the event is deleted, it is removed from the calendar file and from every * main window instance. * If 'findUniqueId' is true and 'id' does not specify a resource, all resources * will be searched for the event's unique ID. * Reply = false if event ID not found, or if more than one event with the same * ID is found. */ -bool KAlarmApp::handleEvent(const EventId& id, EventFunc function, bool findUniqueId) +bool KAlarmApp::handleEvent(const EventId& id, QueuedAction action, bool findUniqueId) { + Q_ASSERT(!(int(action) & ~int(QueuedAction::ActionMask))); + // Delete any expired wake-on-suspend config data KAlarm::checkRtcWakeConfig(); const QString eventID(id.eventId()); KAEvent* event = AlarmCalendar::resources()->event(id, findUniqueId); if (!event) { if (id.resourceId() != -1) qCWarning(KALARM_LOG) << "KAlarmApp::handleEvent: Event ID not found, or duplicated:" << eventID; else qCWarning(KALARM_LOG) << "KAlarmApp::handleEvent: Event ID not found:" << eventID; return false; } - switch (function) + switch (action) { - case EVENT_CANCEL: + case QueuedAction::Cancel: qCDebug(KALARM_LOG) << "KAlarmApp::handleEvent:" << eventID << ", CANCEL"; KAlarm::deleteEvent(*event, true); break; - case EVENT_TRIGGER: // handle it if it's due, else execute it regardless - case EVENT_HANDLE: // handle it if it's due + case QueuedAction::Trigger: // handle it if it's due, else execute it regardless + case QueuedAction::Handle: // handle it if it's due { const KADateTime now = KADateTime::currentUtcDateTime(); - qCDebug(KALARM_LOG) << "KAlarmApp::handleEvent:" << eventID << "," << (function==EVENT_TRIGGER?"TRIGGER:":"HANDLE:") << qPrintable(now.qDateTime().toString(QStringLiteral("yyyy-MM-dd hh:mm"))) << "UTC"; + qCDebug(KALARM_LOG) << "KAlarmApp::handleEvent:" << eventID << "," << (action==QueuedAction::Trigger?"TRIGGER:":"HANDLE:") << qPrintable(now.qDateTime().toString(QStringLiteral("yyyy-MM-dd hh:mm"))) << "UTC"; bool updateCalAndDisplay = false; bool alarmToExecuteValid = false; KAAlarm alarmToExecute; bool restart = false; // Check all the alarms in turn. // Note that the main alarm is fetched before any other alarms. for (KAAlarm alarm = event->firstAlarm(); alarm.isValid(); alarm = (restart ? event->firstAlarm() : event->nextAlarm(alarm)), restart = false) { // Check if the alarm is due yet. const KADateTime nextDT = alarm.dateTime(true).effectiveKDateTime(); const int secs = nextDT.secsTo(now); if (secs < 0) { // The alarm appears to be in the future. // Check if it's an invalid local time during a daylight // saving time shift, which has actually passed. if (alarm.dateTime().timeSpec() != KADateTime::LocalZone || nextDT > now.toTimeSpec(KADateTime::LocalZone)) { // This alarm is definitely not due yet qCDebug(KALARM_LOG) << "KAlarmApp::handleEvent: Alarm" << alarm.type() << "at" << nextDT.qDateTime() << ": not due"; continue; } } bool reschedule = false; bool rescheduleWork = false; if ((event->workTimeOnly() || event->holidaysExcluded()) && !alarm.deferred()) { // The alarm is restricted to working hours and/or non-holidays // (apart from deferrals). This needs to be re-evaluated every // time it triggers, since working hours could change. if (alarm.dateTime().isDateOnly()) { KADateTime dt(nextDT); dt.setDateOnly(true); reschedule = !event->isWorkingTime(dt); } else reschedule = !event->isWorkingTime(nextDT); rescheduleWork = reschedule; if (reschedule) qCDebug(KALARM_LOG) << "KAlarmApp::handleEvent: Alarm" << alarm.type() << "at" << nextDT.qDateTime() << ": not during working hours"; } if (!reschedule && alarm.repeatAtLogin()) { // Alarm is to be displayed at every login. qCDebug(KALARM_LOG) << "KAlarmApp::handleEvent: REPEAT_AT_LOGIN"; // Check if the main alarm is already being displayed. // (We don't want to display both at the same time.) if (alarmToExecute.isValid()) continue; // Set the time to display if it's a display alarm alarm.setTime(now); } if (!reschedule && event->lateCancel()) { // Alarm is due, and it is to be cancelled if too late. qCDebug(KALARM_LOG) << "KAlarmApp::handleEvent: LATE_CANCEL"; bool cancel = false; if (alarm.dateTime().isDateOnly()) { // The alarm has no time, so cancel it if its date is too far past const int maxlate = event->lateCancel() / 1440; // maximum lateness in days KADateTime limit(DateTime(nextDT.addDays(maxlate + 1)).effectiveKDateTime()); if (now >= limit) { // It's too late to display the scheduled occurrence. // Find the last previous occurrence of the alarm. DateTime next; const KAEvent::OccurType type = event->previousOccurrence(now, next, true); switch (type & ~KAEvent::OCCURRENCE_REPEAT) { case KAEvent::FIRST_OR_ONLY_OCCURRENCE: case KAEvent::RECURRENCE_DATE: case KAEvent::RECURRENCE_DATE_TIME: case KAEvent::LAST_RECURRENCE: limit.setDate(next.date().addDays(maxlate + 1)); if (now >= limit) { if (type == KAEvent::LAST_RECURRENCE || (type == KAEvent::FIRST_OR_ONLY_OCCURRENCE && !event->recurs())) cancel = true; // last occurrence (and there are no repetitions) else reschedule = true; } break; case KAEvent::NO_OCCURRENCE: default: reschedule = true; break; } } } else { // The alarm is timed. Allow it to be the permitted amount late before cancelling it. const int maxlate = maxLateness(event->lateCancel()); if (secs > maxlate) { // It's over the maximum interval late. // Find the most recent occurrence of the alarm. DateTime next; const KAEvent::OccurType type = event->previousOccurrence(now, next, true); switch (type & ~KAEvent::OCCURRENCE_REPEAT) { case KAEvent::FIRST_OR_ONLY_OCCURRENCE: case KAEvent::RECURRENCE_DATE: case KAEvent::RECURRENCE_DATE_TIME: case KAEvent::LAST_RECURRENCE: if (next.effectiveKDateTime().secsTo(now) > maxlate) { if (type == KAEvent::LAST_RECURRENCE || (type == KAEvent::FIRST_OR_ONLY_OCCURRENCE && !event->recurs())) cancel = true; // last occurrence (and there are no repetitions) else reschedule = true; } break; case KAEvent::NO_OCCURRENCE: default: reschedule = true; break; } } } if (cancel) { // All recurrences are finished, so cancel the event event->setArchive(); if (cancelAlarm(*event, alarm.type(), false)) return true; // event has been deleted updateCalAndDisplay = true; continue; } } if (reschedule) { // The latest repetition was too long ago, so schedule the next one switch (rescheduleAlarm(*event, alarm, false, (rescheduleWork ? nextDT : KADateTime()))) { case 1: // A working-time-only alarm has been rescheduled and the // rescheduled time is already due. Start processing the // event again. alarmToExecuteValid = false; restart = true; break; case -1: return true; // event has been deleted default: break; } updateCalAndDisplay = true; continue; } if (!alarmToExecuteValid) { qCDebug(KALARM_LOG) << "KAlarmApp::handleEvent: Alarm" << alarm.type() << ": execute"; alarmToExecute = alarm; // note the alarm to be displayed alarmToExecuteValid = true; // only trigger one alarm for the event } else qCDebug(KALARM_LOG) << "KAlarmApp::handleEvent: Alarm" << alarm.type() << ": skip"; } // If there is an alarm to execute, do this last after rescheduling/cancelling // any others. This ensures that the updated event is only saved once to the calendar. if (alarmToExecute.isValid()) execAlarm(*event, alarmToExecute, true, !alarmToExecute.repeatAtLogin()); else { - if (function == EVENT_TRIGGER) + if (action == QueuedAction::Trigger) { // The alarm is to be executed regardless of whether it's due. // Only trigger one alarm from the event - we don't want multiple // identical messages, for example. const KAAlarm alarm = event->firstAlarm(); if (alarm.isValid()) execAlarm(*event, alarm, false); } if (updateCalAndDisplay) KAlarm::updateEvent(*event); // update the window lists and calendar file - else if (function != EVENT_TRIGGER) { qCDebug(KALARM_LOG) << "KAlarmApp::handleEvent: No action"; } + else if (action != QueuedAction::Trigger) { qCDebug(KALARM_LOG) << "KAlarmApp::handleEvent: No action"; } } break; } + default: + break; } return true; } /****************************************************************************** * Called when an alarm action has completed, to perform any post-alarm actions. */ void KAlarmApp::alarmCompleted(const KAEvent& event) { if (!event.postAction().isEmpty()) { // doShellCommand() will error if the user is not authorised to run // shell commands. const QString command = event.postAction(); qCDebug(KALARM_LOG) << "KAlarmApp::alarmCompleted:" << event.id() << ":" << command; doShellCommand(command, event, nullptr, ProcData::POST_ACTION); } } /****************************************************************************** * Reschedule the alarm for its next recurrence after now. If none remain, * delete it. If the alarm is deleted and it is the last alarm for its event, * the event is removed from the calendar file and from every main window * instance. * If 'nextDt' is valid, the event is rescheduled for the next non-working * time occurrence after that. * Reply = 1 if 'nextDt' is valid and the rescheduled event is already due * = -1 if the event has been deleted * = 0 otherwise. */ int KAlarmApp::rescheduleAlarm(KAEvent& event, const KAAlarm& alarm, bool updateCalAndDisplay, const KADateTime& nextDt) { qCDebug(KALARM_LOG) << "KAlarmApp::rescheduleAlarm: Alarm type:" << alarm.type(); int reply = 0; bool update = false; event.startChanges(); if (alarm.repeatAtLogin()) { // Leave an alarm which repeats at every login until its main alarm triggers if (!event.reminderActive() && event.reminderMinutes() < 0) { // Executing an at-login alarm: first schedule the reminder // which occurs AFTER the main alarm. event.activateReminderAfter(KADateTime::currentUtcDateTime()); } // Repeat-at-login alarms are usually unchanged after triggering. // Ensure that the archive flag (which was set in execAlarm()) is saved. update = true; } else if (alarm.isReminder() || alarm.deferred()) { // It's a reminder alarm or an extra deferred alarm, so delete it event.removeExpiredAlarm(alarm.type()); update = true; } else { // Reschedule the alarm for its next occurrence. bool cancelled = false; DateTime last = event.mainDateTime(false); // note this trigger time if (last != event.mainDateTime(true)) last = DateTime(); // but ignore sub-repetition triggers bool next = nextDt.isValid(); KADateTime next_dt = nextDt; const KADateTime now = KADateTime::currentUtcDateTime(); do { const KAEvent::OccurType type = event.setNextOccurrence(next ? next_dt : now); switch (type) { case KAEvent::NO_OCCURRENCE: // All repetitions are finished, so cancel the event qCDebug(KALARM_LOG) << "KAlarmApp::rescheduleAlarm: No occurrence"; if (event.reminderMinutes() < 0 && last.isValid() && alarm.type() != KAAlarm::AT_LOGIN_ALARM && !event.mainExpired()) { // Set the reminder which is now due after the last main alarm trigger. // Note that at-login reminders are scheduled in execAlarm(). event.activateReminderAfter(last); updateCalAndDisplay = true; } if (cancelAlarm(event, alarm.type(), updateCalAndDisplay)) return -1; break; default: if (!(type & KAEvent::OCCURRENCE_REPEAT)) break; // Next occurrence is a repeat, so fall through to recurrence handling Q_FALLTHROUGH(); case KAEvent::RECURRENCE_DATE: case KAEvent::RECURRENCE_DATE_TIME: case KAEvent::LAST_RECURRENCE: // The event is due by now and repetitions still remain, so rewrite the event if (updateCalAndDisplay) update = true; break; case KAEvent::FIRST_OR_ONLY_OCCURRENCE: // The first occurrence is still due?!?, so don't do anything break; } if (cancelled) break; if (event.deferred()) { // Just in case there's also a deferred alarm, ensure it's removed event.removeExpiredAlarm(KAAlarm::DEFERRED_ALARM); update = true; } if (next) { // The alarm is restricted to working hours and/or non-holidays. // Check if the calculated next time is valid. next_dt = event.mainDateTime(true).effectiveKDateTime(); if (event.mainDateTime(false).isDateOnly()) { KADateTime dt(next_dt); dt.setDateOnly(true); next = !event.isWorkingTime(dt); } else next = !event.isWorkingTime(next_dt); } } while (next && next_dt <= now); reply = (!cancelled && next_dt.isValid() && (next_dt <= now)) ? 1 : 0; if (event.reminderMinutes() < 0 && last.isValid() && alarm.type() != KAAlarm::AT_LOGIN_ALARM) { // Set the reminder which is now due after the last main alarm trigger. // Note that at-login reminders are scheduled in execAlarm(). event.activateReminderAfter(last); } } event.endChanges(); if (update) KAlarm::updateEvent(event); // update the window lists and calendar file return reply; } /****************************************************************************** * Delete the alarm. If it is the last alarm for its event, the event is removed * from the calendar file and from every main window instance. * Reply = true if event has been deleted. */ bool KAlarmApp::cancelAlarm(KAEvent& event, KAAlarm::Type alarmType, bool updateCalAndDisplay) { qCDebug(KALARM_LOG) << "KAlarmApp::cancelAlarm"; if (alarmType == KAAlarm::MAIN_ALARM && !event.displaying() && event.toBeArchived()) { // The event is being deleted. Save it in the archived resources first. KAEvent ev(event); KAlarm::addArchivedEvent(ev); } event.removeExpiredAlarm(alarmType); if (!event.alarmCount()) { // If it's a command alarm being executed, mark it as deleted ProcData* pd = findCommandProcess(event.id()); if (pd) pd->eventDeleted = true; // Delete it KAlarm::deleteEvent(event, false); return true; } if (updateCalAndDisplay) KAlarm::updateEvent(event); // update the window lists and calendar file return false; } /****************************************************************************** * Cancel any reminder or deferred alarms in an repeat-at-login event. * This should be called when the event is first loaded. * If there are no more alarms left in the event, the event is removed from the * calendar file and from every main window instance. * Reply = true if event has been deleted. */ bool KAlarmApp::cancelReminderAndDeferral(KAEvent& event) { return cancelAlarm(event, KAAlarm::REMINDER_ALARM, false) || cancelAlarm(event, KAAlarm::DEFERRED_REMINDER_ALARM, false) || cancelAlarm(event, KAAlarm::DEFERRED_ALARM, true); } /****************************************************************************** * Execute an alarm by displaying its message or file, or executing its command. * Reply = ShellProcess instance if a command alarm * = MessageWin if an audio alarm * != 0 if successful * = -1 if execution has not completed * = 0 if the alarm is disabled, or if an error message was output. */ void* KAlarmApp::execAlarm(KAEvent& event, const KAAlarm& alarm, bool reschedule, bool allowDefer, bool noPreAction) { if (!mAlarmsEnabled || !event.enabled()) { // The event (or all events) is disabled qCDebug(KALARM_LOG) << "KAlarmApp::execAlarm:" << event.id() << ": disabled"; if (reschedule) rescheduleAlarm(event, alarm, true); return nullptr; } void* result = (void*)1; event.setArchive(); switch (alarm.action()) { case KAAlarm::COMMAND: if (!event.commandDisplay()) { // execCommandAlarm() will error if the user is not authorised // to run shell commands. result = execCommandAlarm(event, alarm); if (reschedule) rescheduleAlarm(event, alarm, true); break; } Q_FALLTHROUGH(); // fall through to MESSAGE case KAAlarm::MESSAGE: case KAAlarm::FILE: { // Display a message, file or command output, provided that the same event // isn't already being displayed MessageWin* win = MessageWin::findEvent(EventId(event)); // Find if we're changing a reminder message to the real message const bool reminder = (alarm.type() & KAAlarm::REMINDER_ALARM); const bool replaceReminder = !reminder && win && (win->alarmType() & KAAlarm::REMINDER_ALARM); if (!reminder && (!event.deferred() || (event.extraActionOptions() & KAEvent::ExecPreActOnDeferral)) && (replaceReminder || !win) && !noPreAction && !event.preAction().isEmpty()) { // It's not a reminder alarm, and it's not a deferred alarm unless the // pre-alarm action applies to deferred alarms, and there is no message // window (other than a reminder window) currently displayed for this // alarm, and we need to execute a command before displaying the new window. // // NOTE: The pre-action is not executed for a recurring alarm if an // alarm message window for a previous occurrence is still visible. // Check whether the command is already being executed for this alarm. for (const ProcData* pd : qAsConst(mCommandProcesses)) { if (pd->event->id() == event.id() && (pd->flags & ProcData::PRE_ACTION)) { qCDebug(KALARM_LOG) << "KAlarmApp::execAlarm: Already executing pre-DISPLAY command"; return pd->process; // already executing - don't duplicate the action } } // doShellCommand() will error if the user is not authorised to run // shell commands. const QString command = event.preAction(); qCDebug(KALARM_LOG) << "KAlarmApp::execAlarm: Pre-DISPLAY command:" << command; const int flags = (reschedule ? ProcData::RESCHEDULE : 0) | (allowDefer ? ProcData::ALLOW_DEFER : 0); if (doShellCommand(command, event, &alarm, (flags | ProcData::PRE_ACTION))) { AlarmCalendar::resources()->setAlarmPending(&event); return result; // display the message after the command completes } // Error executing command if (event.extraActionOptions() & KAEvent::CancelOnPreActError) { // Cancel the rest of the alarm execution qCDebug(KALARM_LOG) << "KAlarmApp::execAlarm:" << event.id() << ": pre-action failed: cancelled"; if (reschedule) rescheduleAlarm(event, alarm, true); return nullptr; } // Display the message even though it failed } if (!win) { // There isn't already a message for this event const int flags = (reschedule ? 0 : MessageWin::NO_RESCHEDULE) | (allowDefer ? 0 : MessageWin::NO_DEFER); (new MessageWin(&event, alarm, flags))->show(); } else if (replaceReminder) { // The caption needs to be changed from "Reminder" to "Message" win->cancelReminder(event, alarm); } else if (!win->hasDefer() && !alarm.repeatAtLogin()) { // It's a repeat-at-login message with no Defer button, // which has now reached its final trigger time and needs // to be replaced with a new message. win->showDefer(); win->showDateTime(event, alarm); } else { // Use the existing message window } if (win) { // Raise the existing message window and replay any sound win->repeat(alarm); // N.B. this reschedules the alarm } break; } case KAAlarm::EMAIL: { qCDebug(KALARM_LOG) << "KAlarmApp::execAlarm: EMAIL to:" << event.emailAddresses(QStringLiteral(",")); QStringList errmsgs; KAMail::JobData data(event, alarm, reschedule, (reschedule || allowDefer)); data.queued = true; int ans = KAMail::send(data, errmsgs); if (ans) { // The email has either been sent or failed - not queued if (ans < 0) result = nullptr; // failure data.queued = false; emailSent(data, errmsgs, (ans > 0)); } else { result = (void*)-1; // email has been queued } if (reschedule) rescheduleAlarm(event, alarm, true); break; } case KAAlarm::AUDIO: { // Play the sound, provided that the same event // isn't already playing MessageWin* win = MessageWin::findEvent(EventId(event)); if (!win) { // There isn't already a message for this event. const int flags = (reschedule ? 0 : MessageWin::NO_RESCHEDULE) | MessageWin::ALWAYS_HIDE; win = new MessageWin(&event, alarm, flags); } else { // There's an existing message window: replay the sound win->repeat(alarm); // N.B. this reschedules the alarm } return win; } default: return nullptr; } return result; } /****************************************************************************** * Called when sending an email has completed. */ void KAlarmApp::emailSent(KAMail::JobData& data, const QStringList& errmsgs, bool copyerr) { if (!errmsgs.isEmpty()) { // Some error occurred, although the email may have been sent successfully if (errmsgs.count() > 1) qCDebug(KALARM_LOG) << "KAlarmApp::emailSent:" << (copyerr ? "Copy error:" : "Failed:") << errmsgs[1]; MessageWin::showError(data.event, data.alarm.dateTime(), errmsgs); } else if (data.queued) Q_EMIT execAlarmSuccess(); } /****************************************************************************** * Execute the command specified in a command alarm. * To connect to the output ready signals of the process, specify a slot to be * called by supplying 'receiver' and 'slot' parameters. */ ShellProcess* KAlarmApp::execCommandAlarm(const KAEvent& event, const KAAlarm& alarm, const QObject* receiver, const char* slot) { // doShellCommand() will error if the user is not authorised to run // shell commands. const int flags = (event.commandXterm() ? ProcData::EXEC_IN_XTERM : 0) | (event.commandDisplay() ? ProcData::DISP_OUTPUT : 0); const QString command = event.cleanText(); if (event.commandScript()) { // Store the command script in a temporary file for execution qCDebug(KALARM_LOG) << "KAlarmApp::execCommandAlarm: Script"; const QString tmpfile = createTempScriptFile(command, false, event, alarm); if (tmpfile.isEmpty()) { setEventCommandError(event, KAEvent::CMD_ERROR); return nullptr; } return doShellCommand(tmpfile, event, &alarm, (flags | ProcData::TEMP_FILE), receiver, slot); } else { qCDebug(KALARM_LOG) << "KAlarmApp::execCommandAlarm:" << command; return doShellCommand(command, event, &alarm, flags, receiver, slot); } } /****************************************************************************** * Execute a shell command line specified by an alarm. * If the PRE_ACTION bit of 'flags' is set, the alarm will be executed via * execAlarm() once the command completes, the execAlarm() parameters being * derived from the remaining bits in 'flags'. * 'flags' must contain the bit PRE_ACTION or POST_ACTION if and only if it is * a pre- or post-alarm action respectively. * To connect to the output ready signals of the process, specify a slot to be * called by supplying 'receiver' and 'slot' parameters. * * Note that if shell access is not authorised, the attempt to run the command * will be errored. */ ShellProcess* KAlarmApp::doShellCommand(const QString& command, const KAEvent& event, const KAAlarm* alarm, int flags, const QObject* receiver, const char* slot) { qCDebug(KALARM_LOG) << "KAlarmApp::doShellCommand:" << command << "," << event.id(); QIODevice::OpenMode mode = QIODevice::WriteOnly; QString cmd; QString tmpXtermFile; if (flags & ProcData::EXEC_IN_XTERM) { // Execute the command in a terminal window. cmd = composeXTermCommand(command, event, alarm, flags, tmpXtermFile); if (cmd.isEmpty()) { qCWarning(KALARM_LOG) << "KAlarmApp::doShellCommand: Command failed (no terminal selected)"; const QStringList errors{i18nc("@info", "Failed to execute command\n(no terminal selected for command alarms)")}; commandErrorMsg(nullptr, event, alarm, flags, errors); return nullptr; } } else { cmd = command; mode = QIODevice::ReadWrite; } ProcData* pd = nullptr; ShellProcess* proc = nullptr; if (!cmd.isEmpty()) { // Use ShellProcess, which automatically checks whether the user is // authorised to run shell commands. proc = new ShellProcess(cmd); proc->setEnv(QStringLiteral("KALARM_UID"), event.id(), true); proc->setOutputChannelMode(KProcess::MergedChannels); // combine stdout & stderr connect(proc, &ShellProcess::shellExited, this, &KAlarmApp::slotCommandExited); if ((flags & ProcData::DISP_OUTPUT) && receiver && slot) { connect(proc, SIGNAL(receivedStdout(ShellProcess*)), receiver, slot); connect(proc, SIGNAL(receivedStderr(ShellProcess*)), receiver, slot); } if (mode == QIODevice::ReadWrite && !event.logFile().isEmpty()) { // Output is to be appended to a log file. // Set up a logging process to write the command's output to. QString heading; if (alarm && alarm->dateTime().isValid()) { const QString dateTime = alarm->dateTime().formatLocale(); heading = QStringLiteral("\n******* KAlarm %1 *******\n").arg(dateTime); } else heading = QStringLiteral("\n******* KAlarm *******\n"); QFile logfile(event.logFile()); if (logfile.open(QIODevice::Append | QIODevice::Text)) { QTextStream out(&logfile); out << heading; logfile.close(); } proc->setStandardOutputFile(event.logFile(), QIODevice::Append); } pd = new ProcData(proc, new KAEvent(event), (alarm ? new KAAlarm(*alarm) : nullptr), flags); if (flags & ProcData::TEMP_FILE) pd->tempFiles += command; if (!tmpXtermFile.isEmpty()) pd->tempFiles += tmpXtermFile; mCommandProcesses.append(pd); if (proc->start(mode)) return proc; } // Error executing command - report it qCWarning(KALARM_LOG) << "KAlarmApp::doShellCommand: Command failed to start"; commandErrorMsg(proc, event, alarm, flags); if (pd) { mCommandProcesses.removeAt(mCommandProcesses.indexOf(pd)); delete pd; } return nullptr; } /****************************************************************************** * Compose a command line to execute the given command in a terminal window. * 'tempScriptFile' receives the name of a temporary script file which is * invoked by the command line, if applicable. * Reply = command line, or empty string if error. */ QString KAlarmApp::composeXTermCommand(const QString& command, const KAEvent& event, const KAAlarm* alarm, int flags, QString& tempScriptFile) const { qCDebug(KALARM_LOG) << "KAlarmApp::composeXTermCommand:" << command << "," << event.id(); tempScriptFile.clear(); QString cmd = Preferences::cmdXTermCommand(); if (cmd.isEmpty()) return QString(); // no terminal application is configured cmd.replace(QLatin1String("%t"), KAboutData::applicationData().displayName()); // set the terminal window title if (cmd.indexOf(QLatin1String("%C")) >= 0) { // Execute the command from a temporary script file if (flags & ProcData::TEMP_FILE) cmd.replace(QLatin1String("%C"), command); // the command is already calling a temporary file else { tempScriptFile = createTempScriptFile(command, true, event, *alarm); if (tempScriptFile.isEmpty()) return QString(); cmd.replace(QLatin1String("%C"), tempScriptFile); // %C indicates where to insert the command } } else if (cmd.indexOf(QLatin1String("%W")) >= 0) { // Execute the command from a temporary script file, // with a sleep after the command is executed tempScriptFile = createTempScriptFile(command + QLatin1String("\nsleep 86400\n"), true, event, *alarm); if (tempScriptFile.isEmpty()) return QString(); cmd.replace(QLatin1String("%W"), tempScriptFile); // %w indicates where to insert the command } else if (cmd.indexOf(QLatin1String("%w")) >= 0) { // Append a sleep to the command. // Quote the command in case it contains characters such as [>|;]. const QString exec = KShell::quoteArg(command + QLatin1String("; sleep 86400")); cmd.replace(QLatin1String("%w"), exec); // %w indicates where to insert the command string } else { // Set the command to execute. // Put it in quotes in case it contains characters such as [>|;]. const QString exec = KShell::quoteArg(command); if (cmd.indexOf(QLatin1String("%c")) >= 0) cmd.replace(QLatin1String("%c"), exec); // %c indicates where to insert the command string else cmd.append(exec); // otherwise, simply append the command string } return cmd; } /****************************************************************************** * Create a temporary script file containing the specified command string. * Reply = path of temporary file, or null string if error. */ QString KAlarmApp::createTempScriptFile(const QString& command, bool insertShell, const KAEvent& event, const KAAlarm& alarm) const { QTemporaryFile tmpFile; tmpFile.setAutoRemove(false); // don't delete file when it is destructed if (!tmpFile.open()) qCCritical(KALARM_LOG) << "Unable to create a temporary script file"; else { tmpFile.setPermissions(QFile::ReadUser | QFile::WriteUser | QFile::ExeUser); QTextStream stream(&tmpFile); if (insertShell) stream << "#!" << ShellProcess::shellPath() << "\n"; stream << command; stream.flush(); if (tmpFile.error() != QFile::NoError) qCCritical(KALARM_LOG) << "Error" << tmpFile.errorString() << " writing to temporary script file"; else return tmpFile.fileName(); } const QStringList errmsgs(i18nc("@info", "Error creating temporary script file")); MessageWin::showError(event, alarm.dateTime(), errmsgs, QStringLiteral("Script")); return QString(); } /****************************************************************************** * Called when a command alarm's execution completes. */ void KAlarmApp::slotCommandExited(ShellProcess* proc) { qCDebug(KALARM_LOG) << "KAlarmApp::slotCommandExited"; // Find this command in the command list for (int i = 0, end = mCommandProcesses.count(); i < end; ++i) { ProcData* pd = mCommandProcesses.at(i); if (pd->process == proc) { // Found the command. Check its exit status. bool executeAlarm = pd->preAction(); const ShellProcess::Status status = proc->status(); if (status == ShellProcess::SUCCESS && !proc->exitCode()) { qCDebug(KALARM_LOG) << "KAlarmApp::slotCommandExited:" << pd->event->id() << ": SUCCESS"; clearEventCommandError(*pd->event, pd->preAction() ? KAEvent::CMD_ERROR_PRE : pd->postAction() ? KAEvent::CMD_ERROR_POST : KAEvent::CMD_ERROR); } else { QString errmsg = proc->errorMessage(); if (status == ShellProcess::SUCCESS || status == ShellProcess::NOT_FOUND) qCWarning(KALARM_LOG) << "KAlarmApp::slotCommandExited:" << pd->event->id() << ":" << errmsg << "exit status =" << status << ", code =" << proc->exitCode(); else qCWarning(KALARM_LOG) << "KAlarmApp::slotCommandExited:" << pd->event->id() << ":" << errmsg << "exit status =" << status; if (pd->messageBoxParent) { // Close the existing informational KMessageBox for this process const QList dialogs = pd->messageBoxParent->findChildren(); if (!dialogs.isEmpty()) delete dialogs[0]; setEventCommandError(*pd->event, pd->preAction() ? KAEvent::CMD_ERROR_PRE : pd->postAction() ? KAEvent::CMD_ERROR_POST : KAEvent::CMD_ERROR); if (!pd->tempFile()) { errmsg += QLatin1Char('\n'); errmsg += proc->command(); } KAMessageBox::error(pd->messageBoxParent, errmsg); } else commandErrorMsg(proc, *pd->event, pd->alarm, pd->flags); if (executeAlarm && (pd->event->extraActionOptions() & KAEvent::CancelOnPreActError)) { qCDebug(KALARM_LOG) << "KAlarmApp::slotCommandExited:" << pd->event->id() << ": pre-action failed: cancelled"; if (pd->reschedule()) rescheduleAlarm(*pd->event, *pd->alarm, true); executeAlarm = false; } } if (pd->preAction()) AlarmCalendar::resources()->setAlarmPending(pd->event, false); if (executeAlarm) execAlarm(*pd->event, *pd->alarm, pd->reschedule(), pd->allowDefer(), true); mCommandProcesses.removeAt(i); delete pd; break; } } // If there are now no executing shell commands, quit if a quit was queued if (mPendingQuit && mCommandProcesses.isEmpty()) quitIf(mPendingQuitCode); } /****************************************************************************** * Output an error message for a shell command, and record the alarm's error status. */ void KAlarmApp::commandErrorMsg(const ShellProcess* proc, const KAEvent& event, const KAAlarm* alarm, int flags, const QStringList& errors) { KAEvent::CmdErrType cmderr; QString dontShowAgain; QStringList errmsgs = errors; if (flags & ProcData::PRE_ACTION) { if (event.extraActionOptions() & KAEvent::DontShowPreActError) return; // don't notify user of any errors for the alarm errmsgs += i18nc("@info", "Pre-alarm action:"); dontShowAgain = QStringLiteral("Pre"); cmderr = KAEvent::CMD_ERROR_PRE; } else if (flags & ProcData::POST_ACTION) { errmsgs += i18nc("@info", "Post-alarm action:"); dontShowAgain = QStringLiteral("Post"); cmderr = (event.commandError() == KAEvent::CMD_ERROR_PRE) ? KAEvent::CMD_ERROR_PRE_POST : KAEvent::CMD_ERROR_POST; } else { dontShowAgain = QStringLiteral("Exec"); cmderr = KAEvent::CMD_ERROR; } // Record the alarm's error status setEventCommandError(event, cmderr); // Display an error message if (proc) { errmsgs += proc->errorMessage(); if (!(flags & ProcData::TEMP_FILE)) errmsgs += proc->command(); dontShowAgain += QString::number(proc->status()); } MessageWin::showError(event, (alarm ? alarm->dateTime() : DateTime()), errmsgs, dontShowAgain); } /****************************************************************************** * Notes that an informational KMessageBox is displayed for this process. */ void KAlarmApp::commandMessage(ShellProcess* proc, QWidget* parent) { // Find this command in the command list for (ProcData* pd : qAsConst(mCommandProcesses)) { if (pd->process == proc) { pd->messageBoxParent = parent; break; } } } /****************************************************************************** * If this is the first time through, open the calendar file, and start * processing the execution queue. */ bool KAlarmApp::initCheck(bool calendarOnly) { static bool firstTime = true; if (firstTime) qCDebug(KALARM_LOG) << "KAlarmApp::initCheck: first time"; if (initialiseTimerResources() || firstTime) { /* Need to open the display calendar now, since otherwise if display * alarms are immediately due, they will often be processed while * MessageWin::redisplayAlarms() is executing open() (but before open() * completes), which causes problems!! */ AlarmCalendar::displayCalendar()->open(); if (!AlarmCalendar::resources()->open()) return false; } if (firstTime) { setArchivePurgeDays(); firstTime = false; } if (!calendarOnly) startProcessQueue(); // start processing the execution queue return true; } /****************************************************************************** * Wait for one or all enabled resources to be populated. * Reply = true if successful. */ bool KAlarmApp::waitUntilPopulated(ResourceId id, int timeout) { qCDebug(KALARM_LOG) << "KAlarmApp::waitUntilPopulated" << id; const Resource res = Resources::resource(id); if ((id < 0 && !Resources::allPopulated()) || (id >= 0 && !res.isPopulated())) { // Use AutoQPointer to guard against crash on application exit while // the event loop is still running. It prevents double deletion (both // on deletion of parent, and on return from this function). AutoQPointer loop = new QEventLoop(DataModel::allAlarmListModel()); //TODO: The choice of parent object for QEventLoop can prevent EntityTreeModel signals // from activating connected slots in AkonadiDataModel, which prevents resources // from being informed that collections have loaded. Need to find a better parent // object - Qt item models seem to work, but what else? // These don't work: Resources::instance(), qApp(), theApp(), MainWindow::mainMainWindow(), AlarmCalendar::resources(), QStandardItemModel. // These do work: CollectionControlModel::instance(), AlarmListModel::all(). if (id < 0) connect(Resources::instance(), &Resources::resourcesPopulated, loop, &QEventLoop::quit); else connect(Resources::instance(), &Resources::resourcePopulated, [loop, &id](Resource& r) { if (r.id() == id) loop->quit(); }); if (timeout > 0) QTimer::singleShot(timeout * 1000, loop, &QEventLoop::quit); loop->exec(); } return (id < 0) ? Resources::allPopulated() : res.isPopulated(); } /****************************************************************************** * Called when an audio thread starts or stops. */ void KAlarmApp::notifyAudioPlaying(bool playing) { Q_EMIT audioPlaying(playing); } /****************************************************************************** * Stop audio play. */ void KAlarmApp::stopAudio() { MessageWin::stopAudio(); } /****************************************************************************** * Set the command error for the specified alarm. */ void KAlarmApp::setEventCommandError(const KAEvent& event, KAEvent::CmdErrType err) const { ProcData* pd = findCommandProcess(event.id()); if (pd && pd->eventDeleted) return; // the alarm has been deleted, so can't set error status if (err == KAEvent::CMD_ERROR_POST && event.commandError() == KAEvent::CMD_ERROR_PRE) err = KAEvent::CMD_ERROR_PRE_POST; event.setCommandError(err); KAEvent* ev = AlarmCalendar::resources()->event(EventId(event)); if (ev && ev->commandError() != err) ev->setCommandError(err); Resource resource = Resources::resourceForEvent(event.id()); resource.handleCommandErrorChange(event); } /****************************************************************************** * Clear the command error for the specified alarm. */ void KAlarmApp::clearEventCommandError(const KAEvent& event, KAEvent::CmdErrType err) const { ProcData* pd = findCommandProcess(event.id()); if (pd && pd->eventDeleted) return; // the alarm has been deleted, so can't set error status KAEvent::CmdErrType newerr = static_cast(event.commandError() & ~err); event.setCommandError(newerr); KAEvent* ev = AlarmCalendar::resources()->event(EventId(event)); if (ev) { newerr = static_cast(ev->commandError() & ~err); ev->setCommandError(newerr); } Resource resource = Resources::resourceForEvent(event.id()); resource.handleCommandErrorChange(event); } /****************************************************************************** * Find the currently executing command process for an event ID, if any. */ KAlarmApp::ProcData* KAlarmApp::findCommandProcess(const QString& eventId) const { for (ProcData* pd : qAsConst(mCommandProcesses)) { if (pd->event->id() == eventId) return pd; } return nullptr; } KAlarmApp::ProcData::ProcData(ShellProcess* p, KAEvent* e, KAAlarm* a, int f) : process(p) , event(e) , alarm(a) , messageBoxParent(nullptr) , flags(f) , eventDeleted(false) { } KAlarmApp::ProcData::~ProcData() { while (!tempFiles.isEmpty()) { // Delete the temporary file called by the XTerm command QFile f(tempFiles.first()); f.remove(); tempFiles.removeFirst(); } delete process; delete event; delete alarm; } // vim: et sw=4: diff --git a/src/kalarmapp.h b/src/kalarmapp.h index eeb780d6..e7f08e4a 100644 --- a/src/kalarmapp.h +++ b/src/kalarmapp.h @@ -1,247 +1,255 @@ /* * kalarmapp.h - the KAlarm application object * Program: kalarm * Copyright © 2001-2020 David Jarvie * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef KALARMAPP_H #define KALARMAPP_H /** @file kalarmapp.h - the KAlarm application object */ #include "eventid.h" #include "kamail.h" #include "preferences.h" #include #include #include #include #include namespace KCal { class Event; } class Resource; class DBusHandler; class MainWindow; class TrayWindow; class ShellProcess; using namespace KAlarmCal; class KAlarmApp : public QApplication { Q_OBJECT public: ~KAlarmApp() override; /** Create the unique instance. */ static KAlarmApp* create(int& argc, char** argv); /** Must be called to complete initialisation after KAboutData is set, * but before the application is activated or restored. */ void initialise(); /** Return the unique instance. */ static KAlarmApp* instance() { return mInstance; } bool checkCalendar() { return initCheck(); } bool wantShowInSystemTray() const; bool alarmsEnabled() const { return mAlarmsEnabled; } bool korganizerEnabled() const { return mKOrganizerEnabled; } int activate(const QStringList& args, const QString& workingDirectory, QString& outputText) { return activateInstance(args, workingDirectory, &outputText); } bool restoreSession(); bool quitIf() { return quitIf(0); } void doQuit(QWidget* parent); static void displayFatalError(const QString& message); void addWindow(TrayWindow* w) { mTrayWindow = w; } void removeWindow(TrayWindow*); TrayWindow* trayWindow() const { return mTrayWindow; } MainWindow* trayMainWindow() const; bool displayTrayIcon(bool show, MainWindow* = nullptr); bool trayIconDisplayed() const { return mTrayWindow; } bool editNewAlarm(MainWindow* = nullptr); void* execAlarm(KAEvent&, const KAAlarm&, bool reschedule, bool allowDefer = true, bool noPreAction = false); ShellProcess* execCommandAlarm(const KAEvent&, const KAAlarm&, const QObject* receiver = nullptr, const char* slot = nullptr); void alarmCompleted(const KAEvent&); void rescheduleAlarm(KAEvent& e, const KAAlarm& a) { rescheduleAlarm(e, a, true); } void purgeAll() { purge(0); } void commandMessage(ShellProcess*, QWidget* parent); void notifyAudioPlaying(bool playing); void setSpreadWindowsState(bool spread); bool windowFocusBroken() const; bool needWindowFocusFix() const; // Methods called indirectly by the DCOP interface bool scheduleEvent(KAEvent::SubAction, const QString& text, const KADateTime&, int lateCancel, KAEvent::Flags flags, const QColor& bg, const QColor& fg, const QFont&, const QString& audioFile, float audioVolume, int reminderMinutes, const KARecurrence& recurrence, const KCalendarCore::Duration& repeatInterval, int repeatCount, uint mailFromID = 0, const KCalendarCore::Person::List& mailAddresses = KCalendarCore::Person::List(), const QString& mailSubject = QString(), const QStringList& mailAttachments = QStringList()); - bool dbusTriggerEvent(const EventId& eventID) { return dbusHandleEvent(eventID, EVENT_TRIGGER); } - bool dbusDeleteEvent(const EventId& eventID) { return dbusHandleEvent(eventID, EVENT_CANCEL); } + bool dbusTriggerEvent(const EventId& eventID) { return dbusHandleEvent(eventID, QueuedAction::Trigger); } + bool dbusDeleteEvent(const EventId& eventID) { return dbusHandleEvent(eventID, QueuedAction::Cancel); } QString dbusList(); public Q_SLOTS: void activateByDBus(const QStringList& args, const QString& workingDirectory); void processQueue(); void setAlarmsEnabled(bool); void purgeNewArchivedDefault(const Resource&); void atLoginEventAdded(const KAEvent&); void notifyAudioStopped() { notifyAudioPlaying(false); } void stopAudio(); void spreadWindows(bool); void emailSent(KAMail::JobData&, const QStringList& errmsgs, bool copyerr = false); Q_SIGNALS: void trayIconToggled(); void alarmEnabledToggled(bool); void audioPlaying(bool); void spreadWindowsToggled(bool); void execAlarmSuccess(); private: typedef Preferences::Feb29Type Feb29Type; // allow it to be used in SIGNAL mechanism private Q_SLOTS: void quitFatal(); void checkNextDueAlarm(); void slotShowInSystemTrayChanged(); void changeStartOfDay(); void slotWorkTimeChanged(const QTime& start, const QTime& end, const QBitArray& days); void slotHolidaysChanged(const KHolidays::HolidayRegion&); void slotFeb29TypeChanged(Feb29Type); void slotResourcesTimeout(); void slotResourcesCreated(); void checkWritableCalendar(); void promptArchivedCalendar(); void slotMessageFontChanged(const QFont&); void setArchivePurgeDays(); void slotResourceAdded(const Resource&); void slotResourcePopulated(const Resource&); void slotPurge() { purge(mArchivedPurgeDays); } void slotCommandExited(ShellProcess*); private: - enum EventFunc + // Actions to execute in processQueue(). May be OR'ed together. + enum class QueuedAction { - EVENT_HANDLE, // if the alarm is due, execute it and then reschedule it - EVENT_TRIGGER, // execute the alarm regardless, and then reschedule it if it's already due - EVENT_CANCEL // delete the alarm + // Action to execute + ActionMask = 0x07, // bit mask to extract action to execute + Handle = 0x01, // if the alarm is due, execute it and then reschedule it + Trigger = 0x02, // execute the alarm regardless, and then reschedule it if it's already due + Cancel = 0x03, // delete the alarm + List = 0x04, // list all alarms + // Modifier flags + FindId = 0x10, // search all resources for unique event ID + Exit = 0x20 // exit application after executing action }; struct ProcData { ProcData(ShellProcess*, KAEvent*, KAAlarm*, int flags = 0); ~ProcData(); enum { PRE_ACTION = 0x01, POST_ACTION = 0x02, RESCHEDULE = 0x04, ALLOW_DEFER = 0x08, TEMP_FILE = 0x10, EXEC_IN_XTERM = 0x20, DISP_OUTPUT = 0x40 }; bool preAction() const { return flags & PRE_ACTION; } bool postAction() const { return flags & POST_ACTION; } bool reschedule() const { return flags & RESCHEDULE; } bool allowDefer() const { return flags & ALLOW_DEFER; } bool tempFile() const { return flags & TEMP_FILE; } bool execInXterm() const { return flags & EXEC_IN_XTERM; } bool dispOutput() const { return flags & DISP_OUTPUT; } ShellProcess* process; KAEvent* event; KAAlarm* alarm; QPointer messageBoxParent; QStringList tempFiles; int flags; bool eventDeleted; }; struct ActionQEntry { - ActionQEntry(EventFunc f, const EventId& id) : function(f), eventId(id) { } - ActionQEntry(const KAEvent& e, EventFunc f = EVENT_HANDLE) : function(f), event(e) { } + ActionQEntry(QueuedAction a, const EventId& id) : action(a), eventId(id) { } + ActionQEntry(const KAEvent& e, QueuedAction a = QueuedAction::Handle) : action(a), event(e) { } ActionQEntry() { } - EventFunc function; - EventId eventId; - KAEvent event; + QueuedAction action; + EventId eventId; + KAEvent event; }; KAlarmApp(int& argc, char** argv); bool initialiseTimerResources(); int activateInstance(const QStringList& args, const QString& workingDirectory, QString* outputText); bool initCheck(bool calendarOnly = false); bool waitUntilPopulated(ResourceId, int timeout); bool quitIf(int exitCode, bool force = false); void createOnlyMainWindow(); bool checkSystemTray(); void startProcessQueue(); void setResourcesTimeout(); void checkArchivedCalendar(); void queueAlarmId(const KAEvent&); - bool dbusHandleEvent(const EventId&, EventFunc); - bool handleEvent(const EventId&, EventFunc, bool findUniqueId = false); + bool dbusHandleEvent(const EventId&, QueuedAction); + bool handleEvent(const EventId&, QueuedAction, bool findUniqueId = false); int rescheduleAlarm(KAEvent&, const KAAlarm&, bool updateCalAndDisplay, const KADateTime& nextDt = KADateTime()); bool cancelAlarm(KAEvent&, KAAlarm::Type, bool updateCalAndDisplay); bool cancelReminderAndDeferral(KAEvent&); ShellProcess* doShellCommand(const QString& command, const KAEvent&, const KAAlarm*, int flags = 0, const QObject* receiver = nullptr, const char* slot = nullptr); QString composeXTermCommand(const QString& command, const KAEvent&, const KAAlarm*, int flags, QString& tempScriptFile) const; QString createTempScriptFile(const QString& command, bool insertShell, const KAEvent&, const KAAlarm&) const; void commandErrorMsg(const ShellProcess*, const KAEvent&, const KAAlarm*, int flags = 0, const QStringList& errmsgs = QStringList()); void purge(int daysToKeep); QStringList scheduledAlarmList(); void setEventCommandError(const KAEvent&, KAEvent::CmdErrType) const; void clearEventCommandError(const KAEvent&, KAEvent::CmdErrType) const; ProcData* findCommandProcess(const QString& eventId) const; static KAlarmApp* mInstance; // the one and only KAlarmApp instance static int mActiveCount; // number of active instances without main windows static int mFatalError; // a fatal error has occurred - just wait to exit static QString mFatalMessage; // fatal error message to output + QString mCommandOption; // command option used on command line bool mInitialised{false}; // initialisation complete: ready to process execution queue bool mRedisplayAlarms{false}; // need to redisplay alarms when collection tree fetched bool mQuitting{false}; // a forced quit is in progress bool mReadOnly{false}; // only read-only access to calendars is needed QString mActivateArg0; // activate()'s first arg the first time it was called DBusHandler* mDBusHandler; // the parent of the main DCOP receiver object TrayWindow* mTrayWindow{nullptr}; // active system tray icon QTimer* mAlarmTimer{nullptr}; // activates KAlarm when next alarm is due QColor mPrefsArchivedColour; // archived alarms text colour int mArchivedPurgeDays{-1}; // how long to keep archived alarms, 0 = don't keep, -1 = keep indefinitely int mPurgeDaysQueued{-1}; // >= 0 to purge the archive calendar from KAlarmApp::processLoop() QVector mPendingPurges; // new resources which may need to be purged when populated QList mCommandProcesses; // currently active command alarm processes QQueue mActionQueue; // queued commands and actions int mPendingQuitCode; // exit code for a pending quit bool mPendingQuit{false}; // quit once the DCOP command and shell command queues have been processed bool mCancelRtcWake{false}; // cancel RTC wake on quitting bool mProcessingQueue{false}; // a mActionQueue entry is currently being processed bool mNoSystemTray; // no system tray exists bool mOldShowInSystemTray; // showing in system tray was selected bool mAlarmsEnabled{true}; // alarms are enabled bool mKOrganizerEnabled; // KOrganizer options are enabled (korganizer exists) bool mWindowFocusBroken; // keyboard focus transfer between windows doesn't work bool mResourcesTimedOut{false}; // timeout has expired for populating resources }; inline KAlarmApp* theApp() { return KAlarmApp::instance(); } #endif // KALARMAPP_H // vim: et sw=4: diff --git a/src/resources/datamodel.cpp b/src/resources/datamodel.cpp index 5a96227d..6d99d2e8 100644 --- a/src/resources/datamodel.cpp +++ b/src/resources/datamodel.cpp @@ -1,100 +1,104 @@ /* * datamodel.cpp - calendar data model dependent functions * Program: kalarm * Copyright © 2019-2020 David Jarvie * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "datamodel.h" #include "akonadidatamodel.h" #include "akonadiresource.h" #include "akonadiresourcecreator.h" #include "akonadicalendarupdater.h" #include "eventmodel.h" #include "resourcemodel.h" namespace DataModel { void initialise() { AkonadiDataModel::instance(); } +void terminate() +{ +} + void reload() { AkonadiDataModel::instance()->reload(); } bool reload(Resource& resource) { return AkonadiDataModel::instance()->reload(resource); } bool isMigrationComplete() { return AkonadiDataModel::instance()->isMigrationComplete(); } void removeDuplicateResources() { AkonadiResource::removeDuplicateResources(); } ResourceListModel* createResourceListModel(QObject* parent) { return ResourceListModel::create(parent); } ResourceFilterCheckListModel* createResourceFilterCheckListModel(QObject* parent) { return ResourceFilterCheckListModel::create(parent); } AlarmListModel* createAlarmListModel(QObject* parent) { return AlarmListModel::create(parent); } AlarmListModel* allAlarmListModel() { return AlarmListModel::all(); } TemplateListModel* createTemplateListModel(QObject* parent) { return TemplateListModel::create(parent); } TemplateListModel* allTemplateListModel() { return TemplateListModel::all(); } ResourceCreator* createResourceCreator(KAlarmCal::CalEvent::Type defaultType, QWidget* parent) { return new AkonadiResourceCreator(defaultType, parent); } void updateCalendarToCurrentFormat(Resource& resource, bool ignoreKeepFormat, QObject* parent) { AkonadiCalendarUpdater::updateToCurrentFormat(resource, ignoreKeepFormat, parent); } } // namespace DataModel // vim: et sw=4: diff --git a/src/resources/datamodel.h b/src/resources/datamodel.h index f19c0cf7..d66f9528 100644 --- a/src/resources/datamodel.h +++ b/src/resources/datamodel.h @@ -1,72 +1,74 @@ /* * datamodel.h - calendar data model dependent functions * Program: kalarm * Copyright © 2019-2020 David Jarvie * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef DATAMODEL_H #define DATAMODEL_H #include class Resource; class ResourceListModel; class ResourceFilterCheckListModel; class AlarmListModel; class TemplateListModel; class ResourceCreator; class QObject; class QWidget; /** Class to create objects dependent on data model. */ namespace DataModel { void initialise(); +void terminate(); + /** Reload all resources' data from storage. * @note In the case of Akonadi, this does not reload from the backend storage. */ void reload(); /** Reload a resource's data from storage. * @note In the case of Akonadi, this does not reload from the backend storage. */ bool reload(Resource&); bool isMigrationComplete(); /** Check for, and remove, any duplicate Akonadi resources, i.e. those which * use the same calendar file/directory. */ void removeDuplicateResources(); ResourceListModel* createResourceListModel(QObject* parent); ResourceFilterCheckListModel* createResourceFilterCheckListModel(QObject* parent); AlarmListModel* createAlarmListModel(QObject* parent); AlarmListModel* allAlarmListModel(); TemplateListModel* createTemplateListModel(QObject* parent); TemplateListModel* allTemplateListModel(); ResourceCreator* createResourceCreator(KAlarmCal::CalEvent::Type defaultType, QWidget* parent); void updateCalendarToCurrentFormat(Resource&, bool ignoreKeepFormat, QObject* parent); } // namespace DataModel #endif // DATAMODEL_H // vim: et sw=4: diff --git a/src/resources/resource.cpp b/src/resources/resource.cpp index 1d767a11..267d605a 100644 --- a/src/resources/resource.cpp +++ b/src/resources/resource.cpp @@ -1,315 +1,321 @@ /* * resource.cpp - generic class containing an alarm calendar resource * Program: kalarm * Copyright © 2019-2020 David Jarvie * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "resource.h" Resource::Resource() : mResource() { } Resource::Resource(ResourceType* r) : mResource(r) { } Resource::~Resource() { } bool Resource::operator==(const Resource& other) const { return mResource == other.mResource; } bool Resource::operator==(const ResourceType* other) const { return mResource.data() == other; } Resource Resource::null() { static Resource nullResource(nullptr); return nullResource; } bool Resource::isNull() const { return mResource.isNull(); } bool Resource::isValid() const { return mResource.isNull() ? false : mResource->isValid(); } bool Resource::failed() const { return mResource.isNull() ? true : mResource->failed(); } #if 0 ResourceType::Ptr Resource::resource() const { return mResource; } #endif ResourceId Resource::id() const { return mResource.isNull() ? -1 : mResource->id(); } ResourceId Resource::displayId() const { return mResource.isNull() ? -1 : mResource->displayId(); } Resource::StorageType Resource::storageType() const { return mResource.isNull() ? NoStorage : static_cast(mResource->storageType()); } QString Resource::storageTypeString(bool description) const { return mResource.isNull() ? QString() : mResource->storageTypeString(description); } QString Resource::storageTypeString(ResourceType::StorageType type) { return ResourceType::storageTypeString(type); } QUrl Resource::location() const { return mResource.isNull() ? QUrl() : mResource->location(); } QString Resource::displayLocation() const { return mResource.isNull() ? QString() : mResource->displayLocation(); } QString Resource::displayName() const { return mResource.isNull() ? QString() : mResource->displayName(); } QString Resource::configName() const { return mResource.isNull() ? QString() : mResource->configName(); } CalEvent::Types Resource::alarmTypes() const { return mResource.isNull() ? CalEvent::EMPTY : mResource->alarmTypes(); } bool Resource::isEnabled(CalEvent::Type type) const { return mResource.isNull() ? false : mResource->isEnabled(type); } CalEvent::Types Resource::enabledTypes() const { return mResource.isNull() ? CalEvent::EMPTY : mResource->enabledTypes(); } void Resource::setEnabled(CalEvent::Type type, bool enabled) { if (!mResource.isNull()) mResource->setEnabled(type, enabled); } void Resource::setEnabled(CalEvent::Types types) { if (!mResource.isNull()) mResource->setEnabled(types); } bool Resource::readOnly() const { return mResource.isNull() ? true : mResource->readOnly(); } int Resource::writableStatus(CalEvent::Type type) const { return mResource.isNull() ? -1 : mResource->writableStatus(type); } bool Resource::isWritable(CalEvent::Type type) const { return mResource.isNull() ? false : mResource->isWritable(type); } bool Resource::keepFormat() const { return mResource.isNull() ? false : mResource->keepFormat(); } void Resource::setKeepFormat(bool keep) { if (!mResource.isNull()) mResource->setKeepFormat(keep); } QColor Resource::backgroundColour() const { return mResource.isNull() ? QColor() : mResource->backgroundColour(); } void Resource::setBackgroundColour(const QColor& colour) { if (!mResource.isNull()) mResource->setBackgroundColour(colour); } QColor Resource::foregroundColour(CalEvent::Types types) const { return mResource.isNull() ? QColor() : mResource->foregroundColour(types); } bool Resource::configIsStandard(CalEvent::Type type) const { return mResource.isNull() ? false : mResource->configIsStandard(type); } CalEvent::Types Resource::configStandardTypes() const { return mResource.isNull() ? CalEvent::EMPTY : mResource->configStandardTypes(); } void Resource::configSetStandard(CalEvent::Type type, bool standard) { if (!mResource.isNull()) mResource->configSetStandard(type, standard); } void Resource::configSetStandard(CalEvent::Types types) { if (!mResource.isNull()) mResource->configSetStandard(types); } bool Resource::isCompatible() const { return mResource.isNull() ? false : mResource->isCompatible(); } KACalendar::Compat Resource::compatibility() const { return mResource.isNull() ? KACalendar::Incompatible : mResource->compatibility(); } KACalendar::Compat Resource::compatibilityVersion(QString& versionString) const { if (mResource.isNull()) { versionString.clear(); return KACalendar::Incompatible; } return mResource->compatibilityVersion(versionString); } void Resource::editResource(QWidget* dialogParent) { if (!mResource.isNull()) mResource->editResource(dialogParent); } bool Resource::removeResource() { return mResource.isNull() ? false : mResource->removeResource(); } bool Resource::load(bool readThroughCache) { return mResource.isNull() ? false : mResource->load(readThroughCache); } bool Resource::reload() { return mResource.isNull() ? false : mResource->reload(); } bool Resource::isPopulated() const { return mResource.isNull() ? false : mResource->isPopulated(); } bool Resource::save(bool writeThroughCache) { return mResource.isNull() ? false : mResource->save(writeThroughCache); } bool Resource::isSaving() const { return mResource.isNull() ? false : mResource->isSaving(); } +void Resource::close() +{ + if (!mResource.isNull()) + mResource->close(); +} + QList Resource::events() const { return mResource.isNull() ? QList() : mResource->events(); } KAEvent Resource::event(const QString& eventId) const { return mResource.isNull() ? KAEvent() : mResource->event(eventId); } bool Resource::containsEvent(const QString& eventId) const { return mResource.isNull() ? false : mResource->containsEvent(eventId); } bool Resource::addEvent(const KAEvent& event) { return mResource.isNull() ? false : mResource->addEvent(event); } bool Resource::updateEvent(const KAEvent& event) { return mResource.isNull() ? false : mResource->updateEvent(event); } bool Resource::deleteEvent(const KAEvent& event) { return mResource.isNull() ? false : mResource->deleteEvent(event); } void Resource::handleCommandErrorChange(const KAEvent& event) { if (!mResource.isNull()) mResource->handleCommandErrorChange(event); } void Resource::notifyDeletion() { if (!mResource.isNull()) mResource->notifyDeletion(); } bool Resource::isBeingDeleted() const { return mResource.isNull() ? false : mResource->isBeingDeleted(); } // vim: et sw=4: diff --git a/src/resources/resource.h b/src/resources/resource.h index a111eb12..e0f0c59e 100644 --- a/src/resources/resource.h +++ b/src/resources/resource.h @@ -1,396 +1,401 @@ /* * resource.h - generic class containing an alarm calendar resource * Program: kalarm * Copyright © 2019-2020 David Jarvie * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef RESOURCE_H #define RESOURCE_H #include "resourcetype.h" using namespace KAlarmCal; /** Class for an alarm calendar resource. * It contains a shared pointer to an alarm calendar resource inherited from * ResourceType. * This class is designed to be safe to call even if the pointer to the resource * is null. */ class Resource { public: /** The type of storage used by a resource. */ enum StorageType { NoStorage = ResourceType::NoStorage, File = ResourceType::File, Directory = ResourceType::Directory }; Resource(); explicit Resource(ResourceType*); Resource(const Resource&) = default; ~Resource(); bool operator==(const Resource&) const; bool operator==(const ResourceType*) const; /** Return a null resource. */ static Resource null(); /** Return whether the resource has a null calendar resource pointer. */ bool isNull() const; /** Return whether the resource has a valid configuration. * Note that the resource may be unusable even if it has a valid * configuration: see failed(). */ bool isValid() const; /** Return whether the resource has a fatal error. * Note that failed() will return true if the configuration is invalid * (i.e. isValid() returns false). It will also return true if some other * error prevents the resource being used, e.g. if the calendar file * cannot be created. */ bool failed() const; /** Return the resource's unique ID. */ ResourceId id() const; /** Return the resource's unique ID, as shown to the user. */ ResourceId displayId() const; /** Return the type of storage used by the resource. */ StorageType storageType() const; /** Return the type of the resource (file, remote file, etc.) * for display purposes. * @param description true for description (e.g. "Remote file"), * false for brief label (e.g. "URL"). */ QString storageTypeString(bool description) const; /** Return the type description of a resource (file, remote file, etc.) * for display purposes. This is equivalent to storageTypeString(true). */ static QString storageTypeString(ResourceType::StorageType); /** Return the location(s) of the resource (URL, file path, etc.) */ QUrl location() const; /** Return the location of the resource (URL, file path, etc.) * for display purposes. */ QString displayLocation() const; /** Return the resource's display name. */ QString displayName() const; /** Return the resource's configuration identifier. This is not the * name normally displayed to the user. */ QString configName() const; /** Return which types of alarms the resource can contain. */ CalEvent::Types alarmTypes() const; /** Return whether the resource is enabled for a specified alarm type * (active, archived, template or displaying). * @param type alarm type to check for, or EMPTY to check for any type. */ bool isEnabled(CalEvent::Type type) const; /** Return which alarm types (active, archived or template) the * resource is enabled for. */ CalEvent::Types enabledTypes() const; /** Set the enabled/disabled state of the resource and its alarms, * for a specified alarm type (active, archived or template). The * enabled/disabled state for other alarm types is not affected. * The alarms of that type in a disabled resource are ignored, and * not displayed in the alarm list. The standard status for that type * for a disabled resource is automatically cleared. * @param type alarm type * @param enabled true to set enabled, false to set disabled. */ void setEnabled(CalEvent::Type type, bool enabled); /** Set which alarm types (active, archived or template) the resource * is enabled for. * @param types alarm types */ void setEnabled(CalEvent::Types types); /** Return whether the resource is configured as read-only or is * read-only on disc. */ bool readOnly() const; /** Return whether the resource is both enabled and fully writable for a * given alarm type, i.e. not read-only, and compatible with the current * KAlarm calendar format. * * @param type alarm type to check for, or EMPTY to check for any type. * @return 1 = fully enabled and writable, * 0 = enabled and writable except that backend calendar is in an * old KAlarm format, * -1 = read-only, disabled or incompatible format. */ int writableStatus(CalEvent::Type type = CalEvent::EMPTY) const; /** Return whether the resource is both enabled and fully writable for a * given alarm type, i.e. not read-only, and compatible with the current * KAlarm calendar format. * * @param type alarm type to check for, or EMPTY to check for any type. */ bool isWritable(CalEvent::Type type = CalEvent::EMPTY) const; #if 0 /** Return whether the event can be written to now, i.e. the resource is * active and read-write, and the event is in the current KAlarm format. */ bool isWritable(const KAEvent&) const; #endif /** Return whether the user has chosen not to update the resource's * calendar storage format. */ bool keepFormat() const; /** Set or clear whether the user has chosen not to update the resource's * calendar storage format. */ void setKeepFormat(bool keep); /** Return the background colour used to display alarms belonging to * this resource. * @return display colour, or invalid if none specified */ QColor backgroundColour() const; /** Set the background colour used to display alarms belonging to this * resource. * @param colour display colour, or invalid to use the default colour */ void setBackgroundColour(const QColor& colour); /** Return the foreground colour used to display alarms belonging to * this resource, for given alarm type(s). * @param types Alarm type(s), or EMPTY for the alarm types which the * resource contains. * @return display colour, or invalid if none specified */ QColor foregroundColour(CalEvent::Types types = CalEvent::EMPTY) const; /** Return whether the resource is set in the resource's config to be the * standard resource for a specified alarm type (active, archived or * template). There is no check for whether the resource is enabled, is * writable, or is the only resource set as standard. * * @note To determine whether the resource is actually the standard * resource, call the resource manager's isStandard() method. * * @param type alarm type */ bool configIsStandard(CalEvent::Type type) const; /** Return which alarm types (active, archived or template) the resource * is standard for, as set in its config. This is restricted to the alarm * types which the resource can contain (@see alarmTypes()). * There is no check for whether the resource is enabled, is writable, or * is the only resource set as standard. * * @note To determine what alarm types the resource is actually the standard * resource for, call the resource manager's standardTypes() method. * * @return alarm types. */ CalEvent::Types configStandardTypes() const; /** Set or clear the resource as the standard resource for a specified alarm * type (active, archived or template), storing the setting in the resource's * config. There is no check for whether the resource is eligible to be * set as standard, or to ensure that it is the only standard resource for * the type. * * @note To set the resource's standard status and ensure that it is * eligible and the only standard resource for the type, call * the resource manager's setStandard() method. * * @param type alarm type * @param standard true to set as standard, false to clear standard status. */ void configSetStandard(CalEvent::Type type, bool standard); /** Set which alarm types (active, archived or template) the resource is * the standard resource for, storing the setting in the resource's config. * There is no check for whether the resource is eligible to be set as * standard, or to ensure that it is the only standard resource for the * types. * * @note To set the resource's standard status and ensure that it is * eligible and the only standard resource for the types, call * the resource manager's setStandard() method. * * @param types alarm types.to set as standard */ void configSetStandard(CalEvent::Types types); /** Return whether the resource is in the current KAlarm format. * @see compatibility(), compatibilityVersion() */ bool isCompatible() const; /** Return whether the resource is in a different format from the * current KAlarm format, in which case it cannot be written to. * Note that isWritable() takes account of incompatible format * as well as read-only and enabled statuses. */ KACalendar::Compat compatibility() const; /** Return whether the resource is in a different format from the * current KAlarm format, in which case it cannot be written to. * Note that isWritable() takes account of incompatible format * as well as read-only and enabled statuses. * @param versionString Receives calendar's KAlarm version as a string. */ KACalendar::Compat compatibilityVersion(QString& versionString) const; /** Edit the resource's configuration. */ void editResource(QWidget* dialogParent); /** Remove the resource. The calendar file is not removed. * @return true if the resource has been removed or a removal job has been scheduled. * @note The instance will be invalid once it has been removed. */ bool removeResource(); /** Load the resource from the file, and fetch all events. * If loading is initiated, Resources::resourcePopulated() will be emitted * on completion. * Loading is not performed if the resource is disabled. * If the resource is cached, it will be loaded from the cache file (which * if @p readThroughCache is true, will first be downloaded from the resource file). * * Derived classes must implement loading in doLoad(). * * @param readThroughCache If the resource is cached, refresh the cache first. * @return true if loading succeeded or has been initiated. * false if it failed. */ bool load(bool readThroughCache = true); /** Reload the resource. Any cached data is first discarded. */ bool reload(); /** Return whether the resource has fully loaded. */ bool isPopulated() const; /** Save the resource. * Saving is not performed if the resource is disabled. * If the resource is cached, it will be saved to the cache file (which * if @p writeThroughCache is true, will then be uploaded from the resource file). * @param writeThroughCache If the resource is cached, update the file * after writing to the cache. * @return true if saving succeeded or has been initiated. * false if it failed. */ bool save(bool writeThroughCache = true); /** Return whether the resource is waiting for a save() to complete. */ bool isSaving() const; + /** Close the resource. This saves any unsaved data. + * Saving is not performed if the resource is disabled. + */ + void close(); + /** Return all events belonging to this resource. */ QList events() const; /** Return the event with the given ID, provided its alarm type is enabled for * the resource. * @return The event, or invalid event if not found or alarm type is disabled. */ KAEvent event(const QString& eventId) const; /** Return whether the resource contains the event whose ID is given, provided * the event's alarm type is enabled for the resource. */ bool containsEvent(const QString& eventId) const; /** Add an event to the resource. */ bool addEvent(const KAEvent&); /** Update an event in the resource. Its UID must be unchanged. */ bool updateEvent(const KAEvent&); /** Delete an event from the resource. */ bool deleteEvent(const KAEvent&); /** Called to notify the resource that an event's command error has changed. */ void handleCommandErrorChange(const KAEvent&); /** Must be called to notify the resource that it is being deleted. * This is to prevent expected errors being displayed to the user. * @see isBeingDeleted */ void notifyDeletion(); /** Return whether the resource has been notified that it is being deleted. * @see notifyDeletion */ bool isBeingDeleted() const; /** Check whether the resource is of a specified type. * @tparam Type The resource type to check. */ template bool is() const; private: /** Return the shared pointer to the alarm calendar resource for this resource. * @warning The instance referred to by the pointer will be deleted when all * Resource instances containing it go out of scope or are deleted, * so do not pass the pointer to another function. */ template T* resource() const; ResourceType::Ptr mResource; friend class ResourceType; // needs access to resource() friend uint qHash(const Resource& resource, uint seed); }; inline bool operator==(const ResourceType* a, const Resource& b) { return b == a; } inline bool operator!=(const Resource& a, const Resource& b) { return !(a == b); } inline bool operator!=(const Resource& a, const ResourceType* b) { return !(a == b); } inline bool operator!=(const ResourceType* a, const Resource& b) { return !(b == a); } inline uint qHash(const Resource& resource, uint seed) { return qHash(resource.mResource.data(), seed); } /*============================================================================= * Template definitions. *============================================================================*/ template bool Resource::is() const { return static_cast(qobject_cast(mResource.data())); } template T* Resource::resource() const { return qobject_cast(mResource.data()); } #endif // RESOURCE_H // vim: et sw=4: diff --git a/src/resources/resources.cpp b/src/resources/resources.cpp index 227cbbb9..90ac2a4e 100644 --- a/src/resources/resources.cpp +++ b/src/resources/resources.cpp @@ -1,622 +1,629 @@ /* * resource.cpp - generic class containing an alarm calendar resource * Program: kalarm * Copyright © 2019-2020 David Jarvie * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "resources.h" #include "resource.h" #include "resourcedatamodelbase.h" #include "resourcemodel.h" #include "resourceselectdialog.h" #include "preferences.h" #include "lib/autoqpointer.h" #include "kalarm_debug.h" #include Resources* Resources::mInstance{nullptr}; // Copy of all ResourceType instances with valid ID, wrapped in the Resource // container which manages the instance. QHash Resources::mResources; bool Resources::mCreated{false}; bool Resources::mPopulated{false}; Resources* Resources::instance() { if (!mInstance) mInstance = new Resources; return mInstance; } Resources::Resources() { } +Resources::~Resources() +{ + qCDebug(KALARM_LOG) << "Resources::~Resources"; + for (auto it = mResources.begin(); it != mResources.end(); ++it) + it.value().close(); +} + Resource Resources::resource(ResourceId id) { return mResources.value(id, Resource::null()); } /****************************************************************************** * Return the resources which are enabled for a specified alarm type. * If 'writable' is true, only writable resources are included. */ QVector Resources::enabledResources(CalEvent::Type type, bool writable) { const CalEvent::Types types = (type == CalEvent::EMPTY) ? CalEvent::ACTIVE | CalEvent::ARCHIVED | CalEvent::TEMPLATE : type; QVector result; for (auto it = mResources.constBegin(); it != mResources.constEnd(); ++it) { const Resource& res = it.value(); if (writable && !res.isWritable()) continue; if (res.enabledTypes() & types) result += res; } return result; } /****************************************************************************** * Return the standard resource for an alarm type. */ Resource Resources::getStandard(CalEvent::Type type) { Resources* manager = instance(); bool wantDefaultArchived = (type == CalEvent::ARCHIVED); Resource defaultArchived; for (auto it = manager->mResources.constBegin(); it != manager->mResources.constEnd(); ++it) { const Resource& res = it.value(); if (res.isWritable(type)) { if (res.configIsStandard(type)) return res; if (wantDefaultArchived) { if (defaultArchived.isValid()) wantDefaultArchived = false; // found two archived alarm resources else defaultArchived = res; // this is the first archived alarm resource } } } if (wantDefaultArchived && defaultArchived.isValid()) { // There is no resource specified as the standard archived alarm // resource, but there is exactly one writable archived alarm // resource. Set that resource to be the standard. defaultArchived.configSetStandard(CalEvent::ARCHIVED, true); return defaultArchived; } return Resource(); } /****************************************************************************** * Return whether a collection is the standard collection for a specified * mime type. */ bool Resources::isStandard(const Resource& resource, CalEvent::Type type) { // If it's for archived alarms, get and also set the standard resource if // necessary. if (type == CalEvent::ARCHIVED) return getStandard(type) == resource; return resource.configIsStandard(type) && resource.isWritable(type); } /****************************************************************************** * Return the alarm types for which a resource is the standard resource. */ CalEvent::Types Resources::standardTypes(const Resource& resource, bool useDefault) { if (!resource.isWritable()) return CalEvent::EMPTY; Resources* manager = instance(); auto it = manager->mResources.constFind(resource.id()); if (it == manager->mResources.constEnd()) return CalEvent::EMPTY; CalEvent::Types stdTypes = resource.configStandardTypes() & resource.enabledTypes(); if (useDefault) { // Also return alarm types for which this is the only resource. // Check if it is the only writable resource for these type(s). if (!(stdTypes & CalEvent::ARCHIVED) && resource.isEnabled(CalEvent::ARCHIVED)) { // If it's the only enabled archived alarm resource, set it as standard. getStandard(CalEvent::ARCHIVED); stdTypes = resource.configStandardTypes() & resource.enabledTypes(); } CalEvent::Types enabledNotStd = resource.enabledTypes() & ~stdTypes; if (enabledNotStd) { // The resource is enabled for type(s) for which it is not the standard. for (auto itr = manager->mResources.constBegin(); itr != manager->mResources.constEnd() && enabledNotStd; ++itr) { const Resource& res = itr.value(); if (res != resource && res.isWritable()) { const CalEvent::Types en = res.enabledTypes() & enabledNotStd; if (en) enabledNotStd &= ~en; // this resource handles the same alarm type } } } stdTypes |= enabledNotStd; } return stdTypes; } /****************************************************************************** * Set or clear the standard status for a resource. */ void Resources::setStandard(Resource& resource, CalEvent::Type type, bool standard) { if (!(type & resource.enabledTypes())) return; Resources* manager = instance(); auto it = manager->mResources.find(resource.id()); if (it == manager->mResources.end()) return; resource = it.value(); // just in case it's a different object! if (standard == resource.configIsStandard(type)) return; if (!standard) resource.configSetStandard(type, false); else if (resource.isWritable(type)) { // Clear the standard status for any other resources. for (auto itr = manager->mResources.begin(); itr != manager->mResources.end(); ++itr) { Resource& res = itr.value(); if (res != resource) res.configSetStandard(type, false); } resource.configSetStandard(type, true); } } /****************************************************************************** * Set the alarm types for which a resource the standard resource. */ void Resources::setStandard(Resource& resource, CalEvent::Types types) { types &= resource.enabledTypes(); Resources* manager = instance(); auto it = manager->mResources.find(resource.id()); if (it == manager->mResources.end()) return; resource = it.value(); // just in case it's a different object! if (types != resource.configStandardTypes() && (!types || resource.isWritable())) { if (types) { // Clear the standard status for any other resources. for (auto itr = manager->mResources.begin(); itr != manager->mResources.end(); ++itr) { Resource& res = itr.value(); if (res != resource) { const CalEvent::Types rtypes = res.configStandardTypes(); if (rtypes & types) res.configSetStandard(rtypes & ~types); } } } resource.configSetStandard(types); } } /****************************************************************************** * Find the resource to be used to store an event of a given type. * This will be the standard resource for the type, but if this is not valid, * the user will be prompted to select a resource. */ Resource Resources::destination(CalEvent::Type type, QWidget* promptParent, bool noPrompt, bool* cancelled) { if (cancelled) *cancelled = false; Resource standard; if (type == CalEvent::EMPTY) return standard; standard = getStandard(type); // Archived alarms are always saved in the default resource, // else only prompt if necessary. if (type == CalEvent::ARCHIVED || noPrompt || (!Preferences::askResource() && standard.isValid())) return standard; // Prompt for which collection to use ResourceListModel* model = DataModel::createResourceListModel(promptParent); model->setFilterWritable(true); model->setFilterEnabled(true); model->setEventTypeFilter(type); model->useResourceColour(false); Resource res; switch (model->rowCount()) { case 0: break; case 1: res = model->resource(0); break; default: { // Use AutoQPointer to guard against crash on application exit while // the dialogue is still open. It prevents double deletion (both on // deletion of 'promptParent', and on return from this function). AutoQPointer dlg = new ResourceSelectDialog(model, promptParent); dlg->setWindowTitle(i18nc("@title:window", "Choose Calendar")); dlg->setDefaultResource(standard); if (dlg->exec()) res = dlg->selectedResource(); if (!res.isValid() && cancelled) *cancelled = true; } } return res; } /****************************************************************************** * Return whether all configured resources have been created. */ bool Resources::allCreated() { return instance()->mCreated; } /****************************************************************************** * Return whether all configured resources have been loaded at least once. */ bool Resources::allPopulated() { return instance()->mPopulated; } /****************************************************************************** * Return the resource which an event belongs to, provided its alarm type is * enabled. */ Resource Resources::resourceForEvent(const QString& eventId) { for (auto it = mResources.constBegin(); it != mResources.constEnd(); ++it) { const Resource& res = it.value(); if (res.containsEvent(eventId)) return res; } return Resource::null(); } /****************************************************************************** * Return the resource which an event belongs to, and the event, provided its * alarm type is enabled. */ Resource Resources::resourceForEvent(const QString& eventId, KAEvent& event) { for (auto it = mResources.constBegin(); it != mResources.constEnd(); ++it) { const Resource& res = it.value(); event = res.event(eventId); if (event.isValid()) return res; } if (mResources.isEmpty()) // otherwise, 'event' was set invalid in the loop event = KAEvent(); return Resource::null(); } /****************************************************************************** * Return the resource which has a given configuration identifier. */ Resource Resources::resourceForConfigName(const QString& configName) { for (auto it = mResources.constBegin(); it != mResources.constEnd(); ++it) { const Resource& res = it.value(); if (res.configName() == configName) return res; } return Resource::null(); } /****************************************************************************** * Called after a new resource has been created, when it has completed its * initialisation. */ void Resources::notifyNewResourceInitialised(Resource& res) { if (res.isValid()) Q_EMIT instance()->resourceAdded(res); } /****************************************************************************** * Called when all configured resources have been created for the first time. */ void Resources::notifyResourcesCreated() { mCreated = true; Q_EMIT instance()->resourcesCreated(); checkResourcesPopulated(); } /****************************************************************************** * Called when a resource's events have been loaded. * Emits a signal if all collections have been populated. */ void Resources::notifyResourcePopulated(const ResourceType* res) { if (res) { Resource r = resource(res->id()); if (r.isValid()) Q_EMIT instance()->resourcePopulated(r); } // Check whether all resources have now loaded at least once. checkResourcesPopulated(); } /****************************************************************************** * Called to notify that migration/creation of resources has completed. */ void Resources::notifyResourcesMigrated() { Q_EMIT instance()->migrationCompleted(); } /****************************************************************************** * Called to notify that a resource is about to be removed. */ void Resources::notifyResourceToBeRemoved(ResourceType* res) { if (res) { Resource r = resource(res->id()); if (r.isValid()) Q_EMIT instance()->resourceToBeRemoved(r); } } /****************************************************************************** * Called by a resource to notify that its settings have changed. * Emits the settingsChanged() signal. * If the resource is now read-only and was standard, clear its standard status. * If the resource has newly enabled alarm types, ensure that it doesn't * duplicate any existing standard setting. */ void Resources::notifySettingsChanged(ResourceType* res, ResourceType::Changes change, CalEvent::Types oldEnabled) { if (!res) return; Resource r = resource(res->id()); if (!r.isValid()) return; Resources* manager = instance(); if (change & ResourceType::Enabled) { ResourceType::Changes change = ResourceType::Enabled; // Find which alarm types (if any) have been newly enabled. const CalEvent::Types extra = res->enabledTypes() & ~oldEnabled; CalEvent::Types std = res->configStandardTypes(); const CalEvent::Types extraStd = std & extra; if (extraStd && res->isWritable()) { // Alarm type(s) have been newly enabled, and are set as standard. // Don't allow the resource to be set as standard for those types if // another resource is already the standard. CalEvent::Types disallowedStdTypes{}; for (auto it = manager->mResources.constBegin(); it != manager->mResources.constEnd(); ++it) { const Resource& resit = it.value(); if (resit.id() != res->id() && resit.isWritable()) { disallowedStdTypes |= extraStd & resit.configStandardTypes() & resit.enabledTypes(); if (extraStd == disallowedStdTypes) break; // all the resource's newly enabled standard types are disallowed } } if (disallowedStdTypes) { std &= ~disallowedStdTypes; res->configSetStandard(std); } } if (std) change |= ResourceType::Standard; } Q_EMIT manager->settingsChanged(r, change); if ((change & ResourceType::ReadOnly) && res->readOnly()) { qCDebug(KALARM_LOG) << "Resources::notifySettingsChanged:" << res->displayId() << "ReadOnly"; // A read-only resource can't be the default for any alarm type const CalEvent::Types std = standardTypes(r, false); if (std != CalEvent::EMPTY) { setStandard(r, CalEvent::EMPTY); bool singleType = true; QString msg; switch (std) { case CalEvent::ACTIVE: msg = xi18n("The calendar %1 has been made read-only. " "This was the default calendar for active alarms.", res->displayName()); break; case CalEvent::ARCHIVED: msg = xi18n("The calendar %1 has been made read-only. " "This was the default calendar for archived alarms.", res->displayName()); break; case CalEvent::TEMPLATE: msg = xi18n("The calendar %1 has been made read-only. " "This was the default calendar for alarm templates.", res->displayName()); break; default: msg = xi18nc("@info", "The calendar %1 has been made read-only. " "This was the default calendar for:%2" "Please select new default calendars.", res->displayName(), ResourceDataModelBase::typeListForDisplay(std)); singleType = false; break; } if (singleType) msg = xi18nc("@info", "%1Please select a new default calendar.", msg); notifyResourceMessage(res->id(), ResourceType::MessageType::Info, msg, QString()); } } } void Resources::notifyResourceMessage(ResourceType* res, ResourceType::MessageType type, const QString& message, const QString& details) { if (res) notifyResourceMessage(res->id(), type, message, details); } void Resources::notifyResourceMessage(ResourceId id, ResourceType::MessageType type, const QString& message, const QString& details) { Resource r = resource(id); if (r.isValid()) Q_EMIT instance()->resourceMessage(r, type, message, details); } void Resources::notifyEventsAdded(ResourceType* res, const QList& events) { if (res) { Resource r = resource(res->id()); if (r.isValid()) Q_EMIT instance()->eventsAdded(r, events); } } void Resources::notifyEventUpdated(ResourceType* res, const KAEvent& event) { if (res) { Resource r = resource(res->id()); if (r.isValid()) Q_EMIT instance()->eventUpdated(r, event); } } void Resources::notifyEventsToBeRemoved(ResourceType* res, const QList& events) { if (res) { Resource r = resource(res->id()); if (r.isValid()) Q_EMIT instance()->eventsToBeRemoved(r, events); } } bool Resources::addResource(ResourceType* instance, Resource& resource) { if (!instance || instance->id() < 0) { // Instance is invalid - return an invalid resource. delete instance; resource = Resource::null(); return false; } auto it = mResources.constFind(instance->id()); if (it != mResources.constEnd()) { // Instance ID already exists - return the existing resource. delete instance; resource = it.value(); return false; } // Add a new resource. resource = Resource(instance); mResources[instance->id()] = resource; return true; } void Resources::removeResource(ResourceId id) { if (mResources.remove(id) > 0) Q_EMIT instance()->resourceRemoved(id); } /****************************************************************************** * To be called when a resource has been created or loaded. * If all resources have now loaded for the first time, emit signal. */ void Resources::checkResourcesPopulated() { if (!mPopulated && mCreated) { // Check whether all resources have now loaded at least once. for (auto it = mResources.constBegin(); it != mResources.constEnd(); ++it) { const Resource& res = it.value(); if (res.isEnabled(CalEvent::EMPTY) && !res.isPopulated()) return; } mPopulated = true; Q_EMIT instance()->resourcesPopulated(); } } #if 0 /****************************************************************************** * Return whether one or all enabled collections have been loaded. */ bool Resources::isPopulated(ResourceId id) { if (id >= 0) { const Resource res = resource(id); return res.isPopulated() || res.enabledTypes() == CalEvent::EMPTY; } for (auto it = mResources.constBegin(); it != mResources.constEnd(); ++it) { const Resource& res = it.value(); if (!res.isPopulated() && res.enabledTypes() != CalEvent::EMPTY) return false; } return true; } #endif // vim: et sw=4: diff --git a/src/resources/resources.h b/src/resources/resources.h index 61712700..05cfcee8 100644 --- a/src/resources/resources.h +++ b/src/resources/resources.h @@ -1,297 +1,297 @@ /* * resources.h - container for all ResourceType instances * Program: kalarm - * Copyright © 2019 David Jarvie + * Copyright © 2019-2020 David Jarvie * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef RESOURCES_H #define RESOURCES_H #include "datamodel.h" #include "resource.h" #include "resourcemodel.h" #include class QEventLoop; using namespace KAlarmCal; /** Class to contain all ResourceType instances. * It provides connection to signals from all ResourceType instances. */ class Resources : public QObject { Q_OBJECT public: /** Creates the unique Resources instance. */ static Resources* instance(); - ~Resources() {} + ~Resources(); Resources(const Resources&) = delete; Resources& operator=(const Resources&) const = delete; /** Return a copy of the resource with a given ID. * @return The resource, or invalid if the ID doesn't already exist or is invalid. */ static Resource resource(ResourceId); /** Remove a resource. The calendar file is not removed. * @return true if the resource has been removed or a removal job has been scheduled. */ static bool removeResource(Resource&); /** Return all resources of a kind which contain a specified alarm type. * @tparam Type Resource type to fetch, default = all types. * @param alarmType Alarm type to check for, or CalEvent::EMPTY for any type. */ template static QVector allResources(CalEvent::Type alarmType = CalEvent::EMPTY); /** Return the enabled resources which contain a specified alarm type. * @param type Alarm type to check for, or CalEvent::EMPTY for any type. * @param writable If true, only writable resources are included. */ static QVector enabledResources(CalEvent::Type type = CalEvent::EMPTY, bool writable = false); /** Return the standard resource for an alarm type. This is the resource * which can be set as the default to add new alarms to. * Only enabled and writable resources can be standard. * In the case of archived alarm resources, if no resource is specified * as standard and there is exactly one writable archived alarm resource, * that resource will be automatically set as standard. * * @param type alarm type * @return standard resource, or null if none. */ static Resource getStandard(CalEvent::Type type); /** Return whether a resource is the standard resource for a specified alarm * type. Only enabled and writable resources can be standard. * In the case of archived alarms, if no resource is specified as standard * and the resource is the only writable archived alarm resource, it will * be automatically set as standard. */ static bool isStandard(const Resource& resource, CalEvent::Type); /** Return the alarm type(s) for which a resource is the standard resource. * Only enabled and writable resources can be standard. * @param useDefault false to return the defined standard types, if any; * true to return the types for which it is the standard * or only resource. */ static CalEvent::Types standardTypes(const Resource& resource, bool useDefault = false); /** Set or clear a resource as the standard resource for a specified alarm * type. This does not affect its status for other alarm types. * The resource must be writable and enabled for the type, to set * standard = true. * If the resource is being set as standard, the standard status for the * alarm type is cleared for any other resources. */ static void setStandard(Resource& resource, CalEvent::Type, bool standard); /** Set which alarm types a resource is the standard resource for. * Its standard status is cleared for other alarm types. * The resource must be writable and enabled for the type, to set * standard = true. * If the resource is being set as standard for any alarm types, the * standard status is cleared for those alarm types for any other resources. */ static void setStandard(Resource& resource, CalEvent::Types); /** Find the resource to be used to store an event of a given type. * This will be the standard resource for the type, but if this is not valid, * the user will be prompted to select a resource. * @param type The event type * @param promptParent The parent widget for the prompt * @param noPrompt Don't prompt the user even if the standard resource is not valid * @param cancelled If non-null: set to true if the user cancelled the * prompt dialogue; set to false if any other error */ static Resource destination(CalEvent::Type type, QWidget* promptParent = nullptr, bool noPrompt = false, bool* cancelled = nullptr); /** Return whether all configured resources have been created. */ static bool allCreated(); /** Return whether all configured resources have been loaded at least once. */ static bool allPopulated(); /** Return the resource which an event belongs to, provided that the event's * alarm type is enabled. */ static Resource resourceForEvent(const QString& eventId); /** Return the resource which an event belongs to, and the event, provided * that the event's alarm type is enabled. */ static Resource resourceForEvent(const QString& eventId, KAEvent& event); /** Return the resource which has a given configuration identifier. */ static Resource resourceForConfigName(const QString& configName); /** Called to notify that a new resource has completed its initialisation, * in order to emit the resourceAdded() signal. */ static void notifyNewResourceInitialised(Resource&); /** Called to notify that all configured resources have now been created. */ static void notifyResourcesCreated(); /** Called by a resource to notify that loading of events has successfully completed. */ static void notifyResourcePopulated(const ResourceType*); /** Called to notify that migration/creation of resources has completed. */ static void notifyResourcesMigrated(); /** Called to notify that a resource is about to be removed. */ static void notifyResourceToBeRemoved(ResourceType*); /** Called by a resource to notify that its settings have changed. * This will cause the settingsChanged() signal to be emitted. */ static void notifySettingsChanged(ResourceType*, ResourceType::Changes, CalEvent::Types oldEnabled); /** Called by a resource when a user message should be displayed. * This will cause the resourceMessage() signal to be emitted. * @param message Must include the resource's display name in order to * identify the resource to the user. */ static void notifyResourceMessage(ResourceType*, ResourceType::MessageType, const QString& message, const QString& details); /** Called when a user message should be displayed for a resource. * This will cause the resourceMessage() signal to be emitted. * @param message Must include the resource's display name in order to * identify the resource to the user. */ static void notifyResourceMessage(ResourceId, ResourceType::MessageType, const QString& message, const QString& details); /** Called by a resource to notify that it has added events. */ static void notifyEventsAdded(ResourceType*, const QList&); /** Called by a resource to notify that it has changed an event. * The event's UID must be unchanged. */ static void notifyEventUpdated(ResourceType*, const KAEvent& event); /** Called by a resource to notify that it is about to delete events. */ static void notifyEventsToBeRemoved(ResourceType*, const QList&); Q_SIGNALS: /** Emitted when a resource's settings have changed. */ void settingsChanged(Resource&, ResourceType::Changes); /** Emitted when all configured resource have been created (but not * necessarily populated). Note that after this, resource migration and * the creation of default resources is performed and notified by the * signal migrationCompleted(). */ void resourcesCreated(); /** Emitted when all configured resources have been loaded for the first time. */ void resourcesPopulated(); /** Signal emitted when resource migration/creation at startup has completed. */ void migrationCompleted(); /** Emitted when a new resource has been created. */ void resourceAdded(Resource&); /** Emitted when a resource's events have been successfully loaded. */ void resourcePopulated(Resource&); /** Emitted when a resource's config and settings are about to be removed. */ void resourceToBeRemoved(Resource&); /** Emitted when a resource's config and settings have been removed. */ void resourceRemoved(ResourceId); /** Emitted when a resource message should be displayed to the user. * @note Connections to this signal should use Qt::QueuedConnection type * to allow processing to continue while the user message is displayed. */ void resourceMessage(Resource&, ResourceType::MessageType, const QString& message, const QString& details); /** Emitted when events have been added to a resource. * Events are only notified whose alarm type is enabled. */ void eventsAdded(Resource&, const QList&); /** Emitted when an event has been updated in a resource. * Events are only notified whose alarm type is enabled. * The event's UID is unchanged. */ void eventUpdated(Resource&, const KAEvent&); /** Emitted when events are about to be deleted from a resource. * Events are only notified whose alarm type is enabled. */ void eventsToBeRemoved(Resource&, const QList&); private: Resources(); /** Add a new ResourceType instance, with a Resource owner. * Once the resource has completed its initialisation, call * notifyNewResourceInitialised() to emit the resourceAdded() signal. * is require * @param type Newly constructed ResourceType instance, which will belong to * 'resource' if successful. On error, it will be deleted. * @param resource If type is invalid, updated to an invalid resource; * If type ID already exists, updated to the existing resource with that ID; * If type ID doesn't exist, updated to the new resource containing res. * @return true if a new resource has been created, false if invalid or already exists. */ static bool addResource(ResourceType* type, Resource& resource); /** Remove the resource with a given ID. * @note The ResourceType instance will only be deleted once all Resource * instances which refer to this ID go out of scope. */ static void removeResource(ResourceId); static void checkResourcesPopulated(); static Resources* mInstance; // the unique instance static QHash mResources; // contains all ResourceType instances with an ID static bool mCreated; // all resources have been created static bool mPopulated; // all resources have been loaded once friend class ResourceType; }; /*============================================================================= * Template definitions. *============================================================================*/ template QVector Resources::allResources(CalEvent::Type type) { const CalEvent::Types types = (type == CalEvent::EMPTY) ? CalEvent::ACTIVE | CalEvent::ARCHIVED | CalEvent::TEMPLATE : type; QVector result; for (auto it = mResources.constBegin(); it != mResources.constEnd(); ++it) { const Resource& res = it.value(); if (res.is() && (res.alarmTypes() & types)) result += res; } return result; } #endif // RESOURCES_H // vim: et sw=4: diff --git a/src/resources/resourcetype.h b/src/resources/resourcetype.h index 7953dc53..34f85fda 100644 --- a/src/resources/resourcetype.h +++ b/src/resources/resourcetype.h @@ -1,473 +1,478 @@ /* * resourcetype.h - base class for an alarm calendar resource type * Program: kalarm * Copyright © 2019-2020 David Jarvie * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef RESOURCETYPE_H #define RESOURCETYPE_H #include #include #include #include #include #include class Resource; using namespace KAlarmCal; /** Abstract base class for an alarm calendar resource type. */ class ResourceType : public QObject { Q_OBJECT public: /** The type of storage used by a resource. */ enum StorageType { NoStorage, File, Directory }; /** Settings change types. These may be combined. * @note A resource's location is not allowed to change, except by * deleting the resource and creating another resource with the new * location. (Note that Akonadi resources do actually allow * location change, but this is handled internally by Akonadi and * has no impact on clients.) */ enum Change { NoChange = 0, Name = 0x01, //!< The resource's display name. AlarmTypes = 0x02, //!< Alarm types contained in the resource. Enabled = 0x04, //!< Alarm types which are enabled. Standard = 0x08, //!< Alarm types which the resource is standard for. ReadOnly = 0x10, //!< The resource's read-only setting. KeepFormat = 0x20, //!< Whether the user has chosen not to convert to the current KAlarm format. UpdateFormat = 0x40, //!< The resource should now be converted to the current KAlarm format. BackgroundColour = 0x80 //!< The background colour to display the resource. }; Q_DECLARE_FLAGS(Changes, Change) /** Resource message types. */ enum class MessageType { Info, Error }; /** A shared pointer to an Resource object. */ typedef QSharedPointer Ptr; ResourceType() {} /** Constructor. * @param temporary If false, the new instance will be added to the list * of instances for lookup; * If true, it's a temporary instance not added to the list. */ explicit ResourceType(ResourceId); virtual ~ResourceType() = 0; /** Return whether the resource has a valid configuration. * Note that the resource may be unusable even if it has a valid * configuration: see failed(). */ virtual bool isValid() const = 0; /** Return whether the resource has a fatal error. * Note that failed() will return true if the configuration is invalid * (i.e. isValid() returns false). It will also return true if some other * error prevents the resource being used, e.g. if the calendar file * cannot be created. */ bool failed() const; /** Return the resource's unique ID. */ ResourceId id() const { return mId; } /** Return the resource's unique ID, as shown to the user. */ virtual ResourceId displayId() const; /** Return the type of storage used by the resource. */ virtual StorageType storageType() const = 0; /** Return the type of the resource (file, remote file, etc.) * for display purposes. * @param description true for description (e.g. "Remote file"), * false for brief label (e.g. "URL"). */ virtual QString storageTypeString(bool description) const = 0; /** Return the type description of a resource (file, remote file, etc.) * for display purposes. This is equivalent to storageTypeString(true). */ static QString storageTypeString(StorageType); /** Return the location(s) of the resource (URL, file path, etc.) */ virtual QUrl location() const = 0; /** Return the location of the resource (URL, file path, etc.) * for display purposes. */ virtual QString displayLocation() const = 0; /** Return the resource's display name. */ virtual QString displayName() const = 0; /** Return the resource's configuration identifier. This is not the * name normally displayed to the user. */ virtual QString configName() const = 0; /** Return which types of alarms the resource can contain. */ virtual CalEvent::Types alarmTypes() const = 0; /** Return whether the resource is enabled for a specified alarm type * (active, archived, template or displaying). * @param type alarm type to check for, or EMPTY to check for any type. */ bool isEnabled(CalEvent::Type type) const; /** Return which alarm types (active, archived or template) the * resource is enabled for. */ virtual CalEvent::Types enabledTypes() const = 0; /** Set the enabled/disabled state of the resource and its alarms, * for a specified alarm type (active, archived or template). The * enabled/disabled state for other alarm types is not affected. * The alarms of that type in a disabled resource are ignored, and * not displayed in the alarm list. The standard status for that type * for a disabled resource is automatically cleared. * @param type alarm type * @param enabled true to set enabled, false to set disabled. */ virtual void setEnabled(CalEvent::Type type, bool enabled) = 0; /** Set which alarm types (active, archived or template) the resource * is enabled for. * @param types alarm types */ virtual void setEnabled(CalEvent::Types types) = 0; /** Return whether the resource is configured as read-only or is * read-only on disc. */ virtual bool readOnly() const = 0; /** Return whether the resource is both enabled and fully writable for a * given alarm type, i.e. not read-only, and compatible with the current * KAlarm calendar format. * * @param type alarm type to check for, or EMPTY to check for any type. * @return 1 = fully enabled and writable, * 0 = enabled and writable except that backend calendar is in an * old KAlarm format, * -1 = read-only, disabled or incompatible format. */ virtual int writableStatus(CalEvent::Type type = CalEvent::EMPTY) const = 0; /** Return whether the resource is both enabled and fully writable for a * given alarm type, i.e. not read-only, and compatible with the current * KAlarm calendar format. * * @param type alarm type to check for, or EMPTY to check for any type. */ bool isWritable(CalEvent::Type type = CalEvent::EMPTY) const; #if 0 /** Return whether the event can be written to now, i.e. the resource is * active and read-write, and the event is in the current KAlarm format. */ virtual bool isWritable(const KAEvent&) const = 0; #endif /** Return whether the user has chosen not to update the resource's * calendar storage format. */ virtual bool keepFormat() const = 0; /** Set or clear whether the user has chosen not to update the resource's * calendar storage format. */ virtual void setKeepFormat(bool keep) = 0; /** Return the background colour used to display alarms belonging to * this resource. * @return display colour, or invalid if none specified */ virtual QColor backgroundColour() const = 0; /** Set the background colour used to display alarms belonging to this * resource. * @param colour display colour, or invalid to use the default colour */ virtual void setBackgroundColour(const QColor& colour) = 0; /** Return the foreground colour used to display alarms belonging to * this resource, for given alarm type(s). * @param types Alarm type(s), or EMPTY for the alarm types which the * resource contains. * @return display colour, or invalid if none specified */ QColor foregroundColour(CalEvent::Types types = CalEvent::EMPTY) const; /** Return whether the resource is set in the resource's config to be the * standard resource for a specified alarm type (active, archived or * template). There is no check for whether the resource is enabled, is * writable, or is the only resource set as standard. * * @note To determine whether the resource is actually the standard * resource, call the resource manager's isStandard() method. * * @param type alarm type */ virtual bool configIsStandard(CalEvent::Type type) const = 0; /** Return which alarm types (active, archived or template) the resource * is standard for, as set in its config. This is restricted to the alarm * types which the resource can contain (@see alarmTypes()). * There is no check for whether the resource is enabled, is writable, or * is the only resource set as standard. * * @note To determine what alarm types the resource is actually the standard * resource for, call the resource manager's standardTypes() method. * * @return alarm types. */ virtual CalEvent::Types configStandardTypes() const = 0; /** Set or clear the resource as the standard resource for a specified alarm * type (active, archived or template), storing the setting in the resource's * config. There is no check for whether the resource is eligible to be * set as standard, or to ensure that it is the only standard resource for * the type. * * @note To set the resource's standard status and ensure that it is * eligible and the only standard resource for the type, call * the resource manager's setStandard() method. * * @param type alarm type * @param standard true to set as standard, false to clear standard status. */ virtual void configSetStandard(CalEvent::Type type, bool standard) = 0; /** Set which alarm types (active, archived or template) the resource is * the standard resource for, storing the setting in the resource's config. * There is no check for whether the resource is eligible to be set as * standard, or to ensure that it is the only standard resource for the * types. * * @note To set the resource's standard status and ensure that it is * eligible and the only standard resource for the types, call * the resource manager's setStandard() method. * * @param types alarm types.to set as standard */ virtual void configSetStandard(CalEvent::Types types) = 0; /** Return whether the resource is in the current KAlarm format. * @see compatibility(), compatibilityVersion() */ bool isCompatible() const; /** Return whether the resource is in a different format from the * current KAlarm format, in which case it cannot be written to. * Note that isWritable() takes account of incompatible format * as well as read-only and enabled statuses. */ KACalendar::Compat compatibility() const; /** Return whether the resource is in a different format from the * current KAlarm format, in which case it cannot be written to. * Note that isWritable() takes account of incompatible format * as well as read-only and enabled statuses. * @param versionString Receives calendar's KAlarm version as a string. */ virtual KACalendar::Compat compatibilityVersion(QString& versionString) const = 0; /** Edit the resource's configuration. */ virtual void editResource(QWidget* dialogParent) = 0; /** Remove the resource. The calendar file is not removed. * Derived classes must call removeResource(ResourceId) once they have removed * the resource, in order to invalidate this instance and remove it from the * list held by Resources. * @note The instance will be invalid once it has been removed. * @return true if the resource has been removed or a removal job has been scheduled. */ virtual bool removeResource() = 0; /** Load the resource from the file, and fetch all events. * If loading is initiated, Resources::resourcePopulated() will be emitted * on completion. * Loading is not performed if the resource is disabled. * If the resource is cached, it will be loaded from the cache file (which * if @p readThroughCache is true, will first be downloaded from the resource file). * * Derived classes must implement loading in doLoad(). * * @param readThroughCache If the resource is cached, refresh the cache first. * @return true if loading succeeded or has been initiated. * false if it failed. */ virtual bool load(bool readThroughCache = true) = 0; /** Reload the resource. Any cached data is first discarded. */ virtual bool reload() = 0; /** Return whether the resource has fully loaded. * Once loading completes after the resource has initialised, this should * always return true. */ virtual bool isPopulated() const { return mLoaded; } /** Save the resource. * Saving is not performed if the resource is disabled. * If the resource is cached, it will be saved to the cache file (which * if @p writeThroughCache is true, will then be uploaded from the resource file). * @param writeThroughCache If the resource is cached, update the file * after writing to the cache. * @param force Save even if no changes have been made since last * loaded or saved. * @return true if saving succeeded or has been initiated. * false if it failed. */ virtual bool save(bool writeThroughCache = true, bool force = false) = 0; /** Return whether the resource is waiting for a save() to complete. */ virtual bool isSaving() const { return false; } + /** Close the resource. This saves any unsaved data. + * Saving is not performed if the resource is disabled. + */ + virtual void close() {} + /** Return all events belonging to this resource, for enabled alarm types. */ QList events() const; /** Return the event with the given ID, provided its alarm type is enabled for * the resource. * @return The event, or invalid event if not found or alarm type is disabled. */ KAEvent event(const QString& eventId) const; using QObject::event; // prevent "hidden" warning /** Return whether the resource contains the event whose ID is given, provided * the event's alarm type is enabled for the resource. */ bool containsEvent(const QString& eventId) const; /** Add an event to the resource. */ virtual bool addEvent(const KAEvent&) = 0; /** Update an event in the resource. Its UID must be unchanged. */ virtual bool updateEvent(const KAEvent&) = 0; /** Delete an event from the resource. */ virtual bool deleteEvent(const KAEvent&) = 0; /** Called to notify the resource that an event's command error has changed. */ virtual void handleCommandErrorChange(const KAEvent&) = 0; /** Must be called to notify the resource that it is being deleted. * This is to prevent expected errors being displayed to the user. * @see isBeingDeleted */ void notifyDeletion(); /** Return whether the resource has been notified that it is being deleted. * @see notifyDeletion */ bool isBeingDeleted() const; Q_SIGNALS: /** Emitted by the all() instance, when the resource's settings have changed. */ void settingsChanged(ResourceId, Changes); /** Emitted by the all() instance, when a resource message should be displayed to the user. * @note Connections to this signal should use Qt::QueuedConnection type. * @param message Derived classes must include the resource's display name. */ void resourceMessage(ResourceId, MessageType, const QString& message, const QString& details); protected: /** Add a new ResourceType instance, with a Resource owner. * @param type Newly constructed ResourceType instance, which will belong to * 'resource' if successful. On error, it will be deleted. * @param resource If type is invalid, updated to an invalid resource; * If type ID already exists, updated to the existing resource with that ID; * If type ID doesn't exist, updated to the new resource containing res. * @return true if a new resource has been created, false if invalid or already exists. */ static bool addResource(ResourceType* type, Resource& resource); /** Remove the resource with a given ID. * @note The ResourceType instance will only be deleted once all Resource * instances which refer to this ID go out of scope. */ static void removeResource(ResourceId); /** To be called when the resource has loaded, to update the list of loaded * events for the resource. This should include both enabled and disabled * alarm types. */ void setLoadedEvents(QHash& newEvents); /** To be called when events have been created or updated, to amend them in * the resource's list. * @param notify Whether to notify added and updated events; if false, * notifyUpdatedEvents() must be called afterwards. */ void setUpdatedEvents(const QList& events, bool notify = true); /** Notify added and updated events, if setUpdatedEvents() was called with * notify = false. */ void notifyUpdatedEvents(); /** To be called when events have been deleted, to delete them from the resource's list. */ void setDeletedEvents(const QList& events); /** To be called when the loaded status of the resource has changed. */ void setLoaded(bool loaded) const; /** To be called if the resource has encountered a fatal error. * A fatal error is one that can never be recovered from. */ void setFailed(); static QString storageTypeStr(bool description, bool file, bool local); template static T* resource(Resource&); template static const T* resource(const Resource&); private: static ResourceType* data(Resource&); static const ResourceType* data(const Resource&); QHash mEvents; // all events (of ALL types) in the resource, indexed by ID QList mEventsAdded; // events added to mEvents but not yet notified QList mEventsUpdated; // events updated in mEvents but not yet notified ResourceId mId{-1}; // resource's ID, which can't be changed bool mFailed{false}; // the resource has a fatal error mutable bool mLoaded{false}; // the resource has finished loading bool mBeingDeleted{false}; // the resource is currently being deleted }; /*============================================================================= * Template definitions. *============================================================================*/ template T* ResourceType::resource(Resource& res) { return qobject_cast(data(res)); } template const T* ResourceType::resource(const Resource& res) { return qobject_cast(data(res)); } #endif // RESOURCETYPE_H // vim: et sw=4: