diff --git a/DESIGN.html b/DESIGN.html index 8c0c0eff..934e5855 100644 --- a/DESIGN.html +++ b/DESIGN.html @@ -1,156 +1,199 @@ KAlarm Design Notes

KAlarm Design Notes

See the KAlarmCal design notes (kalarmcal/DESIGN.html) for details of calendar storage.

Classes

This section summarises some of the C++ classes used to build KAlarm.

+ + + + +
ClassBase classDescription
CollectionSearchQObjectFetches a list of all Akonadi collections which handle a specified mime + type, and then optionally fetches or deletes all Items from them with a + given GID. This class is only used to access KOrganizer collections.
+

Calendar Resource Classes

- + + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ClassBase classDescription

Generic Resource and Event Classes
ResourceTypeQObject Abstract base class representing an alarm calendar resource. Classes inherited from this class are accessed via the Resource class, which encapsulates them.
Resource Represents an alarm calendar resource. Contains a shared pointer to a ResourceType object. It is designed to be safe to use even if the pointer to the ResourceType object is null.
ResourcesQObject Singleton class which contains all ResourceType instances. It allows connection to signals from all resource instances.
ResourceCreatorQObject Base class to interactively create a resource.
CalendarUpdaterQObject Base class to update the backend calendar format of a single alarm calendar.
ResourceSelectDialogQDialog A dialog which shows a list of resources and allows the user to select one.
DataModelA namespace which provides functions independent of which data model - type (Akonadi, etc.) is being used
A class which provides functions independent of which data model + type (Akonadi or file resource) is being used
ResourceDataModelBase Base class for models containing all calendars and the events (alarms and templates) within them.
ResourceFilterModelQSortFilterProxyModel Proxy filter model providing all calendar resources (not events) of a specified alarm type (active/archived/template). The selected alarm type may be changed as desired.
ResourceListModelKDescendantsProxyModel Proxy model converting the resource tree into a flat list. The model may be restricted to specified alarm types. It can optionally be restricted to writable and/or enabled resources.
ResourceCheckListModelKCheckableProxyModel Proxy model providing a checkable list of all resources. An alarm type is specified, whereby resources which are enabled for that alarm type are checked; Resources which do not contain that alarm type, or which are disabled for that alarm type, are unchecked.
ResourceFilterCheckListModelQSortFilterProxyModel Proxy model providing a checkable resource list, filtered to contain only one alarm type. The selected alarm type may be changed as desired.
EventListModelQSortFilterProxyModel Proxy filter model providing all events of a specified alarm type (active alarms, archived alarms or alarm templates), in enabled resources.
AlarmListModelEventListModel Filter proxy model containing all alarms of specified types (active, archived, template), in enabled resources.
TemplateListModelEventListModel Filter proxy model containing alarm templates, optionally for specified alarm action types (display, command, etc.), in enabled resources.
BirthdayModelAkonadi::ContactsTreeModel Provides the model for all contacts.
BirthdaySortModelQSortFilterProxyModel Filters and sorts contacts for display.
ResourceViewQListView View for a ResourceFilterCheckListModel.
EventListViewQTreeView View for an EventListModel.
AlarmListViewEventListView View showing a list of alarms.
TemplateListViewEventListView View showing a list of alarm templates.
EventListDelegateQItemDelegate Delegate for an EventListView.
AlarmListDelegateEventListDelegate Delegate for an AlarmListView. Handles editing and display of the alarm list.
TemplateListDelegateEventListDelegate Delegate for a TemplateListView. Handles editing and display of the list of alarm templates.
DirResourceImportDialogKAssistantDialogDialogue for importing a calendar directory resource. For use when + migrating Akonadi alarm calendars.

Akonadi Resource and Event Classes
AkonadiDataModelAkonadi::EntityTreeModel,
ResourceDataModelBase
Contains all KAlarm collections and the items (alarms and templates) within them.
CollectionSearchQObjectFetches a list of all Akonadi collections which handle a specified mime - type, and then optionally fetches or deletes all Items from them with a - given GID.
AkonadiResourceResourceTypeAn Akonadi calendar resource.
AkonadiResourceMigratorQObject Migrates KResources alarm calendars from pre-Akonadi versions of KAlarm, and creates default calendar resources if none exist.
AkonadiResourceCreatorResourceCreator Interactively creates an Akonadi resource.
AkonadiCalendarUpdaterCalendarUpdater Updates the backend calendar format of one Akonadi alarm calendar.

Non-Akonadi Resource and Event Classes
FileResourceDataModelQAbstractItemModel,
ResourceDataModelBase
Model containing all file system calendar resources and the events + (alarms and templates) within them.
FileResourceResourceTypeAbstract base class for a file system calendar resource.
SingleFileResourceFileResourceA file system calendar resource held in a single file.
SingleFileResourceConfigDialogQDialogConfiguration dialogue for a SingleFileResource resource.
FileResourceMigratorQObjectMigrates Akonadi or KResources alarm calendars from previous versions + of KAlarm, and creates default calendar resources if none exist.
FileResourceCreatorResourceCreatorInteractively creates a file resource.
FileResourceConfigManagerManager for file system resource configuration files. Reads + configuration files and creates resources at startup, and updates + configuration files with resource configuration changes.
FileResourceSettingsEncapsulates the configuration settings of a file system resource.
FileResourceCalendarUpdaterCalendarUpdaterUpdates the backend calendar format of one file resource alarm calendar.
diff --git a/src/kalarmapp.cpp b/src/kalarmapp.cpp index 5812bb16..85624b50 100644 --- a/src/kalarmapp.cpp +++ b/src/kalarmapp.cpp @@ -1,2707 +1,2705 @@ /* * 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 "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 namespace { const int RESOURCES_TIMEOUT = 30; // timeout (seconds) for resources 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. */ inline int maxLateness(int lateCancel) { static const int LATENESS_LEEWAY = 5; int lc = (lateCancel >= 1) ? (lateCancel - 1)*60 : 0; return LATENESS_LEEWAY + lc; } QWidget* mainWidget() { return MainWindow::mainMainWindow(); } } 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; } ResourcesCalendar::terminate(); DisplayCalendar::terminate(); DataModel::terminate(); } /****************************************************************************** * 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 (!ResourcesCalendar::instance()) { qCDebug(KALARM_LOG) << "KAlarmApp::initialise: initialising calendars"; Desktop::setMainWindowFunc(&mainWidget); DataModel::initialise(); ResourcesCalendar::initialise(); DisplayCalendar::initialise(); connect(ResourcesCalendar::instance(), &ResourcesCalendar::earliestAlarmChanged, this, &KAlarmApp::checkNextDueAlarm); connect(ResourcesCalendar::instance(), &ResourcesCalendar::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; 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 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)) exitCode = 1; else { mCommandOption = options->commandName(); mActionQueue.enqueue(ActionQEntry(action, options->eventId())); startProcessQueue(); // start processing the execution queue dontRedisplay = true; } 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)) 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; } break; case CommandOptions::EDIT: // Edit a specified existing alarm. // Open the calendar and wait for the calendar resources to be populated. if (!initCheck(false)) exitCode = 1; else { mCommandOption = options->commandName(); if (firstInstance) mEditingCmdLineAlarm = 0x10; // want to redisplay alarms if successful mActionQueue.enqueue(ActionQEntry(QueuedAction::Edit, options->eventId())); startProcessQueue(); // start processing the execution queue dontRedisplay = true; } 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; } // 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); createOnlyMainWindow(); // prevent the application from quitting } break; } case CommandOptions::EDIT_NEW_PRESET: // Edit a new alarm, preset with a template if (!initCheck()) exitCode = 1; else { // 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()); createOnlyMainWindow(); // prevent the application from quitting } 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 (ResourcesCalendar::instance()) { 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->setWindowState(Qt::WindowMinimized); win->show(); } } /****************************************************************************** * 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 ResourcesCalendar::terminate(); DisplayCalendar::terminate(); 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 = ResourcesCalendar::instance()->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.action == QueuedAction::Handle && entry.eventId == id) return; // the alarm is already queued } 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()) { ActionQEntry& entry = mActionQueue.head(); // Can't process the first action until its resource has been populated. const ResourceId id = entry.eventId.resourceId(); if ((id < 0 && !Resources::allPopulated()) || (id >= 0 && !Resources::resource(id).isPopulated())) { // If resource population has timed out, discard all queued events. if (mResourcesTimedOut) { qCCritical(KALARM_LOG) << "Error! Timeout reading calendars"; mActionQueue.clear(); } break; } // Process the first action in the queue. 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 (action) { case QueuedAction::Trigger: execAlarm(entry.event, entry.event.firstAlarm(), false); break; 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; } default: break; } } else { if (action == QueuedAction::Edit) { int editingCmdLineAlarm = mEditingCmdLineAlarm & 3; bool keepQueued = editingCmdLineAlarm <= 1; switch (editingCmdLineAlarm) { case 0: // Initiate editing an alarm specified on the command line. mEditingCmdLineAlarm |= 1; QTimer::singleShot(0, this, &KAlarmApp::slotEditAlarmById); break; case 1: // Currently editing the alarm. break; case 2: // The edit has completed. mEditingCmdLineAlarm = 0; break; default: break; } if (keepQueued) break; } else { 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; if (!mEditingCmdLineAlarm) { // 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(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()); ResourcesCalendar::instance()->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(RESOURCES_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); } } } /****************************************************************************** * Edit an alarm specified on the command line. */ void KAlarmApp::slotEditAlarmById() { qCDebug(KALARM_LOG) << "KAlarmApp::slotEditAlarmById"; ActionQEntry& entry = mActionQueue.head(); if (!KAlarm::editAlarmById(entry.eventId)) { CommandOptions::printError(xi18nc("@info:shell", "%1: Event %2 not found, or not editable", mCommandOption, entry.eventId.eventId())); mActionQueue.clear(); quitIf(1); } else { createOnlyMainWindow(); // prevent the application from quitting if (mEditingCmdLineAlarm & 0x10) { mRedisplayAlarms = false; MessageWin::redisplayAlarms(); } mEditingCmdLineAlarm = 2; // indicate edit completion QTimer::singleShot(0, this, &KAlarmApp::processQueue); } } /****************************************************************************** * 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, 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, QueuedAction action) { qCDebug(KALARM_LOG) << "KAlarmApp::dbusHandleEvent:" << 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, 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 = ResourcesCalendar::instance()->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 (action) { case QueuedAction::Cancel: qCDebug(KALARM_LOG) << "KAlarmApp::handleEvent:" << eventID << ", CANCEL"; KAlarm::deleteEvent(*event, true); break; 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 << "," << (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 (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 (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))) { ResourcesCalendar::instance()->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()) ResourcesCalendar::instance()->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!! */ DisplayCalendar::instance()->open(); } if (firstTime) { setArchivePurgeDays(); firstTime = false; } if (!calendarOnly) startProcessQueue(); // start processing the execution queue return true; } /****************************************************************************** * 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 = ResourcesCalendar::instance()->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 = ResourcesCalendar::instance()->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 71cb2574..9aa3d7b3 100644 --- a/src/kalarmapp.h +++ b/src/kalarmapp.h @@ -1,257 +1,257 @@ /* * 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, 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 slotEditAlarmById(); - 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: // Actions to execute in processQueue(). May be OR'ed together. enum class QueuedAction { // 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 Edit = 0x04, // edit an alarm (command line option) List = 0x05, // list all alarms (command line option) // 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(QueuedAction a, const EventId& id) : action(a), eventId(id) { } ActionQEntry(const KAEvent& e, QueuedAction a = QueuedAction::Handle) : action(a), event(e) { } ActionQEntry() { } 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 quitIf(int exitCode, bool force = false); void createOnlyMainWindow(); bool checkSystemTray(); void startProcessQueue(); void setResourcesTimeout(); + void checkWritableCalendar(); void checkArchivedCalendar(); void queueAlarmId(const KAEvent&); 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 mEditingCmdLineAlarm {0}; // whether currently editing alarm specified on command line 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/akonadidatamodel.cpp b/src/resources/akonadidatamodel.cpp index 756beb36..bb1379b8 100644 --- a/src/resources/akonadidatamodel.cpp +++ b/src/resources/akonadidatamodel.cpp @@ -1,1035 +1,1035 @@ /* * akonadidatamodel.cpp - KAlarm calendar file access using Akonadi * Program: kalarm * Copyright © 2007-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 "akonadidatamodel.h" #include "resources/akonadicalendarupdater.h" #include "resources/akonadiresourcecreator.h" #include "resources/akonadiresourcemigrator.h" #include "resources/eventmodel.h" #include "resources/resourcemodel.h" #include "resources/resources.h" #include "preferences.h" #include "lib/synchtimer.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 using namespace Akonadi; using namespace KAlarmCal; // Ensure ResourceDataModelBase::UserRole is valid. ResourceDataModelBase does // not include Akonadi headers, so here we check that it has been set to be // compatible with EntityTreeModel::UserRole. static_assert((int)ResourceDataModelBase::UserRole>=(int)Akonadi::EntityTreeModel::UserRole, "ResourceDataModelBase::UserRole wrong value"); /*============================================================================= = Class: AkonadiDataModel =============================================================================*/ bool AkonadiDataModel::mInstanceIsOurs = false; int AkonadiDataModel::mTimeHourPos = -2; /****************************************************************************** * Construct and return the singleton. */ AkonadiDataModel* AkonadiDataModel::instance() { if (!mInstance) { mInstance = new AkonadiDataModel(new ChangeRecorder(qApp), qApp); mInstanceIsOurs = true; } return mInstanceIsOurs ? (AkonadiDataModel*)mInstance : nullptr; } /****************************************************************************** * Constructor. */ AkonadiDataModel::AkonadiDataModel(ChangeRecorder* monitor, QObject* parent) : EntityTreeModel(monitor, parent) , ResourceDataModelBase() , mMonitor(monitor) { // Populate all collections, selected/enabled or unselected/disabled. setItemPopulationStrategy(ImmediatePopulation); // Restrict monitoring to collections containing the KAlarm mime types monitor->setCollectionMonitored(Collection::root()); monitor->setResourceMonitored("akonadi_kalarm_resource"); monitor->setResourceMonitored("akonadi_kalarm_dir_resource"); monitor->setMimeTypeMonitored(KAlarmCal::MIME_ACTIVE); monitor->setMimeTypeMonitored(KAlarmCal::MIME_ARCHIVED); monitor->setMimeTypeMonitored(KAlarmCal::MIME_TEMPLATE); monitor->itemFetchScope().fetchFullPayload(); monitor->itemFetchScope().fetchAttribute(); AttributeFactory::registerAttribute(); AttributeFactory::registerAttribute(); AttributeFactory::registerAttribute(); connect(monitor, SIGNAL(collectionChanged(Akonadi::Collection,QSet)), SLOT(slotCollectionChanged(Akonadi::Collection,QSet))); connect(monitor, &Monitor::collectionRemoved, this, &AkonadiDataModel::slotCollectionRemoved); initResourceMigrator(); MinuteTimer::connect(this, SLOT(slotUpdateTimeTo())); Preferences::connect(SIGNAL(archivedColourChanged(QColor)), this, SLOT(slotUpdateArchivedColour(QColor))); Preferences::connect(SIGNAL(disabledColourChanged(QColor)), this, SLOT(slotUpdateDisabledColour(QColor))); Preferences::connect(SIGNAL(holidaysChanged(KHolidays::HolidayRegion)), this, SLOT(slotUpdateHolidays())); Preferences::connect(SIGNAL(workTimeChanged(QTime,QTime,QBitArray)), this, SLOT(slotUpdateWorkingHours())); connect(Resources::instance(), &Resources::resourceMessage, this, &AkonadiDataModel::slotResourceMessage, Qt::QueuedConnection); connect(this, &AkonadiDataModel::rowsInserted, this, &AkonadiDataModel::slotRowsInserted); connect(this, &AkonadiDataModel::rowsAboutToBeRemoved, this, &AkonadiDataModel::slotRowsAboutToBeRemoved); connect(this, &Akonadi::EntityTreeModel::collectionTreeFetched, this, &AkonadiDataModel::slotCollectionTreeFetched); connect(this, &Akonadi::EntityTreeModel::collectionPopulated, this, &AkonadiDataModel::slotCollectionPopulated); connect(monitor, &Monitor::itemChanged, this, &AkonadiDataModel::slotMonitoredItemChanged); connect(ServerManager::self(), &ServerManager::stateChanged, this, &AkonadiDataModel::checkResources); checkResources(ServerManager::state()); } /****************************************************************************** * Destructor. */ AkonadiDataModel::~AkonadiDataModel() { if (mInstance == this) { mInstance = nullptr; mInstanceIsOurs = false; } } /****************************************************************************** * Called when the server manager changes state. * If it is now running, i.e. the agent manager knows about * all existing resources. * Once it is running, i.e. the agent manager knows about * all existing resources, if necessary migrate any KResources alarm calendars from * pre-Akonadi versions of KAlarm, or create default Akonadi calendar resources * if any are missing. */ void AkonadiDataModel::checkResources(ServerManager::State state) { switch (state) { case ServerManager::Running: if (!isMigrating() && !isMigrationComplete()) { qCDebug(KALARM_LOG) << "AkonadiDataModel::checkResources: Server running"; setMigrationInitiated(); AkonadiResourceMigrator::execute(); } break; case ServerManager::NotRunning: qCDebug(KALARM_LOG) << "AkonadiDataModel::checkResources: Server stopped"; setMigrationInitiated(false); initResourceMigrator(); Q_EMIT serverStopped(); break; default: break; } } /****************************************************************************** * Initialise the calendar migrator so that it can be run (either for the first * time, or again). */ void AkonadiDataModel::initResourceMigrator() { AkonadiResourceMigrator::reset(); connect(AkonadiResourceMigrator::instance(), &AkonadiResourceMigrator::creating, this, &AkonadiDataModel::slotCollectionBeingCreated); connect(AkonadiResourceMigrator::instance(), &QObject::destroyed, this, &AkonadiDataModel::slotMigrationCompleted); } ChangeRecorder* AkonadiDataModel::monitor() { return instance()->mMonitor; } /****************************************************************************** * Return the data for a given role, for a specified item. */ QVariant AkonadiDataModel::data(const QModelIndex& index, int role) const { if (role == ResourceIdRole) role = CollectionIdRole; // use the base model for this if (roleHandled(role) || role == ParentResourceIdRole) { const Collection collection = EntityTreeModel::data(index, CollectionRole).value(); if (collection.isValid()) { // This is a Collection row // Update the collection's resource with the current collection value. const Resource& res = updateResource(collection); bool handled; const QVariant value = resourceData(role, res, handled); if (handled) return value; } else { Item item = EntityTreeModel::data(index, ItemRole).value(); if (item.isValid()) { // This is an Item row const QString mime = item.mimeType(); if ((mime != KAlarmCal::MIME_ACTIVE && mime != KAlarmCal::MIME_ARCHIVED && mime != KAlarmCal::MIME_TEMPLATE) || !item.hasPayload()) return QVariant(); Resource res; const KAEvent ev(event(item, index, res)); // this sets item.parentCollection() if (role == ParentResourceIdRole) return item.parentCollection().id(); bool handled; const QVariant value = eventData(role, index.column(), ev, res, handled); if (handled) return value; } } } return EntityTreeModel::data(index, role); } /****************************************************************************** * Return the number of columns for either a collection or an item. */ int AkonadiDataModel::entityColumnCount(HeaderGroup group) const { switch (group) { case CollectionTreeHeaders: return 1; case ItemListHeaders: return ColumnCount; default: return EntityTreeModel::entityColumnCount(group); } } /****************************************************************************** * Return offset to add to headerData() role, for item models. */ int AkonadiDataModel::headerDataEventRoleOffset() const { return TerminalUserRole * ItemListHeaders; } /****************************************************************************** * Return data for a column heading. */ QVariant AkonadiDataModel::entityHeaderData(int section, Qt::Orientation orientation, int role, HeaderGroup group) const { bool eventHeaders = false; switch (group) { case ItemListHeaders: eventHeaders = true; Q_FALLTHROUGH(); // fall through to CollectionTreeHeaders case CollectionTreeHeaders: { bool handled; const QVariant value = ResourceDataModelBase::headerData(section, orientation, role, eventHeaders, handled); if (handled) return value; break; } default: break; } return EntityTreeModel::entityHeaderData(section, orientation, role, group); } /****************************************************************************** * Recursive function to Q_EMIT the dataChanged() signal for all items in a * specified column range. */ void AkonadiDataModel::signalDataChanged(bool (*checkFunc)(const Item&), int startColumn, int endColumn, const QModelIndex& parent) { int start = -1; int end = -1; for (int row = 0, count = rowCount(parent); row < count; ++row) { const QModelIndex ix = index(row, 0, parent); const Item item = ix.data(ItemRole).value(); const bool isItem = item.isValid(); if (isItem) { if ((*checkFunc)(item)) { // For efficiency, Q_EMIT a single signal for each group of // consecutive items, rather than a separate signal for each item. if (start < 0) start = row; end = row; continue; } } if (start >= 0) Q_EMIT dataChanged(index(start, startColumn, parent), index(end, endColumn, parent)); start = -1; if (!isItem) signalDataChanged(checkFunc, startColumn, endColumn, ix); } if (start >= 0) Q_EMIT dataChanged(index(start, startColumn, parent), index(end, endColumn, parent)); } /****************************************************************************** * Signal every minute that the time-to-alarm values have changed. */ static bool checkItem_isActive(const Item& item) { return item.mimeType() == KAlarmCal::MIME_ACTIVE; } void AkonadiDataModel::slotUpdateTimeTo() { signalDataChanged(&checkItem_isActive, TimeToColumn, TimeToColumn, QModelIndex()); } /****************************************************************************** * Called when the colour used to display archived alarms has changed. */ static bool checkItem_isArchived(const Item& item) { return item.mimeType() == KAlarmCal::MIME_ARCHIVED; } void AkonadiDataModel::slotUpdateArchivedColour(const QColor&) { qCDebug(KALARM_LOG) << "AkonadiDataModel::slotUpdateArchivedColour"; signalDataChanged(&checkItem_isArchived, 0, ColumnCount - 1, QModelIndex()); } /****************************************************************************** * Called when the colour used to display disabled alarms has changed. */ static bool checkItem_isDisabled(const Item& item) { if (item.hasPayload()) { const KAEvent event = item.payload(); if (event.isValid()) return !event.enabled(); } return false; } void AkonadiDataModel::slotUpdateDisabledColour(const QColor&) { qCDebug(KALARM_LOG) << "AkonadiDataModel::slotUpdateDisabledColour"; signalDataChanged(&checkItem_isDisabled, 0, ColumnCount - 1, QModelIndex()); } /****************************************************************************** * Called when the definition of holidays has changed. */ static bool checkItem_excludesHolidays(const Item& item) { if (item.hasPayload()) { const KAEvent event = item.payload(); if (event.isValid() && event.holidaysExcluded()) return true; } return false; } void AkonadiDataModel::slotUpdateHolidays() { qCDebug(KALARM_LOG) << "AkonadiDataModel::slotUpdateHolidays"; Q_ASSERT(TimeToColumn == TimeColumn + 1); // signal should be emitted only for TimeTo and Time columns signalDataChanged(&checkItem_excludesHolidays, TimeColumn, TimeToColumn, QModelIndex()); } /****************************************************************************** * Called when the definition of working hours has changed. */ static bool checkItem_workTimeOnly(const Item& item) { if (item.hasPayload()) { const KAEvent event = item.payload(); if (event.isValid() && event.workTimeOnly()) return true; } return false; } void AkonadiDataModel::slotUpdateWorkingHours() { qCDebug(KALARM_LOG) << "AkonadiDataModel::slotUpdateWorkingHours"; Q_ASSERT(TimeToColumn == TimeColumn + 1); // signal should be emitted only for TimeTo and Time columns signalDataChanged(&checkItem_workTimeOnly, TimeColumn, TimeToColumn, QModelIndex()); } /****************************************************************************** * Reload all collections from Akonadi storage. The backend data is not reloaded. */ void AkonadiDataModel::reload() { qCDebug(KALARM_LOG) << "AkonadiDataModel::reload"; const Collection::List collections = mMonitor->collectionsMonitored(); for (const Collection& collection : collections) { mMonitor->setCollectionMonitored(collection, false); mMonitor->setCollectionMonitored(collection, true); } } /****************************************************************************** * Reload a collection from Akonadi storage. The backend data is not reloaded. */ bool AkonadiDataModel::reload(Resource& resource) { if (!resource.isValid()) return false; qCDebug(KALARM_LOG) << "AkonadiDataModel::reload:" << resource.displayId(); Collection collection(resource.id()); mMonitor->setCollectionMonitored(collection, false); mMonitor->setCollectionMonitored(collection, true); return true; } /****************************************************************************** * Disable the widget if the database engine is not available, and display an * error overlay. */ void AkonadiDataModel::widgetNeedsDatabase(QWidget* widget) { Akonadi::ControlGui::widgetNeedsAkonadi(widget); } /****************************************************************************** * Check for, and remove, any duplicate Akonadi resources, i.e. those which use * the same calendar file/directory. */ void AkonadiDataModel::removeDuplicateResources() { AkonadiResource::removeDuplicateResources(); } /****************************************************************************** * Create an AkonadiResourceCreator instance. */ ResourceCreator* AkonadiDataModel::createResourceCreator(KAlarmCal::CalEvent::Type defaultType, QWidget* parent) { return new AkonadiResourceCreator(defaultType, parent); } /****************************************************************************** * Update a resource's backend calendar file to the current KAlarm format. */ void AkonadiDataModel::updateCalendarToCurrentFormat(Resource& resource, bool ignoreKeepFormat, QObject* parent) { AkonadiCalendarUpdater::updateToCurrentFormat(resource, ignoreKeepFormat, parent); } /****************************************************************************** * Create model instances which are dependent on the resource data model type. */ ResourceListModel* AkonadiDataModel::createResourceListModel(QObject* parent) { return ResourceListModel::create(parent); } ResourceFilterCheckListModel* AkonadiDataModel::createResourceFilterCheckListModel(QObject* parent) { return ResourceFilterCheckListModel::create(parent); } AlarmListModel* AkonadiDataModel::createAlarmListModel(QObject* parent) { return AlarmListModel::create(parent); } AlarmListModel* AkonadiDataModel::allAlarmListModel() { return AlarmListModel::all(); } TemplateListModel* AkonadiDataModel::createTemplateListModel(QObject* parent) { return TemplateListModel::create(parent); } TemplateListModel* AkonadiDataModel::allTemplateListModel() { return TemplateListModel::all(); } /****************************************************************************** * Returns the index to a specified event. */ QModelIndex AkonadiDataModel::eventIndex(const KAEvent& event) const { return itemIndex(Item(mEventIds.value(event.id()).itemId)); } /****************************************************************************** * Returns the index to a specified event. */ QModelIndex AkonadiDataModel::eventIndex(const QString& eventId) const { return itemIndex(Item(mEventIds.value(eventId).itemId)); } /****************************************************************************** * Return all events belonging to a collection. */ QList AkonadiDataModel::events(ResourceId id) const { QList list; const QModelIndex ix = modelIndexForCollection(this, Collection(id)); if (ix.isValid()) getChildEvents(ix, list); for (KAEvent& ev : list) ev.setResourceId(id); return list; } /****************************************************************************** * Recursive function to append all child Events with a given mime type. */ void AkonadiDataModel::getChildEvents(const QModelIndex& parent, QList& events) const { for (int row = 0, count = rowCount(parent); row < count; ++row) { const QModelIndex ix = index(row, 0, parent); const Item item = ix.data(ItemRole).value(); if (item.isValid()) { if (item.hasPayload()) { KAEvent event = item.payload(); if (event.isValid()) events += event; } } else { const Collection c = ix.data(CollectionRole).value(); if (c.isValid()) getChildEvents(ix, events); } } } KAEvent AkonadiDataModel::event(const QString& eventId) const { return event(eventIndex(eventId)); } KAEvent AkonadiDataModel::event(const QModelIndex& ix) const { if (!ix.isValid()) return KAEvent(); Item item = ix.data(ItemRole).value(); Resource r; return event(item, ix, r); } /****************************************************************************** * Return the event for an Item at a specified model index. * The item's parent collection is set, as is the event's collection ID. */ KAEvent AkonadiDataModel::event(Akonadi::Item& item, const QModelIndex& ix, Resource& res) const { //TODO: Tune performance: This function is called very frequently with the same parameters if (ix.isValid()) { const Collection pc = ix.data(ParentCollectionRole).value(); item.setParentCollection(pc); res = resource(pc.id()); if (res.isValid()) { // Fetch the KAEvent defined by the Item, including commandError. return AkonadiResource::event(res, item); } } res = Resource::null(); return KAEvent(); } /****************************************************************************** * Return the up to date Item for a specified Akonadi ID. */ Item AkonadiDataModel::itemById(Item::Id id) const { Item item(id); if (!refresh(item)) return Item(); return item; } /****************************************************************************** * Return the Item for a given event. */ Item AkonadiDataModel::itemForEvent(const QString& eventId) const { const QModelIndex ix = eventIndex(eventId); if (!ix.isValid()) return Item(); return ix.data(ItemRole).value(); } #if 0 /****************************************************************************** * Add an event to a specified Collection. * If the event is scheduled to be added to the collection, it is updated with * its Akonadi item ID. * The event's 'updated' flag is cleared. * Reply = true if item creation has been scheduled. */ bool AkonadiDataModel::addEvent(KAEvent& event, Resource& resource) { qCDebug(KALARM_LOG) << "AkonadiDataModel::addEvent: ID:" << event.id(); if (!resource.addEvent(event)) return false; // Note that the item ID will be inserted in mEventIds after the Akonadi // Item has been created by ItemCreateJob, when slotRowsInserted() is called. mEventIds[event.id()] = EventIds(resource.id()); return true; } #endif /****************************************************************************** * Called when rows have been inserted into the model. */ void AkonadiDataModel::slotRowsInserted(const QModelIndex& parent, int start, int end) { qCDebug(KALARM_LOG) << "AkonadiDataModel::slotRowsInserted:" << start << "-" << end << "(parent =" << parent << ")"; QHash> events; for (int row = start; row <= end; ++row) { const QModelIndex ix = index(row, 0, parent); const Collection collection = ix.data(CollectionRole).value(); if (collection.isValid()) { // A collection has been inserted. Create a new resource to hold it. qCDebug(KALARM_LOG) << "AkonadiDataModel::slotRowsInserted: Collection" << collection.id() << collection.name(); Resource& resource = updateResource(collection); // Ignore it if it isn't owned by a valid Akonadi resource. if (resource.isValid()) { setCollectionChanged(resource, collection, true); Resources::notifyNewResourceInitialised(resource); if (!collection.hasAttribute()) { // If the compatibility attribute is missing at this point, // it doesn't always get notified later, so fetch the // collection to ensure that we see it. AgentInstance agent = AgentManager::self()->instance(collection.resource()); CollectionFetchJob* job = new CollectionFetchJob(Collection::root(), CollectionFetchJob::Recursive); job->fetchScope().setResource(agent.identifier()); connect(job, &CollectionFetchJob::result, instance(), &AkonadiDataModel::collectionFetchResult); } } } else { // An item has been inserted Item item = ix.data(ItemRole).value(); if (item.isValid()) { qCDebug(KALARM_LOG) << "item id=" << item.id() << ", revision=" << item.revision(); Resource res; const KAEvent evnt = event(item, ix, res); // this sets item.parentCollection() if (evnt.isValid()) { qCDebug(KALARM_LOG) << "AkonadiDataModel::slotRowsInserted: Event" << evnt.id(); // Only notify new events if the collection is already populated. // If not populated, all events will be notified when it is // eventually populated. if (res.isPopulated()) events[res] += evnt; mEventIds[evnt.id()] = EventIds(item.parentCollection().id(), item.id()); } // Notify the resource containing the item. AkonadiResource::notifyItemChanged(res, item, true); } } } for (auto it = events.constBegin(); it != events.constEnd(); ++it) { Resource res = it.key(); AkonadiResource::notifyEventsChanged(res, it.value()); } } /****************************************************************************** * Called when a CollectionFetchJob has completed. * Check for and process changes in attribute values. */ void AkonadiDataModel::collectionFetchResult(KJob* j) { CollectionFetchJob* job = qobject_cast(j); if (j->error()) qCWarning(KALARM_LOG) << "AkonadiDataModel::collectionFetchResult: CollectionFetchJob" << job->fetchScope().resource()<< "error: " << j->errorString(); else { const Collection::List collections = job->collections(); for (const Collection& c : collections) { qCDebug(KALARM_LOG) << "AkonadiDataModel::collectionFetchResult:" << c.id(); auto it = mResources.find(c.id()); if (it == mResources.end()) continue; Resource& resource = it.value(); setCollectionChanged(resource, c, false); } } } /****************************************************************************** * Called when rows are about to be removed from the model. */ void AkonadiDataModel::slotRowsAboutToBeRemoved(const QModelIndex& parent, int start, int end) { qCDebug(KALARM_LOG) << "AkonadiDataModel::slotRowsAboutToBeRemoved:" << start << "-" << end << "(parent =" << parent << ")"; QHash> events; for (int row = start; row <= end; ++row) { const QModelIndex ix = index(row, 0, parent); Item item = ix.data(ItemRole).value(); Resource res; const KAEvent evnt = event(item, ix, res); // this sets item.parentCollection() if (evnt.isValid()) { qCDebug(KALARM_LOG) << "AkonadiDataModel::slotRowsAboutToBeRemoved: Collection:" << item.parentCollection().id() << ", Event ID:" << evnt.id(); events[res] += evnt; mEventIds.remove(evnt.id()); } } for (auto it = events.constBegin(); it != events.constEnd(); ++it) { Resource res = it.key(); AkonadiResource::notifyEventsToBeDeleted(res, it.value()); } } /****************************************************************************** * Called when a monitored collection has changed. * Updates the collection held by the collection's resource, and notifies * changes of interest. */ void AkonadiDataModel::slotCollectionChanged(const Akonadi::Collection& c, const QSet& attributeNames) { qCDebug(KALARM_LOG) << "AkonadiDataModel::slotCollectionChanged:" << c.id() << attributeNames; auto it = mResources.find(c.id()); if (it != mResources.end()) { // The Monitor::collectionChanged() signal is not always emitted when // attributes are created! So check whether any attributes not // included in 'attributeNames' have been created. Resource& resource = it.value(); setCollectionChanged(resource, c, attributeNames.contains(CompatibilityAttribute::name())); } } /****************************************************************************** * Called when a monitored collection's properties or content have changed. * Optionally emits a signal if properties of interest have changed. */ void AkonadiDataModel::setCollectionChanged(Resource& resource, const Collection& collection, bool checkCompat) { AkonadiResource::notifyCollectionChanged(resource, collection, checkCompat); if (isMigrating()) { mCollectionIdsBeingCreated.removeAll(collection.id()); if (mCollectionsBeingCreated.isEmpty() && mCollectionIdsBeingCreated.isEmpty() && AkonadiResourceMigrator::completed()) { qCDebug(KALARM_LOG) << "AkonadiDataModel::setCollectionChanged: Migration completed"; setMigrationComplete(); } } } /****************************************************************************** * Called when a monitored collection is removed. */ void AkonadiDataModel::slotCollectionRemoved(const Collection& collection) { const Collection::Id id = collection.id(); qCDebug(KALARM_LOG) << "AkonadiDataModel::slotCollectionRemoved:" << id; mResources.remove(collection.id()); // AkonadiResource will remove the resource from Resources. } /****************************************************************************** * Called when a collection creation is about to start, or has completed. */ void AkonadiDataModel::slotCollectionBeingCreated(const QString& path, Akonadi::Collection::Id id, bool finished) { if (finished) { mCollectionsBeingCreated.removeAll(path); mCollectionIdsBeingCreated << id; } else mCollectionsBeingCreated << path; } /****************************************************************************** * Called when the collection tree has been fetched for the first time. */ void AkonadiDataModel::slotCollectionTreeFetched() { - Resources::notifyResourcesCreated(); + setCalendarsCreated(); } /****************************************************************************** * Called when a collection has been populated. */ void AkonadiDataModel::slotCollectionPopulated(Akonadi::Collection::Id id) { qCDebug(KALARM_LOG) << "AkonadiDataModel::slotCollectionPopulated:" << id; AkonadiResource::notifyCollectionLoaded(id, events(id)); } /****************************************************************************** * Called when calendar migration has completed. */ void AkonadiDataModel::slotMigrationCompleted() { if (mCollectionsBeingCreated.isEmpty() && mCollectionIdsBeingCreated.isEmpty()) { qCDebug(KALARM_LOG) << "AkonadiDataModel: Migration completed"; setMigrationComplete(); } } /****************************************************************************** * Called when an item in the monitored collections has changed. */ void AkonadiDataModel::slotMonitoredItemChanged(const Akonadi::Item& item, const QSet&) { qCDebug(KALARM_LOG) << "AkonadiDataModel::slotMonitoredItemChanged: item id=" << item.id() << ", revision=" << item.revision(); const QModelIndex ix = itemIndex(item); if (ix.isValid()) { Resource res; Item itm = item; KAEvent evnt = event(itm, ix, res); // this sets item.parentCollection() if (evnt.isValid()) { // Notify the resource containing the item. if (res.isValid()) AkonadiResource::notifyItemChanged(res, itm, false); // Wait to ensure that the base EntityTreeModel has processed the // itemChanged() signal first, before we notify AkonadiResource // that the event has changed. mPendingEventChanges.enqueue(evnt); QTimer::singleShot(0, this, &AkonadiDataModel::slotEmitEventUpdated); } } } /****************************************************************************** * Called to Q_EMIT a signal when an event in the monitored collections has * changed. */ void AkonadiDataModel::slotEmitEventUpdated() { while (!mPendingEventChanges.isEmpty()) { const KAEvent event = mPendingEventChanges.dequeue(); Resource res = Resources::resource(event.resourceId()); AkonadiResource::notifyEventsChanged(res, {event}); } } /****************************************************************************** * Refresh the specified Collection with up to date data. * Return: true if successful, false if collection not found. */ bool AkonadiDataModel::refresh(Akonadi::Collection& collection) const { const QModelIndex ix = modelIndexForCollection(this, collection); if (!ix.isValid()) return false; collection = ix.data(CollectionRole).value(); // Also update our own copy of the collection. updateResource(collection); return true; } /****************************************************************************** * Refresh the specified Item with up to date data. * Return: true if successful, false if item not found. */ bool AkonadiDataModel::refresh(Akonadi::Item& item) const { const QModelIndex ix = itemIndex(item); if (!ix.isValid()) return false; item = ix.data(ItemRole).value(); return true; } /****************************************************************************** * Return the AkonadiResource object for a collection ID. */ Resource AkonadiDataModel::resource(Collection::Id id) const { return mResources.value(id, AkonadiResource::nullResource()); } /****************************************************************************** * Return the resource at a specified index, with up to date data. */ Resource AkonadiDataModel::resource(const QModelIndex& ix) const { return mResources.value(ix.data(CollectionIdRole).toLongLong(), AkonadiResource::nullResource()); } /****************************************************************************** * Find the QModelIndex of a resource. */ QModelIndex AkonadiDataModel::resourceIndex(const Resource& resource) const { const Collection& collection = AkonadiResource::collection(resource); const QModelIndex ix = modelIndexForCollection(this, collection); if (!ix.isValid()) return QModelIndex(); return ix; } /****************************************************************************** * Find the QModelIndex of a resource with a given ID. */ QModelIndex AkonadiDataModel::resourceIndex(Akonadi::Collection::Id id) const { const QModelIndex ix = modelIndexForCollection(this, Collection(id)); if (!ix.isValid()) return QModelIndex(); return ix; } /****************************************************************************** * Return a reference to the collection held in a Resource. This is the * definitive copy of the collection used by this model. * Return: the collection held by the model, or null if not found. */ Collection* AkonadiDataModel::collection(Collection::Id id) const { auto it = mResources.find(id); if (it != mResources.end()) { Collection& c = AkonadiResource::collection(it.value()); if (c.isValid()) return &c; } return nullptr; } /****************************************************************************** * Return a reference to the collection held in a Resource. This is the * definitive copy of the collection used by this model. * Return: the collection held by the model, or null if not found. */ Collection* AkonadiDataModel::collection(const Resource& resource) const { return collection(resource.id()); } /****************************************************************************** * Find the QModelIndex of an item. */ QModelIndex AkonadiDataModel::itemIndex(const Akonadi::Item& item) const { const QModelIndexList ixs = modelIndexesForItem(this, item); for (const QModelIndex& ix : ixs) { if (ix.isValid()) return ix; } return QModelIndex(); } /****************************************************************************** * Update the resource which holds a given Collection, by copying the Collection * value into it. If there is no resource, a new resource is created. * Param: collection - this should have been fetched from the model to ensure * that its value is up to date. */ Resource& AkonadiDataModel::updateResource(const Collection& collection) const { auto it = mResources.find(collection.id()); if (it != mResources.end()) { Collection& resourceCol = AkonadiResource::collection(it.value()); if (&collection != &resourceCol) resourceCol = collection; } else { // Create a new resource for the collection. it = mResources.insert(collection.id(), AkonadiResource::create(collection)); } return it.value(); } /****************************************************************************** * Display a message to the user. */ void AkonadiDataModel::slotResourceMessage(ResourceType::MessageType type, const QString& message, const QString& details) { handleResourceMessage(type, message, details); } // vim: et sw=4: diff --git a/src/resources/fileresource.h b/src/resources/fileresource.h index ea00db0d..b50fd252 100644 --- a/src/resources/fileresource.h +++ b/src/resources/fileresource.h @@ -1,402 +1,403 @@ /* * fileresource.h - base class for calendar resource accessed via file system * Program: kalarm * Copyright © 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 FILERESOURCE_H #define FILERESOURCE_H #include "resourcetype.h" #include #include #include #include #include #include class FileResourceSettings; using namespace KAlarmCal; -/** Base class for an alarm calendar resource accessed directly through the file system. - * Public access to this class and derived classes is normally via the Resource class. +/** Abstract base class for an alarm calendar resource accessed directly + * through the file system. Public access to this class and derived classes is + * normally via the Resource class. */ class FileResource : public ResourceType { Q_OBJECT public: /** Current status of resource. */ // IF YOU ALTER THE ORDER OF THIS ENUM, ENSURE THAT ALL VALUES WHICH INDICATE AN // UNUSABLE RESOURCE ARE >= 'Unusable'. enum class Status { Ready, // the resource is ready to use Loading, // the resource is loading, and will be ready soon Saving, // the resource is saving, and will be ready soon Broken, // the resource is in error Unusable, // ... values greater than this indicate an unusable resource Closed, // the resource has been closed. (Closed resources cannot be reopened.) NotConfigured // the resource lacks necessary configuration }; /** Constructor. * Initialises the resource and initiates loading its events. */ explicit FileResource(FileResourceSettings* settings); ~FileResource(); /** Return whether the resource has a valid configuration. */ bool isValid() const override; /** Return the resource's unique ID, as shown to the user. */ ResourceId displayId() const override; /** 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 override; /** Return the location(s) of the resource (URL, file path, etc.) */ QUrl location() const override; /** Return the location of the resource (URL, file path, etc.) * for display purposes. */ QString displayLocation() const override; /** Return the resource's display name. */ QString displayName() const override; /** Return the resource's configuration identifier. This is not the * name normally displayed to the user. */ QString configName() const override; /** Return which types of alarms the resource can contain. */ CalEvent::Types alarmTypes() const override; /** Return which alarm types (active, archived or template) the * resource is enabled for. */ CalEvent::Types enabledTypes() const override; /** 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) override; /** Set which alarm types (active, archived or template) the resource * is enabled for. * @param types alarm types */ void setEnabled(CalEvent::Types types) override; /** Return whether the resource is configured as read-only or is * read-only on disc. */ bool readOnly() const override; /** Specify the read-only configuration status of the resource. */ void setReadOnly(bool); /** 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 override; using ResourceType::isWritable; /** 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; /** Return whether the user has chosen not to update the resource's * calendar storage format. */ bool keepFormat() const override; /** Set or clear whether the user has chosen not to update the resource's * calendar storage format. */ void setKeepFormat(bool keep) override; /** Return the background colour used to display alarms belonging to * this resource. * @return display colour, or invalid if none specified */ QColor backgroundColour() const override; /** 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) override; /** 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 FileResourceManager::isStandard(). * * @param type alarm type */ bool configIsStandard(CalEvent::Type type) const override; /** 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 FileResourceManager::standardTypes(). * * @return alarm types. */ CalEvent::Types configStandardTypes() const override; /** 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 * FileResourceManager::setStandard(). * * @param type alarm type * @param standard true to set as standard, false to clear standard status. */ void configSetStandard(CalEvent::Type type, bool standard) override; /** 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 * FileResourceManager::setStandard(). * * @param types alarm types.to set as standard */ void configSetStandard(CalEvent::Types types) override; /** 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 override; /** Edit the resource's configuration. */ void editResource(QWidget* dialogParent) override; /** Remove the resource. The calendar file is not removed. * @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. */ bool removeResource() override; /** Return the current status of the resource. */ Status status() const { return mStatus; } /** 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) override; /** 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. */ bool save(bool writeThroughCache = true, bool force = false) override; /** Add an event to the resource. * Derived classes must implement event addition in doAddEvent(). */ bool addEvent(const KAEvent&) override; /** Update an event in the resource. Its UID must be unchanged. * Derived classes must implement event update in doUpdateEvent(). */ bool updateEvent(const KAEvent&) override; /** Delete an event from the resource. * Derived classes must implement event deletion in doDeleteEvent(). */ bool deleteEvent(const KAEvent&) override; /** Called to notify the resource that an event's command error has changed. */ void handleCommandErrorChange(const KAEvent&) override; virtual void showProgress(bool) {} /*----------------------------------------------------------------------------- * The methods below are all particular to the FileResource class, and in order * to be accessible to clients are defined as 'static'. *----------------------------------------------------------------------------*/ /** Update a resource to the current KAlarm storage format. */ static bool updateStorageFormat(Resource&); protected: /** Identifier for use in cache file names etc. */ QString identifier() const; /** Find the compatibility of an existing calendar file. */ static KACalendar::Compat getCompatibility(const KCalendarCore::FileStorage::Ptr& fileStorage, int& version); /** Update the resource to the current KAlarm storage format. */ virtual bool updateStorageFmt() = 0; /** This method is called by load() to allow derived classes to implement * loading the resource from its backend, and fetch all events into * @p newEvents. * If the resource is cached, it should be loaded from the cache file (which * if @p readThroughCache is true, should first be downloaded from the * resource file). * If the resource initiates but does not complete loading, loaded() must be * called when loading completes or fails. * @see loaded() * * @param newEvents To be updated to contain the events fetched. * @param readThroughCache If the resource is cached, refresh the cache first. * @return 1 = loading succeeded * 0 = loading has been initiated, but has not yet completed * -1 = loading failed. */ virtual int doLoad(QHash& newEvents, bool readThroughCache, QString& errorMessage) = 0; /** To be called by derived classes on completion of loading the resource, * only if doLoad() initiated but did not complete loading. * @param success true if loading succeeded, false if failed. * @param newEvents The events which have been fetched. */ void loaded(bool success, QHash& newEvents, const QString& errorMessage); /** Schedule the resource for saving. * Derived classes may reimplement this method to delay calling save(), so * as to enable multiple event changes to be saved together. * The default is to call save() immediately. * * @return true if saving succeeded or has been initiated/scheduled. * false if it failed. */ virtual bool scheduleSave(bool writeThroughCache = true) { return save(writeThroughCache); } /** This method is called by save() to allow derived classes to implement * saving the resource to its backend. * If the resource is cached, it should be saved to the cache file (which * if @p writeThroughCache is true, should then be uploaded from the * resource file). * If the resource initiates but does not complete saving, saved() must be * called when saving completes or fails. * @see saved() * * @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 1 = saving succeeded * 0 = saving has been initiated, but has not yet completed * -1 = saving failed. */ virtual int doSave(bool writeThroughCache, bool force, QString& errorMessage) = 0; /** Determine whether the resource can be saved. If not, an error message * will be displayed to the user. */ bool checkSave(); /** To be called by derived classes on completion of saving the resource, * only if doSave() initiated but did not complete saving. * @param success true if saving succeeded, false if failed. */ void saved(bool success, const QString& errorMessage); /** This method is called by addEvent() to allow derived classes to add * an event to the resource. */ virtual bool doAddEvent(const KAEvent&) = 0; /** This method is called by updateEvent() to allow derived classes to update * an event in the resource. The event's UID must be unchanged. */ virtual bool doUpdateEvent(const KAEvent&) = 0; /** This method is called by deleteEvent() to allow derived classes to delete * an event from the resource. */ virtual bool doDeleteEvent(const KAEvent&) = 0; /** Called when settings have changed, to allow derived classes to process * the changes. * @note Resources::notifySettingsChanged() is called after this, to * notify clients. */ virtual void handleSettingsChange(Changes) {} FileResourceSettings* mSettings; // the resource's configuration int mVersion {KACalendar::IncompatibleFormat}; // the calendar format version KACalendar::Compat mCompatibility {KACalendar::Incompatible}; // whether resource is in compatible format /* typedef QHash CompatibilityMap; // indexed by event ID CompatibilityMap mCompatibilityMap; // whether individual events are in compatible format */ Status mStatus {Status::NotConfigured}; // current status of resource }; #endif // FILERESOURCE_H // vim: et sw=4: diff --git a/src/resources/fileresourceconfigmanager.cpp b/src/resources/fileresourceconfigmanager.cpp index 7a931ee1..6c16fedb 100644 --- a/src/resources/fileresourceconfigmanager.cpp +++ b/src/resources/fileresourceconfigmanager.cpp @@ -1,265 +1,264 @@ /* * fileresourceconfigmanager.cpp - config manager for resources accessed via file system * Program: kalarm * Copyright © 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 "fileresourceconfigmanager.h" #include "resources.h" #include "singlefileresource.h" #include "fileresourcecalendarupdater.h" #include "preferences.h" #include "lib/messagebox.h" #include "kalarm_debug.h" #include #include #include #include namespace { // Config file keys const char* GROUP_GENERAL = "General"; const char* KEY_LASTID = "LastId"; } FileResourceConfigManager* FileResourceConfigManager::mInstance {nullptr}; /****************************************************************************** * Creates and returns the unique instance. */ FileResourceConfigManager* FileResourceConfigManager::instance() { if (!mInstance) mInstance = new FileResourceConfigManager; return mInstance; } /****************************************************************************** * Constructor. Reads the current config and creates all resources. */ FileResourceConfigManager::FileResourceConfigManager() { mConfig = new KConfig(QStringLiteral("kalarmresources")); } /****************************************************************************** * Read the current config and create all resources. */ void FileResourceConfigManager::createResources(QObject* parent) { FileResourceConfigManager* manager = instance(); if (manager->mCreated) return; manager->mCreated = 1; - QStringList resourceGroups = manager->mConfig->groupList().filter(QRegularExpression(QStringLiteral("^Resource \\d+$"))); + QStringList resourceGroups = manager->mConfig->groupList().filter(QRegularExpression(QStringLiteral("^Resource_\\d+$"))); if (!resourceGroups.isEmpty()) { std::sort(resourceGroups.begin(), resourceGroups.end(), [](const QString& g1, const QString& g2) { return g1.mid(9).toInt() < g2.mid(9).toInt(); }); KConfigGroup general(manager->mConfig, GROUP_GENERAL); manager->mLastId = general.readEntry(KEY_LASTID, 0) | ResourceType::IdFlag; for (const QString& resourceGroup : resourceGroups) { int groupIndex = resourceGroup.mid(9).toInt(); FileResourceSettings::Ptr settings(new FileResourceSettings(manager->mConfig, resourceGroup)); if (!settings->isValid()) { qCWarning(KALARM_LOG) << "FileResourceConfigManager: Invalid config for" << resourceGroup; manager->mConfig->deleteGroup(resourceGroup); // invalid config for this resource } else { // Check for and remove duplicate URL or 'standard' setting for (auto it = manager->mResources.constBegin(); it != manager->mResources.constEnd(); ++it) { const ResourceData& data = it.value(); if (settings->url() == data.resource.location()) { qCWarning(KALARM_LOG) << "FileResourceConfigManager: Duplicate URL in config for" << resourceGroup; manager->mConfig->deleteGroup(resourceGroup); // invalid config for this resource qCWarning(KALARM_LOG) << "FileResourceConfigManager: Deleted duplicate resource" << settings->displayName(); settings.clear(); break; } const CalEvent::Types std = settings->standardTypes() & data.settings->standardTypes(); if (std) { qCWarning(KALARM_LOG) << "FileResourceConfigManager: Duplicate 'standard' setting in config for" << resourceGroup; settings->setStandard(settings->standardTypes() ^ std); } } if (settings) { Resource resource(createResource(settings)); manager->mResources[settings->id()] = ResourceData(resource, settings); manager->mConfigGroups[groupIndex] = settings->id(); Resources::notifyNewResourceInitialised(resource); // Update the calendar to the current KAlarm format if necessary, and // if the user agrees. FileResourceCalendarUpdater::updateToCurrentFormat(resource, false, parent); } } } // Allow any calendar updater instances to complete and auto-delete. FileResourceCalendarUpdater::waitForCompletion(); } manager->mCreated = 2; - Resources::notifyResourcesCreated(); } /****************************************************************************** * Destructor. Writes the current config. */ FileResourceConfigManager::~FileResourceConfigManager() { writeConfig(); delete mConfig; mInstance = nullptr; } /****************************************************************************** * Writes the 'kalarmresources' config file. */ void FileResourceConfigManager::writeConfig() { // No point in writing unless the config has already been read! if (mInstance) mInstance->mConfig->sync(); } /****************************************************************************** * Return the IDs of all calendar resources. */ QList FileResourceConfigManager::resourceIds() { return instance()->mResources.keys(); } /****************************************************************************** * Create a new calendar resource with the given settings. */ Resource FileResourceConfigManager::addResource(FileResourceSettings::Ptr& settings) { // Find the first unused config group name index. FileResourceConfigManager* manager = instance(); int lastIndex = 0; for (auto it = manager->mConfigGroups.constBegin(); it != manager->mConfigGroups.constEnd(); ++it) { int index = it.key(); if (index > lastIndex + 1) break; lastIndex = index; } const int groupIndex = lastIndex + 1; // Get a unique ID. const int id = ++manager->mLastId; settings->setId(id); // Save the new last-used ID, but strip out IdFlag to make it more legible. KConfigGroup general(manager->mConfig, GROUP_GENERAL); general.writeEntry(KEY_LASTID, id & ~ResourceType::IdFlag); const QString configGroup = groupName(groupIndex); settings->createConfig(manager->mConfig, configGroup); manager->mConfigGroups[groupIndex] = id; Resource resource(createResource(settings)); manager->mResources[id] = ResourceData(resource, settings); Resources::notifyNewResourceInitialised(resource); return resource; } /****************************************************************************** * Delete a calendar resource and its settings. */ bool FileResourceConfigManager::removeResource(Resource& resource) { if (resource.isValid()) { FileResourceConfigManager* manager = instance(); const ResourceId id = resource.id(); const int groupIndex = manager->findResourceGroup(id); if (groupIndex >= 0) { const QString configGroup = groupName(groupIndex); manager->mConfig->deleteGroup(configGroup); manager->mConfigGroups.remove(groupIndex); manager->mResources.remove(id); return true; } } return false; } /****************************************************************************** * Return the available file system resource types handled by the manager. */ QList FileResourceConfigManager::storageTypes() { return { ResourceType::File // , ResourceType::Directory // not currently intended to be implemented }; } /****************************************************************************** * Find the config group for a resource ID. */ int FileResourceConfigManager::findResourceGroup(ResourceId id) const { for (auto it = mConfigGroups.constBegin(); it != mConfigGroups.constEnd(); ++it) if (it.value() == id) return it.key(); return -1; } /****************************************************************************** * Return the config group name for a given config group index. */ QString FileResourceConfigManager::groupName(int groupIndex) { - return QStringLiteral("Resource %1").arg(groupIndex); + return QStringLiteral("Resource_%1").arg(groupIndex); } /****************************************************************************** * Create a new resource with the given settings. */ Resource FileResourceConfigManager::createResource(FileResourceSettings::Ptr& settings) { switch (settings->storageType()) { case FileResourceSettings::File: return SingleFileResource::create(settings.data()); case FileResourceSettings::Directory: // not currently intended to be implemented default: return Resource::null(); } } // vim: et sw=4: diff --git a/src/resources/fileresourceconfigmanager.h b/src/resources/fileresourceconfigmanager.h index ead77538..93f3ffee 100644 --- a/src/resources/fileresourceconfigmanager.h +++ b/src/resources/fileresourceconfigmanager.h @@ -1,101 +1,105 @@ /* * fileresourceconfigmanager.h - config manager for resources accessed via file system * Program: kalarm * Copyright © 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 FILERESOURCECONFIGMANAGER_H #define FILERESOURCECONFIGMANAGER_H #include "resource.h" #include "fileresource.h" #include "fileresourcesettings.h" #include class KConfig; +/** Manager for configuration files for file system resources. + * Reads configuration files and creates resources at startup, and updates + * configuration files when resource configurations change. + */ class FileResourceConfigManager { public: /** Returns the unique instance, and creates it if necessary. * Call createResources() to read the resource configuration and create * the resources defined in it. */ static FileResourceConfigManager* instance(); /** Destructor. * Writes the 'kalarmresources' config file. */ ~FileResourceConfigManager(); /** Reads the 'kalarmresources' config file and creates the resources * defined in it. If called more than once, this method will do nothing. */ static void createResources(QObject* parent); /** Writes the 'kalarmresources' config file. */ static void writeConfig(); /** Return the IDs of all file system calendar resources. */ static QList resourceIds(); /** Create a new file system calendar resource with the given settings. * Use writeConfig() to write the updated config file. * @param settings The resource's configuration; updated with the unique * ID for the resource. */ static Resource addResource(FileResourceSettings::Ptr& settings); /** Delete a specified file system calendar resource and its settings. * The calendar file is not removed. * Use writeConfig() to write the updated config file. * To be called only from FileResource, which will delete the resource * from Resources. */ static bool removeResource(Resource&); /** Return the available file system resource types handled by the manager. */ static QList storageTypes(); private: FileResourceConfigManager(); int findResourceGroup(ResourceId id) const; static QString groupName(int groupIndex); static Resource createResource(FileResourceSettings::Ptr&); struct ResourceData { Resource resource; FileResourceSettings::Ptr settings; ResourceData() {} ResourceData(Resource r, FileResourceSettings::Ptr s) : resource(r), settings(s) {} ResourceData(const ResourceData&) = default; ResourceData& operator=(const ResourceData&) = default; }; static FileResourceConfigManager* mInstance; KConfig* mConfig; QHash mResources; // resource ID, resource & its settings QMap mConfigGroups; // group name index, resource ID ResourceId mLastId {0}; // last ID which was allocated to any resource int mCreated {0}; // 1 = createResources() has been run, 2 = completed }; #endif // FILERESOURCECONFIGMANAGER_H // vim: et sw=4: diff --git a/src/resources/fileresourcedatamodel.cpp b/src/resources/fileresourcedatamodel.cpp index 4ea29bd3..9cc95528 100644 --- a/src/resources/fileresourcedatamodel.cpp +++ b/src/resources/fileresourcedatamodel.cpp @@ -1,1010 +1,1013 @@ /* * fileresourcedatamodel.cpp - model containing file system resources and their events * Program: kalarm * Copyright © 2007-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 "fileresourcedatamodel.h" #include "fileresourcecalendarupdater.h" #include "fileresourcecreator.h" #include "fileresourcemigrator.h" #include "eventmodel.h" #include "resourcemodel.h" #include "resources.h" #include "preferences.h" #include "lib/synchtimer.h" #include "kalarm_debug.h" // Represents a resource or event within the data model. struct FileResourceDataModel::Node { private: KAEvent* eitem; // if type Event, the KAEvent, which is owned by this instance Resource ritem; // if type Resource, the resource Resource owner; // resource containing this KAEvent, or null public: Type type; Node(Resource& r) : ritem(r), type(Type::Resource) {} Node(KAEvent* e, Resource& r) : eitem(e), owner(r), type(Type::Event) {} ~Node() { if (type == Type::Event) delete eitem; } Resource resource() const { return (type == Type::Resource) ? ritem : Resource(); } KAEvent* event() const { return (type == Type::Event) ? eitem : nullptr; } Resource parent() const { return (type == Type::Event) ? owner : Resource(); } }; bool FileResourceDataModel::mInstanceIsOurs = false; /****************************************************************************** * Returns the unique instance, creating it first if necessary. */ FileResourceDataModel* FileResourceDataModel::instance(QObject* parent) { if (!mInstance) { mInstance = new FileResourceDataModel(parent); mInstanceIsOurs = true; } return mInstanceIsOurs ? (FileResourceDataModel*)mInstance : nullptr; } /****************************************************************************** */ FileResourceDataModel::FileResourceDataModel(QObject* parent) : QAbstractItemModel(parent) , ResourceDataModelBase() , mHaveEvents(false) { // Get a list of all resources, and their alarms, if they have already been // created before this, by a previous call to FileResourceConfigManager::createResources(). const QList resourceIds = FileResourceConfigManager::resourceIds(); for (ResourceId id : resourceIds) { Resource resource = Resources::resource(id); if (!mResourceNodes.contains(resource)) addResource(resource); } Resources* resources = Resources::instance(); connect(resources, &Resources::resourceAdded, this, &FileResourceDataModel::addResource); connect(resources, &Resources::resourcePopulated, this, &FileResourceDataModel::slotResourceLoaded); connect(resources, &Resources::resourceToBeRemoved, this, &FileResourceDataModel::slotRemoveResource); connect(resources, &Resources::eventsAdded, this, &FileResourceDataModel::slotEventsAdded); connect(resources, &Resources::eventUpdated, this, &FileResourceDataModel::slotEventUpdated); connect(resources, &Resources::eventsToBeRemoved, this, &FileResourceDataModel::deleteEvents); connect(resources, &Resources::settingsChanged, this, &FileResourceDataModel::slotResourceSettingsChanged); connect(resources, &Resources::resourceMessage, this, &FileResourceDataModel::slotResourceMessage, Qt::QueuedConnection); FileResourceConfigManager::createResources(this); + setCalendarsCreated(); FileResourceMigrator* migrator = FileResourceMigrator::instance(); - if (migrator) + if (!migrator) + setMigrationComplete(); + else { connect(migrator, &QObject::destroyed, this, &FileResourceDataModel::slotMigrationCompleted); setMigrationInitiated(); migrator->execute(); } MinuteTimer::connect(this, SLOT(slotUpdateTimeTo())); Preferences::connect(SIGNAL(archivedColourChanged(QColor)), this, SLOT(slotUpdateArchivedColour(QColor))); Preferences::connect(SIGNAL(disabledColourChanged(QColor)), this, SLOT(slotUpdateDisabledColour(QColor))); Preferences::connect(SIGNAL(holidaysChanged(KHolidays::HolidayRegion)), this, SLOT(slotUpdateHolidays())); Preferences::connect(SIGNAL(workTimeChanged(QTime,QTime,QBitArray)), this, SLOT(slotUpdateWorkingHours())); } /****************************************************************************** */ FileResourceDataModel::~FileResourceDataModel() { qCDebug(KALARM_LOG) << "FileResourceDataModel::~FileResourceDataModel"; if (mInstance == this) { mInstance = nullptr; mInstanceIsOurs = false; } delete Resources::instance(); } /****************************************************************************** * Return whether a model index refers to a resource or an event. */ FileResourceDataModel::Type FileResourceDataModel::type(const QModelIndex& ix) const { if (ix.isValid()) { const Node* node = reinterpret_cast(ix.internalPointer()); if (node) return node->type; } return Type::Error; } /****************************************************************************** * Return the resource with the given ID. */ Resource FileResourceDataModel::resource(ResourceId id) const { return Resource(Resources::resource(id)); } /****************************************************************************** * Return the resource referred to by an index, or invalid resource if the index * is not for a resource. */ Resource FileResourceDataModel::resource(const QModelIndex& ix) const { if (ix.isValid()) { const Node* node = reinterpret_cast(ix.internalPointer()); if (node) { Resource res = node->resource(); if (!res.isNull()) return res; } } return Resource(); } /****************************************************************************** * Find the QModelIndex of a resource. */ QModelIndex FileResourceDataModel::resourceIndex(const Resource& resource) const { if (resource.isValid()) { int row = mResources.indexOf(const_cast(resource)); if (row >= 0) return createIndex(row, 0, mResourceNodes.value(Resource()).at(row)); } return QModelIndex(); } /****************************************************************************** * Find the QModelIndex of a resource. */ QModelIndex FileResourceDataModel::resourceIndex(ResourceId id) const { return resourceIndex(Resources::resource(id)); } /****************************************************************************** * Return the event with the given ID. */ KAEvent FileResourceDataModel::event(const QString& eventId) const { const Node* node = mEventNodes.value(eventId, nullptr); if (node) { KAEvent* event = node->event(); if (event) return *event; } return KAEvent(); } /****************************************************************************** * Return the event referred to by an index, or invalid event if the index is * not for an event. */ KAEvent FileResourceDataModel::event(const QModelIndex& ix) const { if (ix.isValid()) { const Node* node = reinterpret_cast(ix.internalPointer()); if (node) { const KAEvent* event = node->event(); if (event) return *event; } } return KAEvent(); } /****************************************************************************** * Return the index to a specified event. */ QModelIndex FileResourceDataModel::eventIndex(const QString& eventId) const { Node* node = mEventNodes.value(eventId, nullptr); if (node) { Resource resource = node->parent(); if (resource.isValid()) { const QVector nodes = mResourceNodes.value(resource); int row = nodes.indexOf(node); if (row >= 0) return createIndex(row, 0, node); } } return QModelIndex(); } /****************************************************************************** * Return the index to a specified event. */ QModelIndex FileResourceDataModel::eventIndex(const KAEvent& event) const { return eventIndex(event.id()); } /****************************************************************************** * Add an event to a specified resource. * The event's 'updated' flag is cleared. * Reply = true if item creation has been scheduled. */ bool FileResourceDataModel::addEvent(KAEvent& event, Resource& resource) { qCDebug(KALARM_LOG) << "FileResourceDataModel::addEvent: ID:" << event.id(); return resource.addEvent(event); } /****************************************************************************** * Recursive function to Q_EMIT the dataChanged() signal for all events in a * specified column range. */ void FileResourceDataModel::signalDataChanged(bool (*checkFunc)(const KAEvent*), int startColumn, int endColumn, const QModelIndex& parent) { int start = -1; int end = -1; for (int row = 0, count = rowCount(parent); row < count; ++row) { KAEvent* event = nullptr; const QModelIndex ix = index(row, 0, parent); const Node* node = reinterpret_cast(ix.internalPointer()); if (node) { event = node->event(); if (event) { if ((*checkFunc)(event)) { // For efficiency, emit a single signal for each group of // consecutive events, rather than a separate signal for each event. if (start < 0) start = row; end = row; continue; } } } if (start >= 0) Q_EMIT dataChanged(index(start, startColumn, parent), index(end, endColumn, parent)); start = -1; if (!event) signalDataChanged(checkFunc, startColumn, endColumn, ix); } if (start >= 0) Q_EMIT dataChanged(index(start, startColumn, parent), index(end, endColumn, parent)); } void FileResourceDataModel::slotMigrationCompleted() { qCDebug(KALARM_LOG) << "FileResourceDataModel: Migration completed"; setMigrationComplete(); } /****************************************************************************** * Signal every minute that the time-to-alarm values have changed. */ static bool checkEvent_isActive(const KAEvent* event) { return event->category() == CalEvent::ACTIVE; } void FileResourceDataModel::slotUpdateTimeTo() { signalDataChanged(&checkEvent_isActive, TimeToColumn, TimeToColumn, QModelIndex()); } /****************************************************************************** * Called when the colour used to display archived alarms has changed. */ static bool checkEvent_isArchived(const KAEvent* event) { return event->category() == CalEvent::ARCHIVED; } void FileResourceDataModel::slotUpdateArchivedColour(const QColor&) { qCDebug(KALARM_LOG) << "FileResourceDataModel::slotUpdateArchivedColour"; signalDataChanged(&checkEvent_isArchived, 0, ColumnCount - 1, QModelIndex()); } /****************************************************************************** * Called when the colour used to display disabled alarms has changed. */ static bool checkEvent_isDisabled(const KAEvent* event) { return !event->enabled(); } void FileResourceDataModel::slotUpdateDisabledColour(const QColor&) { qCDebug(KALARM_LOG) << "FileResourceDataModel::slotUpdateDisabledColour"; signalDataChanged(&checkEvent_isDisabled, 0, ColumnCount - 1, QModelIndex()); } /****************************************************************************** * Called when the definition of holidays has changed. * Update the next trigger time for all alarms which are set to recur only on * non-holidays. */ static bool checkEvent_excludesHolidays(const KAEvent* event) { return event->holidaysExcluded(); } void FileResourceDataModel::slotUpdateHolidays() { qCDebug(KALARM_LOG) << "FileResourceDataModel::slotUpdateHolidays"; Q_ASSERT(TimeToColumn == TimeColumn + 1); // signal should be emitted only for TimeTo and Time columns signalDataChanged(&checkEvent_excludesHolidays, TimeColumn, TimeToColumn, QModelIndex()); } /****************************************************************************** * Called when the definition of working hours has changed. * Update the next trigger time for all alarms which are set to recur only * during working hours. */ static bool checkEvent_workTimeOnly(const KAEvent* event) { return event->workTimeOnly(); } void FileResourceDataModel::slotUpdateWorkingHours() { qCDebug(KALARM_LOG) << "FileResourceDataModel::slotUpdateWorkingHours"; Q_ASSERT(TimeToColumn == TimeColumn + 1); // signal should be emitted only for TimeTo and Time columns signalDataChanged(&checkEvent_workTimeOnly, TimeColumn, TimeToColumn, QModelIndex()); } /****************************************************************************** * Called when loading of a resource is complete. */ void FileResourceDataModel::slotResourceLoaded(Resource& resource) { qCDebug(KALARM_LOG) << "FileResourceDataModel::slotResourceLoaded:" << resource.displayName(); addResource(resource); } /****************************************************************************** * Called when a resource setting has changed. */ void FileResourceDataModel::slotResourceSettingsChanged(Resource& res, ResourceType::Changes change) { if (change & ResourceType::Enabled) { if (res.enabledTypes()) { qCDebug(KALARM_LOG) << "FileResourceDataModel::slotResourceSettingsChanged: Enabled" << res.displayName(); addResource(res); } else { qCDebug(KALARM_LOG) << "FileResourceDataModel::slotResourceSettingsChanged: Disabled" << res.displayName(); removeResourceEvents(res); } } if (change & (ResourceType::Name | ResourceType::Standard | ResourceType::ReadOnly)) { qCDebug(KALARM_LOG) << "FileResourceDataModel::slotResourceSettingsChanged:" << res.displayName(); const QModelIndex resourceIx = resourceIndex(res); if (resourceIx.isValid()) Q_EMIT dataChanged(resourceIx, resourceIx); } if (change & ResourceType::BackgroundColour) { qCDebug(KALARM_LOG) << "FileResourceDataModel::slotResourceSettingsChanged: Colour" << res.displayName(); const QVector& eventNodes = mResourceNodes.value(res); const int lastRow = eventNodes.count() - 1; if (lastRow >= 0) Q_EMIT dataChanged(createIndex(0, 0, eventNodes[0]), createIndex(lastRow, ColumnCount - 1, eventNodes[lastRow])); } // if (change & (ResourceType::AlarmTypes | ResourceType::KeepFormat | ResourceType::UpdateFormat)) // qCDebug(KALARM_LOG) << "FileResourceDataModel::slotResourceSettingsChanged: UNHANDLED" << res.displayName(); } /****************************************************************************** * Called when events have been added to a resource. Add events to the list. */ void FileResourceDataModel::slotEventsAdded(Resource& resource, const QList& events) { if (events.isEmpty()) return; qCDebug(KALARM_LOG) << "FileResourceDataModel::slotEventsAdded:" << resource.displayId() << "Count:" << events.count(); if (mResourceNodes.contains(resource)) { // If events with the same ID already exist, remove them first. QList eventsToAdd = events; { QList eventsToDelete; for (int i = eventsToAdd.count(); --i >= 0; ) { const KAEvent& event = eventsToAdd.at(i); const Node* dnode = mEventNodes.value(event.id(), nullptr); if (dnode) { if (dnode->parent() != resource) { qCWarning(KALARM_LOG) << "FileResourceDataModel::slotEventsAdded: Event ID already exists in another resource"; eventsToAdd.removeAt(i); } else eventsToDelete << *dnode->event(); } } if (!eventsToDelete.isEmpty()) deleteEvents(resource, eventsToDelete); } if (!eventsToAdd.isEmpty()) { QVector& resourceEventNodes = mResourceNodes[resource]; int row = resourceEventNodes.count(); const QModelIndex resourceIx = resourceIndex(resource); beginInsertRows(resourceIx, row, row + eventsToAdd.count() - 1); for (const KAEvent& event : qAsConst(eventsToAdd)) { KAEvent* ev = new KAEvent(event); ev->setResourceId(resource.id()); Node* node = new Node(ev, resource); resourceEventNodes += node; mEventNodes[ev->id()] = node; } endInsertRows(); if (!mHaveEvents) updateHaveEvents(true); } } } /****************************************************************************** * Update an event which already exists (and with the same UID) in the model. */ void FileResourceDataModel::slotEventUpdated(Resource& resource, const KAEvent& event) { auto it = mEventNodes.find(event.id()); if (it != mEventNodes.end()) { Node* node = it.value(); if (node && node->parent() == resource) { KAEvent* oldEvent = node->event(); if (oldEvent) { *oldEvent = event; const QVector eventNodes = mResourceNodes.value(resource); int row = eventNodes.indexOf(node); if (row >= 0) { const QModelIndex resourceIx = resourceIndex(resource); Q_EMIT dataChanged(index(row, 0, resourceIx), index(row, ColumnCount - 1, resourceIx)); } } } } } /****************************************************************************** * Delete events from their resource. */ bool FileResourceDataModel::deleteEvents(Resource& resource, const QList& events) { qCDebug(KALARM_LOG) << "FileResourceDataModel::deleteEvents:" << resource.displayName() << "Count:" << events.count(); QModelIndex resourceIx = resourceIndex(resource); if (!resourceIx.isValid()) return false; auto it = mResourceNodes.find(resource); if (it == mResourceNodes.end()) return false; QVector& eventNodes = it.value(); // Find the row numbers of the events to delete. QVector rowsToDelete; for (const KAEvent& event : events) { Node* node = mEventNodes.value(event.id(), nullptr); if (node && node->parent() == resource) { int row = eventNodes.indexOf(node); if (row >= 0) rowsToDelete << row; } } // Delete the events in groups of consecutive rows (if any). std::sort(rowsToDelete.begin(), rowsToDelete.end()); for (int i = 0, count = rowsToDelete.count(); i < count; ) { int row = rowsToDelete.at(i); int lastRow = row; while (++i < count && rowsToDelete.at(i) == lastRow + 1) ++lastRow; beginRemoveRows(resourceIx, row, lastRow); do { Node* node = eventNodes.at(row); eventNodes.removeAt(row); mEventNodes.remove(node->event()->id()); delete node; } while (++row <= lastRow); endRemoveRows(); } if (mHaveEvents && mEventNodes.isEmpty()) updateHaveEvents(false); return true; } /****************************************************************************** * Add a resource and all its events into the model. */ void FileResourceDataModel::addResource(Resource& resource) { // Get the events to add to the model const QList events = resource.events(); qCDebug(KALARM_LOG) << "FileResourceDataModel::addResource" << resource.displayName() << ", event count:" << events.count(); const QModelIndex resourceIx = resourceIndex(resource); if (resourceIx.isValid()) { // The resource already exists: remove its existing events from the model. bool noEvents = events.isEmpty(); removeResourceEvents(resource, noEvents); if (noEvents) return; beginInsertRows(resourceIx, 0, events.count() - 1); } else { // Add the new resource to the model QVector& resourceNodes = mResourceNodes[Resource()]; int row = resourceNodes.count(); beginInsertRows(QModelIndex(), row, row); mResources += resource; resourceNodes += new Node(resource); mResourceNodes.insert(resource, QVector()); } if (!events.isEmpty()) { QVector& resourceEventNodes = mResourceNodes[resource]; for (const KAEvent& event : events) { Node* node = new Node(new KAEvent(event), resource); resourceEventNodes += node; mEventNodes[event.id()] = node; } } endInsertRows(); if (!mHaveEvents && !mEventNodes.isEmpty()) updateHaveEvents(true); else if (mHaveEvents && mEventNodes.isEmpty()) updateHaveEvents(false); } /****************************************************************************** * Remove a resource and its events from the list. * This has to be called before the resource is actually deleted or reloaded. If * not, timer based updates can occur between the resource being deleted and * slotResourceSettingsChanged(Deleted) being triggered, leading to crashes when * data from the resource's events is fetched. */ void FileResourceDataModel::slotRemoveResource(Resource& resource) { qCDebug(KALARM_LOG) << "FileResourceDataModel::slotRemoveResource" << resource.displayName(); int row = mResources.indexOf(resource); if (row < 0) return; int count = 0; beginRemoveRows(QModelIndex(), row, row); mResources.removeAt(row); mResourceNodes[Resource()].removeAt(row); auto it = mResourceNodes.find(resource); if (it != mResourceNodes.end()) { count = removeResourceEvents(it.value()); mResourceNodes.erase(it); } endRemoveRows(); if (count) { if (mHaveEvents && mEventNodes.isEmpty()) updateHaveEvents(false); } } /****************************************************************************** * Remove a resource's events from the list. */ void FileResourceDataModel::removeResourceEvents(Resource& resource, bool setHaveEvents) { qCDebug(KALARM_LOG) << "FileResourceDataModel::removeResourceEvents" << resource.displayName(); const QModelIndex resourceIx = resourceIndex(resource); if (resourceIx.isValid()) { // The resource already exists: remove its existing events from the model. QVector& eventNodes = mResourceNodes[resource]; if (!eventNodes.isEmpty()) { beginRemoveRows(resourceIx, 0, eventNodes.count() - 1); int count = removeResourceEvents(eventNodes); endRemoveRows(); if (count && setHaveEvents) { if (mHaveEvents && mEventNodes.isEmpty()) updateHaveEvents(false); } } } } /****************************************************************************** * Remove a resource's events from the list. * beginRemoveRows() must be called before this method, and endRemoveRows() * afterwards. Then, removeConfigEvents() must be called with the return value * from this method as a parameter. * Return - number of events which have been removed. */ int FileResourceDataModel::removeResourceEvents(QVector& eventNodes) { qCDebug(KALARM_LOG) << "FileResourceDataModel::removeResourceEvents"; int count = 0; for (Node* node : eventNodes) { KAEvent* event = node->event(); if (event) { const QString eventId = event->id(); mEventNodes.remove(eventId); ++count; } delete node; } eventNodes.clear(); return count; } /****************************************************************************** * Terminate access to the data model, and tidy up. */ void FileResourceDataModel::terminate() { delete mInstance; } /****************************************************************************** * Reload all resources' data from storage. */ void FileResourceDataModel::reload() { for (int i = 0, iMax = mResources.count(); i < iMax; ++i) mResources[i].reload(); } /****************************************************************************** * Reload a resource's data from storage. */ bool FileResourceDataModel::reload(Resource& resource) { if (!resource.isValid()) return false; qCDebug(KALARM_LOG) << "FileResourceDataModel::reload:" << resource.displayId(); return resource.reload(); } /****************************************************************************** * Create a FileResourceCreator instance. */ ResourceCreator* FileResourceDataModel::createResourceCreator(KAlarmCal::CalEvent::Type defaultType, QWidget* parent) { return new FileResourceCreator(defaultType, parent); } /****************************************************************************** * Update a resource's backend calendar file to the current KAlarm format. */ void FileResourceDataModel::updateCalendarToCurrentFormat(Resource& resource, bool ignoreKeepFormat, QObject* parent) { FileResourceCalendarUpdater::updateToCurrentFormat(resource, ignoreKeepFormat, parent); } /****************************************************************************** * Create model instances which are dependent on the resource data model type. */ ResourceListModel* FileResourceDataModel::createResourceListModel(QObject* parent) { return ResourceListModel::create(parent); } ResourceFilterCheckListModel* FileResourceDataModel::createResourceFilterCheckListModel(QObject* parent) { return ResourceFilterCheckListModel::create(parent); } AlarmListModel* FileResourceDataModel::createAlarmListModel(QObject* parent) { return AlarmListModel::create(parent); } AlarmListModel* FileResourceDataModel::allAlarmListModel() { return AlarmListModel::all(); } TemplateListModel* FileResourceDataModel::createTemplateListModel(QObject* parent) { return TemplateListModel::create(parent); } TemplateListModel* FileResourceDataModel::allTemplateListModel() { return TemplateListModel::all(); } /****************************************************************************** * Return the number of children of a model index. */ int FileResourceDataModel::rowCount(const QModelIndex& parent) const { if (!parent.isValid()) return mResourceNodes.keys().count() - 1; const Node* node = reinterpret_cast(parent.internalPointer()); if (node && node->type == Type::Resource) return mResourceNodes.value(node->resource()).count(); return 0; } /****************************************************************************** * Return the number of columns for children of a model index. */ int FileResourceDataModel::columnCount(const QModelIndex& parent) const { // Although the number of columns differs between resources and events, // returning different values here doesn't work. So return the maximum // number of columns. Q_UNUSED(parent); return ColumnCount; } /****************************************************************************** * Return the model index for a specified item. */ QModelIndex FileResourceDataModel::index(int row, int column, const QModelIndex& parent) const { if (row >= 0 && column >= 0) { if (!parent.isValid()) { if (!column) { const QVector& nodes = mResourceNodes.value(Resource()); if (row >= 0 && row < nodes.count()) return createIndex(row, column, nodes[row]); } } else { if (column < ColumnCount) { const Node* node = reinterpret_cast(parent.internalPointer()); if (node) { Resource resource = node->resource(); if (resource.isValid()) { const QVector& nodes = mResourceNodes.value(resource); if (row < nodes.count()) return createIndex(row, column, nodes[row]); } } } } } return QModelIndex(); } /****************************************************************************** * Return the model index for the parent of a specified item. */ QModelIndex FileResourceDataModel::parent(const QModelIndex& ix) const { const Node* node = reinterpret_cast(ix.internalPointer()); if (node) { Resource resource = node->parent(); if (resource.isValid()) { int row = mResources.indexOf(resource); if (row >= 0) return createIndex(row, 0, mResourceNodes.value(Resource()).at(row)); } } return QModelIndex(); } /****************************************************************************** * Return the indexes which match a data value in the 'start' index column. */ QModelIndexList FileResourceDataModel::match(const QModelIndex& start, int role, const QVariant& value, int hits, Qt::MatchFlags flags) const { switch (role) { case ResourceIdRole: { QModelIndexList result; const ResourceId id = value.toLongLong(); if (id >= 0) { const QModelIndex ix = resourceIndex(id); if (ix.isValid()) result += ix; } return result; } case EventIdRole: { QModelIndexList result; const QModelIndex ix = eventIndex(value.toString()); if (ix.isValid()) result += ix; return result; } default: break; } return QAbstractItemModel::match(start, role, value, hits, flags); } /****************************************************************************** * Return the data for a given role, for a specified item. */ QVariant FileResourceDataModel::data(const QModelIndex& ix, int role) const { const Node* node = reinterpret_cast(ix.internalPointer()); if (node) { const Resource res = node->resource(); if (!res.isNull()) { bool handled; const QVariant value = resourceData(role, res, handled); if (handled) return value; switch (role) { case Qt::CheckStateRole: return res.enabledTypes() ? Qt::Checked : Qt::Unchecked; default: break; } } else { KAEvent* event = node->event(); if (event) { // This is an Event row if (role == ParentResourceIdRole) return node->parent().id(); const Resource res = node->parent(); bool handled; const QVariant value = eventData(role, ix.column(), *event, res, handled); if (handled) return value; } } // Return invalid value switch (role) { case ItemTypeRole: return static_cast(Type::Error); case ResourceIdRole: case ParentResourceIdRole: return -1; case StatusRole: default: break; } } return QVariant(); } /****************************************************************************** * Set the data for a given role, for a specified item. */ bool FileResourceDataModel::setData(const QModelIndex& ix, const QVariant& value, int role) { // NOTE: need to Q_EMIT dataChanged() whenever something is updated (except via a job). const Node* node = reinterpret_cast(ix.internalPointer()); if (!node) return false; Resource resource = node->resource(); if (resource.isNull()) { // This is an Event row KAEvent* event = node->event(); if (event && event->isValid()) { switch (role) { case Qt::EditRole: { int row = ix.row(); Q_EMIT dataChanged(index(row, 0, ix.parent()), index(row, ColumnCount - 1, ix.parent())); return true; } default: break; } } } return QAbstractItemModel::setData(ix, value, role); } /****************************************************************************** * Return data for a column heading. */ QVariant FileResourceDataModel::headerData(int section, Qt::Orientation orientation, int role) const { bool handled; QVariant value = ResourceDataModelBase::headerData(section, orientation, role, true, handled); if (handled) return value; return QVariant(); } Qt::ItemFlags FileResourceDataModel::flags(const QModelIndex& ix) const { if (!ix.isValid()) return Qt::ItemIsEnabled; return Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsEditable | Qt::ItemIsDragEnabled; } /****************************************************************************** * Display a message to the user. */ void FileResourceDataModel::slotResourceMessage(ResourceType::MessageType type, const QString& message, const QString& details) { handleResourceMessage(type, message, details); } // vim: et sw=4: diff --git a/src/resources/fileresourcemigrator.cpp b/src/resources/fileresourcemigrator.cpp index eaac340e..b2a880d5 100644 --- a/src/resources/fileresourcemigrator.cpp +++ b/src/resources/fileresourcemigrator.cpp @@ -1,512 +1,527 @@ /* * fileresourcemigrator.cpp - migrates or creates KAlarm non-Akonadi resources * Program: kalarm * Copyright © 2011-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 "fileresourcemigrator.h" #include "dirresourceimportdialog.h" #include "fileresourcecalendarupdater.h" #include "fileresourceconfigmanager.h" #include "resources.h" #include "calendarfunctions.h" #include "preferences.h" #include "lib/autoqpointer.h" #include "lib/desktop.h" #include "kalarm_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace KAlarmCal; namespace { const QString KALARM_RESOURCE(QStringLiteral("akonadi_kalarm_resource")); const QString KALARM_DIR_RESOURCE(QStringLiteral("akonadi_kalarm_dir_resource")); const Akonadi::Collection::Rights WritableRights = Akonadi::Collection::CanChangeItem | Akonadi::Collection::CanCreateItem | Akonadi::Collection::CanDeleteItem; bool readDirectoryResource(const QString& dirPath, CalEvent::Types alarmTypes, QHash>& events); } FileResourceMigrator* FileResourceMigrator::mInstance = nullptr; bool FileResourceMigrator::mCompleted = false; /****************************************************************************** * Constructor. */ FileResourceMigrator::FileResourceMigrator(QObject* parent) : QObject(parent) { } FileResourceMigrator::~FileResourceMigrator() { qCDebug(KALARM_LOG) << "~FileResourceMigrator"; mInstance = nullptr; } /****************************************************************************** * Create and return the unique FileResourceMigrator instance. */ FileResourceMigrator* FileResourceMigrator::instance() { if (!mInstance && !mCompleted) + { + // Check whether migration or default resource creation is actually needed. + CalEvent::Types needed = CalEvent::ACTIVE | CalEvent::ARCHIVED | CalEvent::TEMPLATE; + const QVector resources = Resources::allResources(); + for (const Resource& resource : resources) + { + needed &= ~resource.alarmTypes(); + if (!needed) + { + mCompleted = true; + return mInstance; + } + } + // Migration or default resource creation is required. mInstance = new FileResourceMigrator; + } return mInstance; } /****************************************************************************** * Migrate old Akonadi or KResource calendars, and create default file system * resources. */ void FileResourceMigrator::execute() { if (mCompleted) { deleteLater(); return; } - qCDebug(KALARM_LOG) << "FileResourceMigrator::migrateOrCreate"; + qCDebug(KALARM_LOG) << "FileResourceMigrator::execute"; // First, check whether any file system resources already exist, and if so, // find their alarm types. const QVector resources = Resources::allResources(); for (const Resource& resource : resources) mExistingAlarmTypes |= resource.alarmTypes(); if (mExistingAlarmTypes != CalEvent::EMPTY) { // Some file system resources already exist, so no migration is // required. Create any missing default file system resources. createDefaultResources(); } else { // There are no file system resources, so migrate any Akonadi resources. mMigratingAkonadi = true; connect(Akonadi::ServerManager::self(), &Akonadi::ServerManager::stateChanged, this, &FileResourceMigrator::migrateAkonadiResources); migrateAkonadiResources(Akonadi::ServerManager::state()); // Migration of Akonadi collections has now been initiated. On // completion, any missing default resources will be created. if (!mMigratingAkonadi) { // There are no Akonadi resources, so migrate any KResources alarm // calendars from pre-Akonadi versions of KAlarm. migrateKResources(); } } // Allow any calendar updater instances to complete and auto-delete. FileResourceCalendarUpdater::waitForCompletion(); } /****************************************************************************** * Called when the Akonadi server manager changes state. * Once it is running, migrate any Akonadi KAlarm resources. */ void FileResourceMigrator::migrateAkonadiResources(Akonadi::ServerManager::State state) { switch (state) { case Akonadi::ServerManager::Running: { qCDebug(KALARM_LOG) << "FileResourceMigrator::migrateAkonadiResources: initiated"; Akonadi::AttributeFactory::registerAttribute(); const Akonadi::AgentInstance::List agents = Akonadi::AgentManager::self()->instances(); // First, migrate KAlarm calendar file resources. // This will allow any KAlarm directory resources to be merged into // single file resources, if the user prefers that. for (const Akonadi::AgentInstance& agent : agents) { const QString type = agent.type().identifier(); if (type == KALARM_RESOURCE) { // Fetch the resource's collection to determine its alarm types Akonadi::CollectionFetchJob* job = new Akonadi::CollectionFetchJob(Akonadi::Collection::root(), Akonadi::CollectionFetchJob::FirstLevel); job->fetchScope().setResource(agent.identifier()); mFetchesPending << job; connect(job, &KJob::result, this, &FileResourceMigrator::collectionFetchResult); mMigrateKResources = false; // ignore KResources if Akonadi resources exist } } // Now migrate KAlarm directory resources, which must be merged // or converted into single file resources. for (const Akonadi::AgentInstance& agent : agents) { const QString type = agent.type().identifier(); if (type == KALARM_DIR_RESOURCE) { // Fetch the resource's collection to determine its alarm types Akonadi::CollectionFetchJob* job = new Akonadi::CollectionFetchJob(Akonadi::Collection::root(), Akonadi::CollectionFetchJob::FirstLevel); job->fetchScope().setResource(agent.identifier()); mFetchesPending << job; connect(job, &KJob::result, this, &FileResourceMigrator::collectionFetchResult); mMigrateKResources = false; // ignore KResources if Akonadi resources exist } } if (mFetchesPending.isEmpty()) mMigratingAkonadi = false; // there are no Akonadi resources to migrate break; } case Akonadi::ServerManager::Stopping: // Wait until the server has stopped, so that we can restart it. return; default: if (Akonadi::ServerManager::start()) return; // wait for the server to change to Running state // Can't start Akonadi, so give up trying to migrate. qCWarning(KALARM_LOG) << "FileResourceMigrator::migrateAkonadiResources: Failed to start Akonadi server"; mMigratingAkonadi = false; break; } disconnect(Akonadi::ServerManager::self(), nullptr, this, nullptr); if (mMigrateKResources) migrateKResources(); } /****************************************************************************** * Called when an Akonadi collection fetch job has completed. * Migrate the collection to a file system resource. */ void FileResourceMigrator::collectionFetchResult(KJob* j) { Akonadi::CollectionFetchJob* job = static_cast(j); const QString id = job->fetchScope().resource(); if (j->error()) qCCritical(KALARM_LOG) << "FileResourceMigrator::collectionFetchResult: CollectionFetchJob" << id << "error: " << j->errorString(); else { const Akonadi::Collection::List collections = job->collections(); if (collections.isEmpty()) qCCritical(KALARM_LOG) << "FileResourceMigrator::collectionFetchResult: No collections found for resource" << id; else { // Migrate this collection. const Akonadi::Collection& collection(collections[0]); const Akonadi::AgentInstance agent = Akonadi::AgentManager::self()->instance(collection.resource()); const QString resourceType = agent.type().identifier(); const bool readOnly = (collection.rights() & WritableRights) != WritableRights; const CalEvent::Types alarmTypes = CalEvent::types(collections[0].contentMimeTypes()); CalEvent::Types enabledTypes = CalEvent::EMPTY; CalEvent::Types standardTypes = CalEvent::EMPTY; QColor backgroundColour; if (collection.hasAttribute()) { const CollectionAttribute* attr = collection.attribute(); enabledTypes = attr->enabled(); standardTypes = attr->standard(); backgroundColour = attr->backgroundColor(); } if (resourceType == KALARM_RESOURCE) { qCDebug(KALARM_LOG) << "FileResourceMigrator: Creating resource" << collection.displayName() << ", alarm types:" << alarmTypes << ", standard types:" << standardTypes; FileResourceSettings::Ptr settings(new FileResourceSettings( FileResourceSettings::File, QUrl::fromUserInput(collection.remoteId(), QString(), QUrl::AssumeLocalFile), alarmTypes, collection.displayName(), backgroundColour, enabledTypes, standardTypes, readOnly)); Resource resource = FileResourceConfigManager::addResource(settings); // Don't delete the Akonadi resource in case it is wanted by any other application //Akonadi::AgentManager::self()->removeInstance(agent); // Update the calendar to the current KAlarm format if necessary, // and if the user agrees. FileResourceCalendarUpdater* updater = new FileResourceCalendarUpdater(resource, true, this); connect(updater, &QObject::destroyed, this, &FileResourceMigrator::checkIfComplete); updater->update(); // note that 'updater' will auto-delete when finished mExistingAlarmTypes |= alarmTypes; } else if (resourceType == KALARM_DIR_RESOURCE) { // Convert Akonadi directory resource to single file resources. // Use AutoQPointer to guard against crash on application exit while // the dialogue is still open. It prevents double deletion (both on // deletion of parent, and on return from this function). AutoQPointer dlg = new DirResourceImportDialog(collection.displayName(), collection.remoteId(), alarmTypes, Desktop::mainWindow()); if (dlg->exec() == QDialog::Accepted) { if (dlg) { QHash> events; readDirectoryResource(collection.remoteId(), alarmTypes, events); for (auto it = events.constBegin(); it != events.constEnd(); ++it) { const CalEvent::Type alarmType = it.key(); Resource resource; const ResourceId id = dlg->resourceId(alarmType); if (id >= 0) { // The directory resource's alarms are to be // imported into an existing resource. resource = Resources::resource(id); } else { const QUrl destUrl = dlg->url(alarmType); if (!destUrl.isValid()) continue; // this alarm type is not to be imported // The directory resource's alarms are to be // imported into a new resource. qCDebug(KALARM_LOG) << "FileResourceMigrator: Creating resource" << dlg->displayName(alarmType) << ", type:" << alarmType << ", standard:" << (bool)(standardTypes & alarmType); FileResourceSettings::Ptr settings(new FileResourceSettings( FileResourceSettings::File, destUrl, alarmType, dlg->displayName(alarmType), backgroundColour, enabledTypes, (standardTypes & alarmType), readOnly)); resource = FileResourceConfigManager::addResource(settings); } // Add directory events of the appropriate type to this resource. for (const KAEvent& event : qAsConst(it.value())) resource.addEvent(event); mExistingAlarmTypes |= alarmType; } } } } } } mFetchesPending.removeAll(job); if (mFetchesPending.isEmpty()) { // The alarm types of all collections have been found, so now create // any necessary default file system resources. mMigratingAkonadi = false; createDefaultResources(); } } /****************************************************************************** * Called when a CalendarUpdater has been destroyed. * If there are none left, and we have finished, delete this object. */ void FileResourceMigrator::checkIfComplete() { if (mCompleted && !FileResourceCalendarUpdater::pending()) deleteLater(); } /****************************************************************************** * Migrate old KResource calendars, and create default file system resources. */ void FileResourceMigrator::migrateKResources() { if (mExistingAlarmTypes == CalEvent::EMPTY) { // There are no file system resources, so migrate any KResources alarm // calendars from pre-Akonadi versions of KAlarm. qCDebug(KALARM_LOG) << "FileResourceMigrator::migrateKResources"; const QString configFile = QStandardPaths::writableLocation(QStandardPaths::ConfigLocation) + QStringLiteral("/kresources/alarms/stdrc"); const KConfig config(configFile, KConfig::SimpleConfig); // Fetch all the KResource identifiers which are actually in use const KConfigGroup group = config.group("General"); const QStringList keys = group.readEntry("ResourceKeys", QStringList()) + group.readEntry("PassiveResourceKeys", QStringList()); // Create a file system resource for each KResource id for (const QString& id : keys) { // Read the resource configuration parameters from the config const KConfigGroup configGroup = config.group(QLatin1String("Resource_") + id); const QString resourceType = configGroup.readEntry("ResourceType", QString()); const char* pathKey = nullptr; FileResourceSettings::StorageType storageType; if (resourceType == QLatin1String("file")) { storageType = FileResourceSettings::File; pathKey = "CalendarURL"; } else if (resourceType == QLatin1String("dir")) { storageType = FileResourceSettings::Directory; pathKey = "CalendarURL"; } else if (resourceType == QLatin1String("remote")) { storageType = FileResourceSettings::File; pathKey = "DownloadUrl"; } else { qCWarning(KALARM_LOG) << "CalendarCreator: Invalid resource type:" << resourceType; continue; // unknown resource type - can't convert } const QUrl url = QUrl::fromUserInput(configGroup.readPathEntry(pathKey, QString())); CalEvent::Type alarmType = CalEvent::EMPTY; switch (configGroup.readEntry("AlarmType", (int)0)) { case 1: alarmType = CalEvent::ACTIVE; break; case 2: alarmType = CalEvent::ARCHIVED; break; case 4: alarmType = CalEvent::TEMPLATE; break; default: qCWarning(KALARM_LOG) << "FileResourceMigrator::migrateKResources: Invalid alarm type for resource"; continue; } const QString name = configGroup.readEntry("ResourceName", QString()); const bool enabled = configGroup.readEntry("ResourceIsActive", false); const bool standard = configGroup.readEntry("Standard", false); qCDebug(KALARM_LOG) << "FileResourceMigrator::migrateKResources: Migrating:" << name << ", type=" << alarmType << ", path=" << url.toString(); FileResourceSettings::Ptr settings(new FileResourceSettings( storageType, url, alarmType, name, configGroup.readEntry("Color", QColor()), (enabled ? alarmType : CalEvent::EMPTY), (standard ? alarmType : CalEvent::EMPTY), configGroup.readEntry("ResourceIsReadOnly", true))); Resource resource = FileResourceConfigManager::addResource(settings); // Update the calendar to the current KAlarm format if necessary, // and if the user agrees. FileResourceCalendarUpdater* updater = new FileResourceCalendarUpdater(resource, true, this); connect(updater, &QObject::destroyed, this, &FileResourceMigrator::checkIfComplete); updater->update(); // note that 'updater' will auto-delete when finished mExistingAlarmTypes |= alarmType; } } // Create any necessary additional default file system resources. createDefaultResources(); } /****************************************************************************** * Create default file system resources for any alarm types not covered by * existing resources. Normally, this occurs on the first run of KAlarm, but if * resources have been deleted, it could occur on later runs. * If the default calendar files already exist, they will be used; otherwise * they will be created. */ void FileResourceMigrator::createDefaultResources() { qCDebug(KALARM_LOG) << "FileResourceMigrator::createDefaultResources"; if (!(mExistingAlarmTypes & CalEvent::ACTIVE)) createCalendar(CalEvent::ACTIVE, QStringLiteral("calendar.ics"), i18nc("@info", "Active Alarms")); if (!(mExistingAlarmTypes & CalEvent::ARCHIVED)) createCalendar(CalEvent::ARCHIVED, QStringLiteral("expired.ics"), i18nc("@info", "Archived Alarms")); if (!(mExistingAlarmTypes & CalEvent::TEMPLATE)) createCalendar(CalEvent::TEMPLATE, QStringLiteral("template.ics"), i18nc("@info", "Alarm Templates")); mCompleted = true; checkIfComplete(); // delete this instance if everything is finished } /****************************************************************************** * Create a new default local file resource. * This is created as enabled, read-write, and standard for its alarm type. */ void FileResourceMigrator::createCalendar(CalEvent::Type alarmType, const QString& file, const QString& name) { const QUrl url = QUrl::fromLocalFile(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QLatin1Char('/') + file); qCDebug(KALARM_LOG) << "FileResourceMigrator: New:" << name << ", type=" << alarmType << ", path=" << url.toString(); FileResourceSettings::Ptr settings(new FileResourceSettings( FileResourceSettings::File, url, alarmType, name, QColor(), alarmType, CalEvent::EMPTY, false)); Resource resource = FileResourceConfigManager::addResource(settings); if (resource.failed()) { QString errmsg = xi18nc("@info/plain", "Failed to create default calendar %1", name); const QString locn = i18nc("@info File path or URL", "Location: %1", resource.displayLocation()); errmsg = xi18nc("@info", "%1%2", errmsg, locn); Resources::notifyResourceMessage(resource.id(), ResourceType::MessageType::Error, errmsg, QString()); return; } // Update the calendar to the current KAlarm format if necessary, // and if the user agrees. FileResourceCalendarUpdater* updater = new FileResourceCalendarUpdater(resource, true, this); connect(updater, &QObject::destroyed, this, &FileResourceMigrator::checkIfComplete); updater->update(); // note that 'updater' will auto-delete when finished } namespace { /****************************************************************************** * Load and parse events from each file in a calendar directory. */ bool readDirectoryResource(const QString& dirPath, CalEvent::Types alarmTypes, QHash>& events) { if (dirPath.isEmpty()) return false; qCDebug(KALARM_LOG) << "FileResourceMigrator::readDirectoryResource:" << dirPath; const QDir dir(dirPath); if (!dir.exists()) return false; // Read and parse each file in turn QList files; QDirIterator it(dir); while (it.hasNext()) { it.next(); const QString file = it.fileName(); if (!file.isEmpty() && !file.startsWith(QLatin1Char('.')) && !file.endsWith(QLatin1Char('~')) && file != QLatin1String("WARNING_README.txt")) { const QString path = dirPath + QLatin1Char('/') + file; if (QFileInfo::exists(path) // a temporary file may no longer exist && QFileInfo(path).isFile()) { KAlarm::importCalendarFile(QUrl::fromLocalFile(path), alarmTypes, false, Desktop::mainWindow(), events); } } } return true; } } //#include "fileresourcemigrator.moc" // vim: et sw=4: diff --git a/src/resources/fileresourcemigrator.h b/src/resources/fileresourcemigrator.h index 44ec05c8..f661eea8 100644 --- a/src/resources/fileresourcemigrator.h +++ b/src/resources/fileresourcemigrator.h @@ -1,79 +1,80 @@ /* * fileresourcemigrator.h - migrates or creates KAlarm non-Akonadi resources * Program: kalarm * Copyright © 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 FILERESOURCEMIGRATOR_H #define FILERESOURCEMIGRATOR_H #include #include class KJob; namespace Akonadi { class CollectionFetchJob; } class Resource; /** * Class to migrate Akonadi or KResources alarm calendars from previous * versions of KAlarm, and to create default calendar resources if none exist. */ class FileResourceMigrator : public QObject { Q_OBJECT public: ~FileResourceMigrator(); /** Return the unique instance, creating it if necessary. * Note that the instance will be destroyed once migration has completed. - * @return Unique instance, or null if migration has already been done. + * @return Unique instance, or null if migration is not required or has + * already been done. */ static FileResourceMigrator* instance(); /** Initiate resource migration and default resource creation. * When execution is complete, the unique instance will be destroyed. * Connect to the QObject::destroyed() signal to determine when * execution has completed. */ void execute(); static bool completed() { return mCompleted; } private Q_SLOTS: void collectionFetchResult(KJob*); void checkIfComplete(); private: FileResourceMigrator(QObject* parent = nullptr); void migrateAkonadiResources(Akonadi::ServerManager::State); void migrateKResources(); void createDefaultResources(); void createCalendar(KAlarmCal::CalEvent::Type alarmType, const QString& file, const QString& name); static FileResourceMigrator* mInstance; QList mFetchesPending; // pending collection fetch jobs for existing resources KAlarmCal::CalEvent::Types mExistingAlarmTypes {KAlarmCal::CalEvent::EMPTY}; // alarm types provided by existing non-Akonadi resources bool mMigratingAkonadi {false}; // attempting to migrate Akonadi resources bool mMigrateKResources {true}; // need to migrate KResource resources static bool mCompleted; // execute() has completed }; #endif // FILERESOURCEMIGRATOR_H // vim: et sw=4: diff --git a/src/resources/resource.cpp b/src/resources/resource.cpp index 267d605a..5f62ad24 100644 --- a/src/resources/resource.cpp +++ b/src/resources/resource.cpp @@ -1,321 +1,314 @@ /* * 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/resourcedatamodelbase.cpp b/src/resources/resourcedatamodelbase.cpp index 898fafd7..9a9be80f 100644 --- a/src/resources/resourcedatamodelbase.cpp +++ b/src/resources/resourcedatamodelbase.cpp @@ -1,800 +1,808 @@ /* * resourcedatamodelbase.cpp - base for models containing calendars and events * Program: kalarm * Copyright © 2007-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 "resourcedatamodelbase.h" #include "resources.h" #include "preferences.h" #include "lib/desktop.h" #include "lib/messagebox.h" #include "kalarm_debug.h" #include #include #include #include #include #include #include #include #include namespace { QString alarmTimeText(const DateTime& dateTime, char leadingZero = '\0'); QString timeToAlarmText(const DateTime& dateTime); } /*============================================================================= = Class: ResourceDataModelBase =============================================================================*/ ResourceDataModelBase* ResourceDataModelBase::mInstance = nullptr; QPixmap* ResourceDataModelBase::mTextIcon = nullptr; QPixmap* ResourceDataModelBase::mFileIcon = nullptr; QPixmap* ResourceDataModelBase::mCommandIcon = nullptr; QPixmap* ResourceDataModelBase::mEmailIcon = nullptr; QPixmap* ResourceDataModelBase::mAudioIcon = nullptr; QSize ResourceDataModelBase::mIconSize; /****************************************************************************** * Constructor. */ ResourceDataModelBase::ResourceDataModelBase() { if (!mTextIcon) { mTextIcon = new QPixmap(QIcon::fromTheme(QStringLiteral("dialog-information")).pixmap(16, 16)); mFileIcon = new QPixmap(QIcon::fromTheme(QStringLiteral("document-open")).pixmap(16, 16)); mCommandIcon = new QPixmap(QIcon::fromTheme(QStringLiteral("system-run")).pixmap(16, 16)); mEmailIcon = new QPixmap(QIcon::fromTheme(QStringLiteral("mail-unread")).pixmap(16, 16)); mAudioIcon = new QPixmap(QIcon::fromTheme(QStringLiteral("audio-x-generic")).pixmap(16, 16)); mIconSize = mTextIcon->size().expandedTo(mFileIcon->size()).expandedTo(mCommandIcon->size()).expandedTo(mEmailIcon->size()).expandedTo(mAudioIcon->size()); } } ResourceDataModelBase::~ResourceDataModelBase() { } /****************************************************************************** * Create a bulleted list of alarm types for insertion into .... */ QString ResourceDataModelBase::typeListForDisplay(CalEvent::Types alarmTypes) { QString list; if (alarmTypes & CalEvent::ACTIVE) list += QLatin1String("") + i18nc("@info", "Active Alarms") + QLatin1String(""); if (alarmTypes & CalEvent::ARCHIVED) list += QLatin1String("") + i18nc("@info", "Archived Alarms") + QLatin1String(""); if (alarmTypes & CalEvent::TEMPLATE) list += QLatin1String("") + i18nc("@info", "Alarm Templates") + QLatin1String(""); if (!list.isEmpty()) list = QLatin1String("") + list + QLatin1String(""); return list; } /****************************************************************************** * Return the read-only status tooltip for a collection, determined by the * read-write permissions and the KAlarm calendar format compatibility. * A null string is returned if the collection is read-write and compatible. */ QString ResourceDataModelBase::readOnlyTooltip(const Resource& resource) { switch (resource.compatibility()) { case KACalendar::Current: return resource.readOnly() ? i18nc("@info", "Read-only") : QString(); case KACalendar::Converted: case KACalendar::Convertible: return i18nc("@info", "Read-only (old format)"); case KACalendar::Incompatible: default: return i18nc("@info", "Read-only (other format)"); } } /****************************************************************************** * Return data for a column heading. */ QVariant ResourceDataModelBase::headerData(int section, Qt::Orientation orientation, int role, bool eventHeaders, bool& handled) { if (orientation == Qt::Horizontal) { handled = true; if (eventHeaders) { // Event column headers if (section < 0 || section >= ColumnCount) return QVariant(); if (role == Qt::DisplayRole || role == ColumnTitleRole) { switch (section) { case TimeColumn: return i18nc("@title:column", "Time"); case TimeToColumn: return i18nc("@title:column", "Time To"); case RepeatColumn: return i18nc("@title:column", "Repeat"); case ColourColumn: return (role == Qt::DisplayRole) ? QString() : i18nc("@title:column", "Color"); case TypeColumn: return (role == Qt::DisplayRole) ? QString() : i18nc("@title:column", "Type"); case TextColumn: return i18nc("@title:column", "Message, File or Command"); case TemplateNameColumn: return i18nc("@title:column Template name", "Name"); } } else if (role == Qt::WhatsThisRole) return whatsThisText(section); } else { // Calendar column headers if (section != 0) return QVariant(); if (role == Qt::DisplayRole) return i18nc("@title:column", "Calendars"); } } handled = false; return QVariant(); } /****************************************************************************** * Return whether resourceData() or eventData() handle a role. */ bool ResourceDataModelBase::roleHandled(int role) const { switch (role) { case Qt::WhatsThisRole: case Qt::ForegroundRole: case Qt::BackgroundRole: case Qt::DisplayRole: case Qt::TextAlignmentRole: case Qt::DecorationRole: case Qt::SizeHintRole: case Qt::AccessibleTextRole: case Qt::ToolTipRole: case ItemTypeRole: case ResourceIdRole: case BaseColourRole: case TimeDisplayRole: case SortRole: case StatusRole: case ValueRole: case EventIdRole: case ParentResourceIdRole: case EnabledRole: case AlarmActionsRole: case AlarmSubActionRole: case CommandErrorRole: return true; default: return false; } } /****************************************************************************** * Return the data for a given role, for a specified resource. */ QVariant ResourceDataModelBase::resourceData(int& role, const Resource& resource, bool& handled) const { if (roleHandled(role)) // Ensure that roleHandled() is coded correctly { handled = true; switch (role) { case Qt::DisplayRole: return resource.displayName(); case BaseColourRole: role = Qt::BackgroundRole; // use base model background colour break; case Qt::BackgroundRole: { const QColor colour = resource.backgroundColour(); if (colour.isValid()) return colour; break; // use base model background colour } case Qt::ForegroundRole: return resource.foregroundColour(); case Qt::ToolTipRole: return tooltip(resource, CalEvent::ACTIVE | CalEvent::ARCHIVED | CalEvent::TEMPLATE); case ItemTypeRole: return static_cast(Type::Resource); case ResourceIdRole: return resource.id(); default: break; } } handled = false; return QVariant(); } /****************************************************************************** * Return the data for a given role, for a specified event. */ QVariant ResourceDataModelBase::eventData(int role, int column, const KAEvent& event, const Resource& resource, bool& handled) const { if (roleHandled(role)) // Ensure that roleHandled() is coded correctly { handled = true; bool calendarColour = false; switch (role) { case Qt::WhatsThisRole: return whatsThisText(column); case ItemTypeRole: return static_cast(Type::Event); default: break; } if (!event.isValid()) return QVariant(); switch (role) { case EventIdRole: return event.id(); case StatusRole: return event.category(); case AlarmActionsRole: return event.actionTypes(); case AlarmSubActionRole: return event.actionSubType(); case CommandErrorRole: return event.commandError(); default: break; } switch (column) { case TimeColumn: switch (role) { case Qt::BackgroundRole: calendarColour = true; break; case Qt::DisplayRole: if (event.expired()) return alarmTimeText(event.startDateTime(), '0'); return alarmTimeText(event.nextTrigger(KAEvent::DISPLAY_TRIGGER), '0'); case TimeDisplayRole: if (event.expired()) return alarmTimeText(event.startDateTime(), '~'); return alarmTimeText(event.nextTrigger(KAEvent::DISPLAY_TRIGGER), '~'); case Qt::TextAlignmentRole: return Qt::AlignRight; case SortRole: { DateTime due; if (event.expired()) due = event.startDateTime(); else due = event.nextTrigger(KAEvent::DISPLAY_TRIGGER); return due.isValid() ? due.effectiveKDateTime().toUtc().qDateTime() : QDateTime(QDate(9999,12,31), QTime(0,0,0)); } default: break; } break; case TimeToColumn: switch (role) { case Qt::BackgroundRole: calendarColour = true; break; case Qt::DisplayRole: if (event.expired()) return QString(); return timeToAlarmText(event.nextTrigger(KAEvent::DISPLAY_TRIGGER)); case Qt::TextAlignmentRole: return Qt::AlignRight; case SortRole: { if (event.expired()) return -1; const DateTime due = event.nextTrigger(KAEvent::DISPLAY_TRIGGER); const KADateTime now = KADateTime::currentUtcDateTime(); if (due.isDateOnly()) return now.date().daysTo(due.date()) * 1440; return (now.secsTo(due.effectiveKDateTime()) + 59) / 60; } } break; case RepeatColumn: switch (role) { case Qt::BackgroundRole: calendarColour = true; break; case Qt::DisplayRole: return repeatText(event); case Qt::TextAlignmentRole: return Qt::AlignHCenter; case SortRole: return repeatOrder(event); } break; case ColourColumn: switch (role) { case Qt::BackgroundRole: { const KAEvent::Actions type = event.actionTypes(); if (type & KAEvent::ACT_DISPLAY) return event.bgColour(); if (type == KAEvent::ACT_COMMAND) { if (event.commandError() != KAEvent::CMD_NO_ERROR) return QColor(Qt::red); } break; } case Qt::ForegroundRole: if (event.commandError() != KAEvent::CMD_NO_ERROR) { if (event.actionTypes() == KAEvent::ACT_COMMAND) return QColor(Qt::white); QColor colour = Qt::red; int r, g, b; event.bgColour().getRgb(&r, &g, &b); if (r > 128 && g <= 128 && b <= 128) colour = QColor(Qt::white); return colour; } break; case Qt::DisplayRole: if (event.commandError() != KAEvent::CMD_NO_ERROR) return QLatin1String("!"); break; case SortRole: { const unsigned i = (event.actionTypes() == KAEvent::ACT_DISPLAY) ? event.bgColour().rgb() : 0; return QStringLiteral("%1").arg(i, 6, 10, QLatin1Char('0')); } default: break; } break; case TypeColumn: switch (role) { case Qt::BackgroundRole: calendarColour = true; break; case Qt::DecorationRole: { QVariant v; v.setValue(*eventIcon(event)); return v; } case Qt::TextAlignmentRole: return Qt::AlignHCenter; case Qt::SizeHintRole: return mIconSize; case Qt::AccessibleTextRole: //TODO: Implement accessibility return QString(); case ValueRole: return static_cast(event.actionSubType()); case SortRole: return QStringLiteral("%1").arg(event.actionSubType(), 2, 10, QLatin1Char('0')); } break; case TextColumn: switch (role) { case Qt::BackgroundRole: calendarColour = true; break; case Qt::DisplayRole: case SortRole: return AlarmText::summary(event, 1); case Qt::ToolTipRole: return AlarmText::summary(event, 10); default: break; } break; case TemplateNameColumn: switch (role) { case Qt::BackgroundRole: calendarColour = true; break; case Qt::DisplayRole: return event.templateName(); case SortRole: return event.templateName().toUpper(); } break; default: break; } if (calendarColour) { const QColor colour = resource.backgroundColour(); if (colour.isValid()) return colour; } switch (role) { case Qt::ForegroundRole: if (!event.enabled()) return Preferences::disabledColour(); if (event.expired()) return Preferences::archivedColour(); break; // use the default for normal active alarms case Qt::ToolTipRole: // Show the last command execution error message switch (event.commandError()) { case KAEvent::CMD_ERROR: return i18nc("@info:tooltip", "Command execution failed"); case KAEvent::CMD_ERROR_PRE: return i18nc("@info:tooltip", "Pre-alarm action execution failed"); case KAEvent::CMD_ERROR_POST: return i18nc("@info:tooltip", "Post-alarm action execution failed"); case KAEvent::CMD_ERROR_PRE_POST: return i18nc("@info:tooltip", "Pre- and post-alarm action execution failed"); default: case KAEvent::CMD_NO_ERROR: break; } break; case EnabledRole: return event.enabled(); default: break; } } handled = false; return QVariant(); } /****************************************************************************** * Return a resource's tooltip text. The resource's enabled status is * evaluated for specified alarm types. */ QString ResourceDataModelBase::tooltip(const Resource& resource, CalEvent::Types types) const { const QString name = QLatin1Char('@') + resource.displayName(); // insert markers for stripping out name const QString type = QLatin1Char('@') + resource.storageTypeString(false); // file/directory/URL etc. const QString locn = resource.displayLocation(); const bool inactive = !(resource.enabledTypes() & types); const QString readonly = readOnlyTooltip(resource); const bool writable = readonly.isEmpty(); const QString disabled = i18nc("@info", "Disabled"); if (inactive || !writable) return xi18nc("@info:tooltip", "%1" "%2: %3" "%4", name, type, locn, (inactive ? disabled : readonly)); return xi18nc("@info:tooltip", "%1" "%2: %3", name, type, locn); } /****************************************************************************** * Return the repetition text. */ QString ResourceDataModelBase::repeatText(const KAEvent& event) { const QString repText = event.recurrenceText(true); return repText.isEmpty() ? event.repetitionText(true) : repText; } /****************************************************************************** * Return a string for sorting the repetition column. */ QString ResourceDataModelBase::repeatOrder(const KAEvent& event) { int repOrder = 0; int repInterval = 0; if (event.repeatAtLogin()) repOrder = 1; else { repInterval = event.recurInterval(); switch (event.recurType()) { case KARecurrence::MINUTELY: repOrder = 2; break; case KARecurrence::DAILY: repOrder = 3; break; case KARecurrence::WEEKLY: repOrder = 4; break; case KARecurrence::MONTHLY_DAY: case KARecurrence::MONTHLY_POS: repOrder = 5; break; case KARecurrence::ANNUAL_DATE: case KARecurrence::ANNUAL_POS: repOrder = 6; break; case KARecurrence::NO_RECUR: default: break; } } return QStringLiteral("%1%2").arg(static_cast('0' + repOrder)).arg(repInterval, 8, 10, QLatin1Char('0')); } /****************************************************************************** * Returns the QWhatsThis text for a specified column. */ QString ResourceDataModelBase::whatsThisText(int column) { switch (column) { case TimeColumn: return i18nc("@info:whatsthis", "Next scheduled date and time of the alarm"); case TimeToColumn: return i18nc("@info:whatsthis", "How long until the next scheduled trigger of the alarm"); case RepeatColumn: return i18nc("@info:whatsthis", "How often the alarm recurs"); case ColourColumn: return i18nc("@info:whatsthis", "Background color of alarm message"); case TypeColumn: return i18nc("@info:whatsthis", "Alarm type (message, file, command or email)"); case TextColumn: return i18nc("@info:whatsthis", "Alarm message text, URL of text file to display, command to execute, or email subject line"); case TemplateNameColumn: return i18nc("@info:whatsthis", "Name of the alarm template"); default: return QString(); } } /****************************************************************************** * Return the icon associated with an event's action. */ QPixmap* ResourceDataModelBase::eventIcon(const KAEvent& event) { switch (event.actionTypes()) { case KAEvent::ACT_EMAIL: return mEmailIcon; case KAEvent::ACT_AUDIO: return mAudioIcon; case KAEvent::ACT_COMMAND: return mCommandIcon; case KAEvent::ACT_DISPLAY: if (event.actionSubType() == KAEvent::FILE) return mFileIcon; Q_FALLTHROUGH(); // fall through to ACT_DISPLAY_COMMAND case KAEvent::ACT_DISPLAY_COMMAND: default: return mTextIcon; } } /****************************************************************************** * Display a message to the user. */ void ResourceDataModelBase::handleResourceMessage(ResourceType::MessageType type, const QString& message, const QString& details) { if (type == ResourceType::MessageType::Error) { qCDebug(KALARM_LOG) << "Resource Error!" << message << details; KAMessageBox::detailedError(Desktop::mainWindow(), message, details); } else if (type == ResourceType::MessageType::Info) { qCDebug(KALARM_LOG) << "Resource user message:" << message << details; // KMessageBox::informationList looks bad, so use our own formatting. const QString msg = details.isEmpty() ? message : message + QStringLiteral("\n\n") + details; KAMessageBox::information(Desktop::mainWindow(), msg); } } bool ResourceDataModelBase::isMigrationComplete() const { return mMigrationStatus == 1; } bool ResourceDataModelBase::isMigrating() const { return mMigrationStatus == 0; } void ResourceDataModelBase::setMigrationInitiated(bool started) { mMigrationStatus = (started ? 0 : -1); } void ResourceDataModelBase::setMigrationComplete() { mMigrationStatus = 1; - Resources::notifyResourcesMigrated(); + if (mCreationStatus) + Resources::notifyResourcesCreated(); +} + +void ResourceDataModelBase::setCalendarsCreated() +{ + mCreationStatus = true; + if (mMigrationStatus == 1) + Resources::notifyResourcesCreated(); } namespace { /****************************************************************************** * Return the alarm time text in the form "date time". * Parameters: * dateTime = the date/time to format. * leadingZero = the character to represent a leading zero, or '\0' for no leading zeroes. */ QString alarmTimeText(const DateTime& dateTime, char leadingZero) { // Whether the date and time contain leading zeroes. static bool leadingZeroesChecked = false; static QString dateFormat; // date format for current locale static QString timeFormat; // time format for current locale static QString timeFullFormat; // time format with leading zero, if different from 'timeFormat' static int hourOffset = 0; // offset within time string to the hour if (!dateTime.isValid()) return i18nc("@info Alarm never occurs", "Never"); if (!leadingZeroesChecked && QApplication::isLeftToRight()) // don't try to align right-to-left languages { // Check whether the day number and/or hour have no leading zeroes, if // they are at the start of the date/time. If no leading zeroes, they // will need to be padded when displayed, so that displayed dates/times // can be aligned with each other. // Note that if leading zeroes are not included in other components, no // alignment will be attempted. QLocale locale; { // Check the date format. 'dd' provides leading zeroes; single 'd' // provides no leading zeroes. dateFormat = locale.dateFormat(QLocale::ShortFormat); } { // Check the time format. // Remove all but hours, minutes and AM/PM, since alarms are on minute // boundaries. Preceding separators are also removed. timeFormat = locale.timeFormat(QLocale::ShortFormat); for (int del = 0, predel = 0, c = 0; c < timeFormat.size(); ++c) { char ch = timeFormat.at(c).toLatin1(); switch (ch) { case 'H': case 'h': case 'm': case 'a': case 'A': if (predel == 1) { timeFormat.remove(del, c - del); c = del; } del = c + 1; // start deleting from the next character if ((ch == 'A' && del < timeFormat.size() && timeFormat.at(del).toLatin1() == 'P') || (ch == 'a' && del < timeFormat.size() && timeFormat.at(del).toLatin1() == 'p')) ++c, ++del; predel = -1; break; case 's': case 'z': case 't': timeFormat.remove(del, c + 1 - del); c = del - 1; if (!predel) predel = 1; break; default: break; } } // 'HH' and 'hh' provide leading zeroes; single 'H' or 'h' provide no // leading zeroes. int i = timeFormat.indexOf(QRegExp(QLatin1String("[hH]"))); int first = timeFormat.indexOf(QRegExp(QLatin1String("[hHmaA]"))); if (i >= 0 && i == first && (i == timeFormat.size() - 1 || timeFormat.at(i) != timeFormat.at(i + 1))) { timeFullFormat = timeFormat; timeFullFormat.insert(i, timeFormat.at(i)); // Find index to hour in formatted times const QTime t(1,30,30); const QString nozero = t.toString(timeFormat); const QString zero = t.toString(timeFullFormat); for (int i = 0; i < nozero.size(); ++i) if (nozero[i] != zero[i]) { hourOffset = i; break; } } } } leadingZeroesChecked = true; const KADateTime kdt = dateTime.effectiveKDateTime().toTimeSpec(Preferences::timeSpec()); QString dateTimeText = kdt.date().toString(dateFormat); if (!dateTime.isDateOnly() || kdt.utcOffset() != dateTime.utcOffset()) { // Display the time of day if it's a date/time value, or if it's // a date-only value but it's in a different time zone dateTimeText += QLatin1Char(' '); bool useFullFormat = leadingZero && !timeFullFormat.isEmpty(); QString timeText = kdt.time().toString(useFullFormat ? timeFullFormat : timeFormat); if (useFullFormat && leadingZero != '0' && timeText.at(hourOffset) == QLatin1Char('0')) timeText[hourOffset] = leadingZero; dateTimeText += timeText; } return dateTimeText + QLatin1Char(' '); } /****************************************************************************** * Return the time-to-alarm text. */ QString timeToAlarmText(const DateTime& dateTime) { if (!dateTime.isValid()) return i18nc("@info Alarm never occurs", "Never"); KADateTime now = KADateTime::currentUtcDateTime(); if (dateTime.isDateOnly()) { int days = now.date().daysTo(dateTime.date()); // xgettext: no-c-format return i18nc("@info n days", "%1d", days); } int mins = (now.secsTo(dateTime.effectiveKDateTime()) + 59) / 60; if (mins < 0) return QString(); char minutes[3] = "00"; minutes[0] = (mins%60) / 10 + '0'; minutes[1] = (mins%60) % 10 + '0'; if (mins < 24*60) return i18nc("@info hours:minutes", "%1:%2", mins/60, QLatin1String(minutes)); // If we render a day count, then we zero-pad the hours, to make the days line up and be more scanable. int hrs = mins / 60; char hours[3] = "00"; hours[0] = (hrs%24) / 10 + '0'; hours[1] = (hrs%24) % 10 + '0'; int days = hrs / 24; return i18nc("@info days hours:minutes", "%1d %2:%3", days, QLatin1String(hours), QLatin1String(minutes)); } } // vim: et sw=4: diff --git a/src/resources/resourcedatamodelbase.h b/src/resources/resourcedatamodelbase.h index d3e5fccc..8883d6e3 100644 --- a/src/resources/resourcedatamodelbase.h +++ b/src/resources/resourcedatamodelbase.h @@ -1,198 +1,202 @@ /* * resourcedatamodelbase.h - base for models containing calendars and events * Program: kalarm * Copyright © 2007-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 RESOURCEDATAMODELBASE_H #define RESOURCEDATAMODELBASE_H #include "resourcetype.h" #include "preferences.h" #include #include class Resource; class ResourceListModel; class ResourceFilterCheckListModel; class AlarmListModel; class TemplateListModel; class ResourceCreator; class QModelIndex; class QPixmap; namespace KAlarmCal { class KAEvent; } using namespace KAlarmCal; /*============================================================================= = Class: ResourceDataModelBase = Base class for models containing all calendars and events. =============================================================================*/ class ResourceDataModelBase { public: /** Data column numbers. */ enum { // Item columns TimeColumn = 0, TimeToColumn, RepeatColumn, ColourColumn, TypeColumn, TextColumn, TemplateNameColumn, ColumnCount }; /** Additional model data roles. */ enum { UserRole = Qt::UserRole + 500, // copied from Akonadi::EntityTreeModel ItemTypeRole = UserRole, // item's type: calendar or event // Calendar roles ResourceIdRole, // the resource ID BaseColourRole, // background colour ignoring collection colour // Event roles EventIdRole, // the event ID ParentResourceIdRole, // the parent resource ID of the event EnabledRole, // true for enabled alarm, false for disabled StatusRole, // KAEvent::ACTIVE/ARCHIVED/TEMPLATE AlarmActionsRole, // KAEvent::Actions AlarmSubActionRole, // KAEvent::Action ValueRole, // numeric value SortRole, // the value to use for sorting TimeDisplayRole, // time column value with '~' representing omitted leading zeroes ColumnTitleRole, // column titles (whether displayed or not) CommandErrorRole // last command execution error for alarm (per user) }; /** The type of a model row. */ enum class Type { Error = 0, Event, Resource }; virtual ~ResourceDataModelBase(); static QSize iconSize() { return mIconSize; } /** Return a bulleted list of alarm types for inclusion in an i18n message. */ static QString typeListForDisplay(CalEvent::Types); /** Get the tooltip for a resource. The resource's enabled status is * evaluated for specified alarm types. */ QString tooltip(const Resource&, CalEvent::Types) const; /** Return the read-only status tooltip for a resource. * A null string is returned if the resource is fully writable. */ static QString readOnlyTooltip(const Resource&); /** Return offset to add to headerData() role, for item models. */ virtual int headerDataEventRoleOffset() const { return 0; } protected: ResourceDataModelBase(); /** Terminate access to the data model, and tidy up. */ virtual void terminate() = 0; /** Reload all resources' data from storage. * @note In the case of Akonadi, this does not reload from the backend storage. */ virtual void reload() = 0; /** Reload a resource's data from storage. * @note In the case of Akonadi, this does not reload from the backend storage. */ virtual bool reload(Resource&) = 0; /** Check for, and remove, any duplicate resources, i.e. those which use * the same calendar file/directory. */ virtual void removeDuplicateResources() = 0; /** Disable the widget if the database engine is not available, and display * an error overlay. */ virtual void widgetNeedsDatabase(QWidget*) = 0; /** Create a ResourceCreator instance for the model. */ virtual ResourceCreator* createResourceCreator(KAlarmCal::CalEvent::Type defaultType, QWidget* parent) = 0; /** Update a resource's backend calendar file to the current KAlarm format. */ virtual void updateCalendarToCurrentFormat(Resource&, bool ignoreKeepFormat, QObject* parent) = 0; virtual ResourceListModel* createResourceListModel(QObject* parent) = 0; virtual ResourceFilterCheckListModel* createResourceFilterCheckListModel(QObject* parent) = 0; virtual AlarmListModel* createAlarmListModel(QObject* parent) = 0; virtual AlarmListModel* allAlarmListModel() = 0; virtual TemplateListModel* createTemplateListModel(QObject* parent) = 0; virtual TemplateListModel* allTemplateListModel() = 0; /** Return the data storage backend type used by this model. */ virtual Preferences::Backend dataStorageBackend() const = 0; static QVariant headerData(int section, Qt::Orientation, int role, bool eventHeaders, bool& handled); /** Return whether resourceData() and/or eventData() handle a role. */ bool roleHandled(int role) const; /** Return the model data for a resource. * @param role may be updated for calling the base model. * @param handled updated to true if the reply is valid, else set to false. */ QVariant resourceData(int& role, const Resource&, bool& handled) const; /** Return the model data for an event. * @param handled updated to true if the reply is valid, else set to false. */ QVariant eventData(int role, int column, const KAEvent& event, const Resource&, bool& handled) const; /** Called when a resource notifies a message to display to the user. */ void handleResourceMessage(ResourceType::MessageType, const QString& message, const QString& details); /** Return whether calendar migration/creation at initialisation has completed. */ bool isMigrationComplete() const; /** Return whether calendar migration is currently in progress. */ bool isMigrating() const; /** To be called when calendar migration has been initiated (or reset). */ void setMigrationInitiated(bool started = true); /** To be called when calendar migration has been initiated (or reset). */ void setMigrationComplete(); + /** To be called when all previously configured calendars have been created. */ + void setCalendarsCreated(); + static QString repeatText(const KAEvent&); static QString repeatOrder(const KAEvent&); static QString whatsThisText(int column); static QPixmap* eventIcon(const KAEvent&); static ResourceDataModelBase* mInstance; private: static QPixmap* mTextIcon; static QPixmap* mFileIcon; static QPixmap* mCommandIcon; static QPixmap* mEmailIcon; static QPixmap* mAudioIcon; static QSize mIconSize; // maximum size of any icon int mMigrationStatus {-1}; // migration status, -1 = no, 0 = initiated, 1 = complete + bool mCreationStatus {false}; // previously configured calendar creation status friend class DataModel; }; #endif // RESOURCEDATAMODELBASE_H // vim: et sw=4: diff --git a/src/resources/resources.cpp b/src/resources/resources.cpp index 44b0cf46..ac6aa531 100644 --- a/src/resources/resources.cpp +++ b/src/resources/resources.cpp @@ -1,632 +1,625 @@ /* * 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 "lib/messagebox.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() { qRegisterMetaType(); } 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() { + qCDebug() << "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) { if (resource(id).isValid()) Q_EMIT instance()->resourceMessage(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 d326c38e..6f879e6b 100644 --- a/src/resources/resources.h +++ b/src/resources/resources.h @@ -1,321 +1,318 @@ /* * resources.h - container for all ResourceType instances * 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 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(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&); /** Sorting criteria for allResources(Type, Sorting). May be OR'ed together. */ enum Sorts { NoSort = 0, DisplayName = 0x01, // sort by display name DefaultFirst = 0x02 // default resource is first in list. Requires a CalEvent::Type to be specified. }; Q_DECLARE_FLAGS(Sorting, Sorts) /** Return all resources of a kind which contain a specified alarm type. * @tparam RType Resource type to fetch, default = all types. * @param alarmType Alarm type to check for, or CalEvent::EMPTY for any type. * @param sorting Sorting criteria to use. */ template static QVector allResources(CalEvent::Type alarmType = CalEvent::EMPTY, Sorting sorting = NoSort); /** 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. */ + /** Return whether all configured and migrated resources have been created. */ static bool allCreated(); - /** Return whether all configured resources have been loaded at least once. */ + /** Return whether all configured and migrated 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. */ + /** Called to notify that all configured and migrated 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(). + * necessarily populated), and any necessary resource migration and + * the creation of default resources has been performed. */ void resourcesCreated(); - /** Emitted when all configured resources have been loaded for the first time. */ + /** Emitted when all configured and migrated 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(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; }; Q_DECLARE_OPERATORS_FOR_FLAGS(Resources::Sorting) /*============================================================================= * Template definitions. *============================================================================*/ template QVector Resources::allResources(CalEvent::Type type, Sorting sorting) { const CalEvent::Types types = (type == CalEvent::EMPTY) ? CalEvent::ACTIVE | CalEvent::ARCHIVED | CalEvent::TEMPLATE : type; QVector result; Resource std; if ((sorting & DefaultFirst) && type != CalEvent::EMPTY) { std = getStandard(type); if (std.isValid() && std.is()) result += std; } const int start = result.size(); for (auto it = mResources.constBegin(); it != mResources.constEnd(); ++it) { const Resource& res = it.value(); if (res != std && res.is() && (res.alarmTypes() & types)) result += res; } if (sorting & DisplayName) std::sort(result.begin() + start, result.end(), [](const Resource& a, const Resource& b) { return a.displayName().compare(b.displayName(), Qt::CaseInsensitive) < 0; }); return result; } #endif // RESOURCES_H // vim: et sw=4: diff --git a/src/resources/singlefileresourceconfigdialog.h b/src/resources/singlefileresourceconfigdialog.h index 87ba95ce..0293520d 100644 --- a/src/resources/singlefileresourceconfigdialog.h +++ b/src/resources/singlefileresourceconfigdialog.h @@ -1,91 +1,91 @@ /* * singlefileresourceconfigdialog.h - configuration dialog for single file resources. * Program: kalarm * Copyright © 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 SINGLEFILERESOURCECONFIGDIALOG_H #define SINGLEFILERESOURCECONFIGDIALOG_H #include #include #include class KJob; namespace KIO { class StatJob; } class Ui_SingleFileResourceConfigWidget; class SingleFileResourceConfigDialog : public QDialog { Q_OBJECT public: - explicit SingleFileResourceConfigDialog(bool create, QWidget* parent); + SingleFileResourceConfigDialog(bool create, QWidget* parent); ~SingleFileResourceConfigDialog(); /** Return the file URL. */ QUrl url() const; /** Set the file URL. */ void setUrl(const QUrl& url, bool readOnly = false); /** Return the resource's display name. */ QString displayName() const; /** Set the resource's display name. */ void setDisplayName(const QString& name); /** Return whether the resource is read-only. */ bool readOnly() const; /** Set the read-only status of the resource. */ void setReadOnly(bool readonly); /** Return the resource's alarm type. */ KAlarmCal::CalEvent::Type alarmType() const; /** Set the resource's alarm type. */ void setAlarmType(KAlarmCal::CalEvent::Type); /** Set a function to validate the entered URL. * The function should return an error text to display to the user, or * empty string if no error. */ void setUrlValidation(QString (*func)(const QUrl&)); protected: void showEvent(QShowEvent*) override; private Q_SLOTS: void validate(); void slotStatJobResult(KJob*); private: void initiateUrlStatusCheck(const QUrl&); void enableOkButton(); void disableOkButton(const QString& statusMessage, bool errorColour = false); Ui_SingleFileResourceConfigWidget* mUi {nullptr}; QString (*mUrlValidationFunc)(const QUrl&) {nullptr}; KIO::StatJob* mStatJob {nullptr}; const bool mCreating; // whether creating or editing the resource bool mCheckingDir {false}; }; #endif // SINGLEFILERESOURCECONFIGDIALOG_H // vim: et sw=4: