diff --git a/src/functions.cpp b/src/functions.cpp index 3f7c7422..1f28dca0 100644 --- a/src/functions.cpp +++ b/src/functions.cpp @@ -1,2013 +1,2013 @@ /* * functions.cpp - miscellaneous functions * Program: kalarm * Copyright © 2001-2018 by 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 "kalarm.h" //krazy:exclude=includes (kalarm.h must be first) #include "functions.h" #include "functions_p.h" #include "collectionmodel.h" #include "collectionsearch.h" #include "alarmcalendar.h" #include "alarmtime.h" #include "autoqpointer.h" #include "alarmlistview.h" #include "editdlg.h" #include "kalarmapp.h" #include "kamail.h" #include "mainwindow.h" #include "messagebox.h" #include "messagewin.h" #include "preferences.h" #include "shellprocess.h" #include "templatelistview.h" #include "templatemenuaction.h" #include "config-kalarm.h" #include #include #include #include #include #include using namespace KCalCore; #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if KDEPIM_HAVE_X11 #include #include #include #include #include #endif #include #include #include #include #include #include #include #include #include #include #include "kalarm_debug.h" using namespace Akonadi; namespace { bool refreshAlarmsQueued = false; QDBusInterface* korgInterface = nullptr; struct UpdateStatusData { KAlarm::UpdateResult status; // status code and KOrganizer error message if any int warnErr; int warnKOrg; explicit UpdateStatusData(KAlarm::UpdateStatus s = KAlarm::UPDATE_OK) : status(s), warnErr(0), warnKOrg(0) {} // Set an error status and increment to number of errors to warn about void setError(KAlarm::UpdateStatus st, int errorCount = -1) { status.set(st); if (errorCount < 0) ++warnErr; else warnErr = errorCount; } // Update the error status with a KOrganizer related status - void korgUpdate(const KAlarm::UpdateResult &result) + void korgUpdate(const KAlarm::UpdateResult& result) { if (result.status != KAlarm::UPDATE_OK) { ++warnKOrg; if (result.status > status.status) status = result; } } }; const QLatin1String KMAIL_DBUS_SERVICE("org.kde.kmail"); //const QLatin1String KMAIL_DBUS_IFACE("org.kde.kmail.kmail"); //const QLatin1String KMAIL_DBUS_WINDOW_PATH("/kmail/kmail_mainwindow_1"); const QLatin1String KORG_DBUS_SERVICE("org.kde.korganizer"); const QLatin1String KORG_DBUS_IFACE("org.kde.korganizer.Korganizer"); // D-Bus object path of KOrganizer's notification interface #define KORG_DBUS_PATH "/Korganizer" #define KORG_DBUS_LOAD_PATH "/korganizer_PimApplication" //const QLatin1String KORG_DBUS_WINDOW_PATH("/korganizer/MainWindow_1"); const QLatin1String KORG_MIME_TYPE("application/x-vnd.akonadi.calendar.event"); const QLatin1String KORGANIZER_UID("-korg"); const QLatin1String ALARM_OPTS_FILE("alarmopts"); const char* DONT_SHOW_ERRORS_GROUP = "DontShowErrors"; void editNewTemplate(EditAlarmDlg::Type, const KAEvent* preset, QWidget* parent); void displayUpdateError(QWidget* parent, KAlarm::UpdateError, const UpdateStatusData&, bool showKOrgError = true); KAlarm::UpdateResult sendToKOrganizer(const KAEvent&); KAlarm::UpdateResult deleteFromKOrganizer(const QString& eventID); KAlarm::UpdateResult runKOrganizer(); QString uidKOrganizer(const QString& eventID); } namespace KAlarm { Private* Private::mInstance = nullptr; /****************************************************************************** * Display a main window with the specified event selected. */ MainWindow* displayMainWindowSelected(Akonadi::Item::Id eventId) { MainWindow* win = MainWindow::firstWindow(); if (!win) { if (theApp()->checkCalendar()) // ensure calendar is open { win = MainWindow::create(); win->show(); } } else { // There is already a main window, so make it the active window win->hide(); // in case it's on a different desktop win->setWindowState(win->windowState() & ~Qt::WindowMinimized); win->show(); win->raise(); win->activateWindow(); } if (win && eventId >= 0) win->selectEvent(eventId); return win; } /****************************************************************************** * Create an "Alarms Enabled/Enable Alarms" action. */ KToggleAction* createAlarmEnableAction(QObject* parent) { KToggleAction* action = new KToggleAction(i18nc("@action", "Enable &Alarms"), parent); action->setChecked(theApp()->alarmsEnabled()); QObject::connect(action, &QAction::toggled, theApp(), &KAlarmApp::setAlarmsEnabled); // The following line ensures that all instances are kept in the same state QObject::connect(theApp(), &KAlarmApp::alarmEnabledToggled, action, &QAction::setChecked); return action; } /****************************************************************************** * Create a "Stop Play" action. */ QAction* createStopPlayAction(QObject* parent) { QAction* action = new QAction(QIcon::fromTheme(QStringLiteral("media-playback-stop")), i18nc("@action", "Stop Play"), parent); action->setEnabled(MessageWin::isAudioPlaying()); QObject::connect(action, &QAction::triggered, theApp(), &KAlarmApp::stopAudio); // The following line ensures that all instances are kept in the same state QObject::connect(theApp(), &KAlarmApp::audioPlaying, action, &QAction::setEnabled); return action; } /****************************************************************************** * Create a "Spread Windows" action. */ KToggleAction* createSpreadWindowsAction(QObject* parent) { KToggleAction* action = new KToggleAction(i18nc("@action", "Spread Windows"), parent); QObject::connect(action, &QAction::triggered, theApp(), &KAlarmApp::spreadWindows); // The following line ensures that all instances are kept in the same state QObject::connect(theApp(), &KAlarmApp::spreadWindowsToggled, action, &QAction::setChecked); return action; } /****************************************************************************** * Add a new active (non-archived) alarm. * Save it in the calendar file and add it to every main window instance. * Parameters: msgParent = parent widget for any calendar selection prompt or * error message. * event - is updated with the actual event ID. */ UpdateResult addEvent(KAEvent& event, Collection* calendar, QWidget* msgParent, int options, bool showKOrgErr) { qCDebug(KALARM_LOG) << event.id(); bool cancelled = false; UpdateStatusData status; if (!theApp()->checkCalendar()) // ensure calendar is open status.status = UPDATE_FAILED; else { // Save the event details in the calendar file, and get the new event ID AlarmCalendar* cal = AlarmCalendar::resources(); // Note that AlarmCalendar::addEvent() updates 'event'. if (!cal->addEvent(event, msgParent, (options & USE_EVENT_ID), calendar, (options & NO_RESOURCE_PROMPT), &cancelled)) { status.status = UPDATE_FAILED; } else { if (!cal->save()) status.status = SAVE_FAILED; } if (status.status == UPDATE_OK) { if ((options & ALLOW_KORG_UPDATE) && event.copyToKOrganizer()) { UpdateResult st = sendToKOrganizer(event); // tell KOrganizer to show the event status.korgUpdate(st); } } } if (status.status != UPDATE_OK && !cancelled && msgParent) displayUpdateError(msgParent, ERR_ADD, status, showKOrgErr); return status.status; } /****************************************************************************** * Add a list of new active (non-archived) alarms. * Save them in the calendar file and add them to every main window instance. * The events are updated with their actual event IDs. */ UpdateResult addEvents(QVector& events, QWidget* msgParent, bool allowKOrgUpdate, bool showKOrgErr) { qCDebug(KALARM_LOG) << events.count(); if (events.isEmpty()) return UpdateResult(UPDATE_OK); UpdateStatusData status; Collection collection; if (!theApp()->checkCalendar()) // ensure calendar is open status.status = UPDATE_FAILED; else { collection = CollectionControlModel::instance()->destination(CalEvent::ACTIVE, msgParent); if (!collection.isValid()) { qCDebug(KALARM_LOG) << "No calendar"; status.status = UPDATE_FAILED; } } if (status.status == UPDATE_OK) { AlarmCalendar* cal = AlarmCalendar::resources(); for (int i = 0, end = events.count(); i < end; ++i) { // Save the event details in the calendar file, and get the new event ID if (!cal->addEvent(events[i], msgParent, false, &collection)) { status.setError(UPDATE_ERROR); continue; } if (allowKOrgUpdate && events[i].copyToKOrganizer()) { UpdateResult st = sendToKOrganizer(events[i]); // tell KOrganizer to show the event status.korgUpdate(st); } } if (status.warnErr == events.count()) status.status = UPDATE_FAILED; else if (!cal->save()) status.setError(SAVE_FAILED, events.count()); // everything failed } if (status.status != UPDATE_OK && msgParent) displayUpdateError(msgParent, ERR_ADD, status, showKOrgErr); return status.status; } /****************************************************************************** * Save the event in the archived calendar and adjust every main window instance. * The event's ID is changed to an archived ID if necessary. */ bool addArchivedEvent(KAEvent& event, Collection* collection) { qCDebug(KALARM_LOG) << event.id(); bool archiving = (event.category() == CalEvent::ACTIVE); if (archiving && !Preferences::archivedKeepDays()) return false; // expired alarms aren't being kept AlarmCalendar* cal = AlarmCalendar::resources(); KAEvent newevent(event); newevent.setItemId(-1); // invalidate the Akonadi item ID since it's a new item KAEvent* const newev = &newevent; if (archiving) { newev->setCategory(CalEvent::ARCHIVED); // this changes the event ID newev->setCreatedDateTime(KADateTime::currentUtcDateTime()); // time stamp to control purging } // Note that archived resources are automatically saved after changes are made if (!cal->addEvent(newevent, nullptr, false, collection)) return false; event = *newev; // update event ID etc. return true; } /****************************************************************************** * Add a new template. * Save it in the calendar file and add it to every template list view. * 'event' is updated with the actual event ID. * Parameters: promptParent = parent widget for any calendar selection prompt. */ UpdateResult addTemplate(KAEvent& event, Collection* collection, QWidget* msgParent) { qCDebug(KALARM_LOG) << event.id(); UpdateStatusData status; // Add the template to the calendar file AlarmCalendar* cal = AlarmCalendar::resources(); KAEvent newev(event); if (!cal->addEvent(newev, msgParent, false, collection)) status.status = UPDATE_FAILED; else { event = newev; // update event ID etc. if (!cal->save()) status.status = SAVE_FAILED; else { return UpdateResult(UPDATE_OK); } } if (msgParent) displayUpdateError(msgParent, ERR_TEMPLATE, status); return status.status; } /****************************************************************************** * Modify an active (non-archived) alarm in the calendar file and in every main * window instance. * The new event must have a different event ID from the old one. */ UpdateResult modifyEvent(KAEvent& oldEvent, KAEvent& newEvent, QWidget* msgParent, bool showKOrgErr) { qCDebug(KALARM_LOG) << oldEvent.id(); UpdateStatusData status; if (!newEvent.isValid()) { deleteEvent(oldEvent, true); status.status = UPDATE_FAILED; } else { EventId oldId(oldEvent); if (oldEvent.copyToKOrganizer()) { // Tell KOrganizer to delete its old event. // But ignore errors, because the user could have manually // deleted it since KAlarm asked KOrganizer to set it up. deleteFromKOrganizer(oldId.eventId()); } // Update the event in the calendar file, and get the new event ID AlarmCalendar* cal = AlarmCalendar::resources(); if (!cal->modifyEvent(oldId, newEvent)) status.status = UPDATE_FAILED; else { if (!cal->save()) status.status = SAVE_FAILED; if (status.status == UPDATE_OK) { if (newEvent.copyToKOrganizer()) { UpdateResult st = sendToKOrganizer(newEvent); // tell KOrganizer to show the new event status.korgUpdate(st); } // Remove "Don't show error messages again" for the old alarm setDontShowErrors(oldId); } } } if (status.status != UPDATE_OK && msgParent) displayUpdateError(msgParent, ERR_MODIFY, status, showKOrgErr); return status.status; } /****************************************************************************** * Update an active (non-archived) alarm from the calendar file and from every * main window instance. * The new event will have the same event ID as the old one. * The event is not updated in KOrganizer, since this function is called when an * existing alarm is rescheduled (due to recurrence or deferral). */ UpdateResult updateEvent(KAEvent& event, QWidget* msgParent, bool archiveOnDelete) { qCDebug(KALARM_LOG) << event.id(); if (!event.isValid()) deleteEvent(event, archiveOnDelete); else { // Update the event in the calendar file. AlarmCalendar* cal = AlarmCalendar::resources(); cal->updateEvent(event); if (!cal->save()) { if (msgParent) displayUpdateError(msgParent, ERR_ADD, UpdateStatusData(SAVE_FAILED)); return UpdateResult(SAVE_FAILED); } } return UpdateResult(UPDATE_OK); } /****************************************************************************** * Update a template in the calendar file and in every template list view. * If 'selectionView' is non-null, the selection highlight is moved to the * updated event in that listView instance. */ UpdateResult updateTemplate(KAEvent& event, QWidget* msgParent) { AlarmCalendar* cal = AlarmCalendar::resources(); KAEvent* newEvent = cal->updateEvent(event); UpdateStatus status = UPDATE_OK; if (!newEvent) status = UPDATE_FAILED; else if (!cal->save()) status = SAVE_FAILED; if (status != UPDATE_OK) { if (msgParent) displayUpdateError(msgParent, ERR_TEMPLATE, UpdateStatusData(SAVE_FAILED)); return UpdateResult(status); } return UpdateResult(UPDATE_OK); } /****************************************************************************** * Delete alarms from the calendar file and from every main window instance. * If the events are archived, the events' IDs are changed to archived IDs if necessary. */ UpdateResult deleteEvent(KAEvent& event, bool archive, QWidget* msgParent, bool showKOrgErr) { QVector events(1, event); return deleteEvents(events, archive, msgParent, showKOrgErr); } UpdateResult deleteEvents(QVector& events, bool archive, QWidget* msgParent, bool showKOrgErr) { qCDebug(KALARM_LOG) << events.count(); if (events.isEmpty()) return UpdateResult(UPDATE_OK); UpdateStatusData status; AlarmCalendar* cal = AlarmCalendar::resources(); bool deleteWakeFromSuspendAlarm = false; QString wakeFromSuspendId = checkRtcWakeConfig().value(0); for (int i = 0, end = events.count(); i < end; ++i) { // Save the event details in the calendar file, and get the new event ID KAEvent* event = &events[i]; QString id = event->id(); // Delete the event from the calendar file if (event->category() != CalEvent::ARCHIVED) { if (event->copyToKOrganizer()) { // The event was shown in KOrganizer, so tell KOrganizer to // delete it. But ignore errors, because the user could have // manually deleted it from KOrganizer since it was set up. UpdateResult st = deleteFromKOrganizer(id); status.korgUpdate(st); } if (archive && event->toBeArchived()) { KAEvent ev(*event); addArchivedEvent(ev); // this changes the event ID to an archived ID } } if (!cal->deleteEvent(*event, false)) // don't save calendar after deleting status.setError(UPDATE_ERROR); if (id == wakeFromSuspendId) deleteWakeFromSuspendAlarm = true; // Remove "Don't show error messages again" for this alarm setDontShowErrors(EventId(*event)); } if (status.warnErr == events.count()) status.status = UPDATE_FAILED; else if (!cal->save()) // save the calendars now status.setError(SAVE_FAILED, events.count()); if (status.status != UPDATE_OK && msgParent) displayUpdateError(msgParent, ERR_DELETE, status, showKOrgErr); // Remove any wake-from-suspend scheduled for a deleted alarm if (deleteWakeFromSuspendAlarm && !wakeFromSuspendId.isEmpty()) cancelRtcWake(msgParent, wakeFromSuspendId); return status.status; } /****************************************************************************** * Delete templates from the calendar file and from every template list view. */ UpdateResult deleteTemplates(const KAEvent::List& events, QWidget* msgParent) { int count = events.count(); qCDebug(KALARM_LOG) << count; if (!count) return UpdateResult(UPDATE_OK); UpdateStatusData status; AlarmCalendar* cal = AlarmCalendar::resources(); for (int i = 0, end = count; i < end; ++i) { // Update the window lists // Delete the template from the calendar file AlarmCalendar* cal = AlarmCalendar::resources(); if (!cal->deleteEvent(*events[i], false)) // don't save calendar after deleting status.setError(UPDATE_ERROR); } if (status.warnErr == count) status.status = UPDATE_FAILED; else if (!cal->save()) // save the calendars now status.setError(SAVE_FAILED, count); if (status.status != UPDATE_OK && msgParent) displayUpdateError(msgParent, ERR_TEMPLATE, status); return status.status; } /****************************************************************************** * Delete an alarm from the display calendar. */ void deleteDisplayEvent(const QString& eventID) { qCDebug(KALARM_LOG) << eventID; AlarmCalendar* cal = AlarmCalendar::displayCalendarOpen(); if (cal) cal->deleteDisplayEvent(eventID, true); // save calendar after deleting } /****************************************************************************** * Undelete archived alarms, and update every main window instance. * The archive bit is set to ensure that they get re-archived if deleted again. * 'ineligibleIDs' is filled in with the IDs of any ineligible events. */ UpdateResult reactivateEvent(KAEvent& event, Collection* calendar, QWidget* msgParent, bool showKOrgErr) { QVector ids; QVector events(1, event); return reactivateEvents(events, ids, calendar, msgParent, showKOrgErr); } UpdateResult reactivateEvents(QVector& events, QVector& ineligibleIDs, Collection* col, QWidget* msgParent, bool showKOrgErr) { qCDebug(KALARM_LOG) << events.count(); ineligibleIDs.clear(); if (events.isEmpty()) return UpdateResult(UPDATE_OK); UpdateStatusData status; Collection collection; if (col) collection = *col; if (!collection.isValid()) collection = CollectionControlModel::instance()->destination(CalEvent::ACTIVE, msgParent); if (!collection.isValid()) { qCDebug(KALARM_LOG) << "No calendar"; status.setError(UPDATE_FAILED, events.count()); } else { int count = 0; AlarmCalendar* cal = AlarmCalendar::resources(); const KADateTime now = KADateTime::currentUtcDateTime(); for (int i = 0, end = events.count(); i < end; ++i) { // Delete the event from the archived resource KAEvent* event = &events[i]; if (event->category() != CalEvent::ARCHIVED || !event->occursAfter(now, true)) { ineligibleIDs += EventId(*event); continue; } ++count; KAEvent newevent(*event); KAEvent* const newev = &newevent; newev->setCategory(CalEvent::ACTIVE); // this changes the event ID if (newev->recurs() || newev->repetition()) newev->setNextOccurrence(now); // skip any recurrences in the past newev->setArchive(); // ensure that it gets re-archived if it is deleted // Save the event details in the calendar file. // This converts the event ID. if (!cal->addEvent(newevent, msgParent, true, &collection)) { status.setError(UPDATE_ERROR); continue; } if (newev->copyToKOrganizer()) { UpdateResult st = sendToKOrganizer(*newev); // tell KOrganizer to show the event status.korgUpdate(st); } if (cal->event(EventId(*event)) // no error if event doesn't exist in archived resource && !cal->deleteEvent(*event, false)) // don't save calendar after deleting status.setError(UPDATE_ERROR); events[i] = newevent; } if (status.warnErr == count) status.status = UPDATE_FAILED; // Save the calendars, even if all events failed, since more than one calendar was updated if (!cal->save() && status.status != UPDATE_FAILED) status.setError(SAVE_FAILED, count); } if (status.status != UPDATE_OK && msgParent) displayUpdateError(msgParent, ERR_REACTIVATE, status, showKOrgErr); return status.status; } /****************************************************************************** * Enable or disable alarms in the calendar file and in every main window instance. * The new events will have the same event IDs as the old ones. */ UpdateResult enableEvents(QVector& events, bool enable, QWidget* msgParent) { qCDebug(KALARM_LOG) << events.count(); if (events.isEmpty()) return UpdateResult(UPDATE_OK); UpdateStatusData status; AlarmCalendar* cal = AlarmCalendar::resources(); bool deleteWakeFromSuspendAlarm = false; QString wakeFromSuspendId = checkRtcWakeConfig().value(0); for (int i = 0, end = events.count(); i < end; ++i) { KAEvent* event = &events[i]; if (event->category() == CalEvent::ACTIVE && enable != event->enabled()) { event->setEnabled(enable); if (!enable && event->id() == wakeFromSuspendId) deleteWakeFromSuspendAlarm = true; // Update the event in the calendar file KAEvent* newev = cal->updateEvent(event); if (!newev) qCCritical(KALARM_LOG) << "Error updating event in calendar:" << event->id(); else { cal->disabledChanged(newev); // If we're disabling a display alarm, close any message window if (!enable && (event->actionTypes() & KAEvent::ACT_DISPLAY)) { MessageWin* win = MessageWin::findEvent(EventId(*event)); delete win; } } } } if (!cal->save()) status.setError(SAVE_FAILED, events.count()); if (status.status != UPDATE_OK && msgParent) displayUpdateError(msgParent, ERR_ADD, status); // Remove any wake-from-suspend scheduled for a disabled alarm if (deleteWakeFromSuspendAlarm && !wakeFromSuspendId.isEmpty()) cancelRtcWake(msgParent, wakeFromSuspendId); return status.status; } /****************************************************************************** * This method must only be called from the main KAlarm queue processing loop, * to prevent asynchronous calendar operations interfering with one another. * * Purge all archived events from the default archived alarm resource whose end * time is longer ago than 'purgeDays'. All events are deleted if 'purgeDays' is * zero. */ void purgeArchive(int purgeDays) { if (purgeDays < 0) return; qCDebug(KALARM_LOG) << purgeDays; const QDate cutoff = KADateTime::currentLocalDate().addDays(-purgeDays); Collection collection = CollectionControlModel::getStandard(CalEvent::ARCHIVED); if (!collection.isValid()) return; KAEvent::List events = AlarmCalendar::resources()->events(collection); for (int i = 0; i < events.count(); ) { if (purgeDays && events.at(i)->createdDateTime().date() >= cutoff) events.remove(i); else ++i; } if (!events.isEmpty()) AlarmCalendar::resources()->purgeEvents(events); // delete the events and save the calendar } /****************************************************************************** * Display an error message about an error when saving an event. * If 'model' is non-null, the AlarmListModel* which it points to is used; if * that is null, it is created. */ QVector getSortedActiveEvents(QObject* parent, AlarmListModel** model) { AlarmListModel* mdl = nullptr; if (!model) model = &mdl; if (!*model) { *model = new AlarmListModel(parent); (*model)->setEventTypeFilter(CalEvent::ACTIVE); (*model)->sort(AlarmListModel::TimeColumn); } QVector result; for (int i = 0, count = (*model)->rowCount(); i < count; ++i) { KAEvent event = (*model)->event(i); if (event.enabled() && !event.expired()) result += event; } return result; } /****************************************************************************** * Display an error message corresponding to a specified alarm update error code. */ -void displayKOrgUpdateError(QWidget* parent, UpdateError code, const UpdateResult &korgError, int nAlarms) +void displayKOrgUpdateError(QWidget* parent, UpdateError code, const UpdateResult& korgError, int nAlarms) { QString errmsg; switch (code) { case ERR_ADD: case ERR_REACTIVATE: errmsg = (nAlarms > 1) ? i18nc("@info", "Unable to show alarms in KOrganizer") : i18nc("@info", "Unable to show alarm in KOrganizer"); break; case ERR_MODIFY: errmsg = i18nc("@info", "Unable to update alarm in KOrganizer"); break; case ERR_DELETE: errmsg = (nAlarms > 1) ? i18nc("@info", "Unable to delete alarms from KOrganizer") : i18nc("@info", "Unable to delete alarm from KOrganizer"); break; case ERR_TEMPLATE: return; } bool showDetail = !korgError.message.isEmpty(); QString msg; switch (korgError.status) { case UPDATE_KORG_ERRINIT: msg = xi18nc("@info", "%1(Could not start KOrganizer)", errmsg); break; case UPDATE_KORG_ERRSTART: msg = xi18nc("@info", "%1(KOrganizer not fully started)", errmsg); break; case UPDATE_KORG_ERR: msg = xi18nc("@info", "%1(Error communicating with KOrganizer)", errmsg); break; default: msg = errmsg; showDetail = false; break; } if (showDetail) KAMessageBox::detailedError(parent, msg, korgError.message); else KAMessageBox::error(parent, msg); } /****************************************************************************** * Execute a New Alarm dialog for the specified alarm type. */ void editNewAlarm(EditAlarmDlg::Type type, QWidget* parent) { execNewAlarmDlg(EditAlarmDlg::create(false, type, parent)); } /****************************************************************************** * Execute a New Alarm dialog for the specified alarm type. */ void editNewAlarm(KAEvent::SubAction action, QWidget* parent, const AlarmText* text) { bool setAction = false; EditAlarmDlg::Type type; switch (action) { case KAEvent::MESSAGE: case KAEvent::FILE: type = EditAlarmDlg::DISPLAY; setAction = true; break; case KAEvent::COMMAND: type = EditAlarmDlg::COMMAND; break; case KAEvent::EMAIL: type = EditAlarmDlg::EMAIL; break; case KAEvent::AUDIO: type = EditAlarmDlg::AUDIO; break; default: return; } EditAlarmDlg* editDlg = EditAlarmDlg::create(false, type, parent); if (setAction || text) editDlg->setAction(action, *text); execNewAlarmDlg(editDlg); } /****************************************************************************** * Execute a New Alarm dialog, optionally either presetting it to the supplied * event, or setting the action and text. */ void editNewAlarm(const KAEvent* preset, QWidget* parent) { execNewAlarmDlg(EditAlarmDlg::create(false, preset, true, parent)); } /****************************************************************************** * Common code for editNewAlarm() variants. */ void execNewAlarmDlg(EditAlarmDlg* editDlg) { // Create a PrivateNewAlarmDlg parented by editDlg. // It will be deleted when editDlg is closed. new PrivateNewAlarmDlg(editDlg); editDlg->show(); editDlg->raise(); editDlg->activateWindow(); } PrivateNewAlarmDlg::PrivateNewAlarmDlg(EditAlarmDlg* dlg) : QObject(dlg) { connect(dlg, &QDialog::accepted, this, &PrivateNewAlarmDlg::okClicked); connect(dlg, &QDialog::rejected, this, &PrivateNewAlarmDlg::cancelClicked); } /****************************************************************************** * Called when the dialogue is accepted (e.g. by clicking the OK button). * Creates the event specified in the instance's dialogue. */ void PrivateNewAlarmDlg::okClicked() { accept(static_cast(parent())); } /****************************************************************************** * Creates the event specified in a given dialogue. */ void PrivateNewAlarmDlg::accept(EditAlarmDlg* editDlg) { KAEvent event; Collection calendar; editDlg->getEvent(event, calendar); // Add the alarm to the displayed lists and to the calendar file UpdateResult status = addEvent(event, &calendar, editDlg); switch (status.status) { case UPDATE_FAILED: return; case UPDATE_KORG_ERR: case UPDATE_KORG_ERRINIT: case UPDATE_KORG_ERRSTART: case UPDATE_KORG_FUNCERR: displayKOrgUpdateError(editDlg, ERR_ADD, status); break; default: break; } Undo::saveAdd(event, calendar); outputAlarmWarnings(editDlg, &event); editDlg->deleteLater(); } /****************************************************************************** * Called when the dialogue is rejected (e.g. by clicking the Cancel button). */ void PrivateNewAlarmDlg::cancelClicked() { static_cast(parent())->deleteLater(); } /****************************************************************************** * Display the alarm edit dialog to edit a new alarm, preset with a template. */ bool editNewAlarm(const QString& templateName, QWidget* parent) { if (!templateName.isEmpty()) { KAEvent* templateEvent = AlarmCalendar::resources()->templateEvent(templateName); if (templateEvent->isValid()) { editNewAlarm(templateEvent, parent); return true; } qCWarning(KALARM_LOG) << templateName << ": template not found"; } return false; } /****************************************************************************** * Create a new template. */ void editNewTemplate(EditAlarmDlg::Type type, QWidget* parent) { ::editNewTemplate(type, nullptr, parent); } /****************************************************************************** * Create a new template, based on an existing event or template. */ void editNewTemplate(const KAEvent* preset, QWidget* parent) { ::editNewTemplate(EditAlarmDlg::Type(0), preset, parent); } /****************************************************************************** * Find the identity of the desktop we are running on. */ QString currentDesktopIdentityName() { return QProcessEnvironment::systemEnvironment().value(QStringLiteral("XDG_CURRENT_DESKTOP")); } /****************************************************************************** * Find the identity of the desktop we are running on. */ Desktop currentDesktopIdentity() { const QString desktop = currentDesktopIdentityName(); if (desktop == QStringLiteral("KDE")) return Desktop::Kde; if (desktop == QStringLiteral("Unity")) return Desktop::Unity; return Desktop::Other; } /****************************************************************************** * Check the config as to whether there is a wake-on-suspend alarm pending, and * if so, delete it from the config if it has expired. * If 'checkExists' is true, the config entry will only be returned if the * event exists. * Reply = config entry: [0] = event's collection ID (Akonadi only), * [1] = event ID, * [2] = trigger time (time_t). * = empty list if none or expired. */ QStringList checkRtcWakeConfig(bool checkEventExists) { KConfigGroup config(KSharedConfig::openConfig(), "General"); const QStringList params = config.readEntry("RtcWake", QStringList()); if (params.count() == 3 && params[2].toUInt() > KADateTime::currentUtcDateTime().toTime_t()) { if (checkEventExists && !AlarmCalendar::getEvent(EventId(params[0].toLongLong(), params[1]))) return QStringList(); return params; // config entry is valid } if (!params.isEmpty()) { config.deleteEntry("RtcWake"); // delete the expired config entry config.sync(); } return QStringList(); } /****************************************************************************** * Delete any wake-on-suspend alarm from the config. */ void deleteRtcWakeConfig() { KConfigGroup config(KSharedConfig::openConfig(), "General"); config.deleteEntry("RtcWake"); config.sync(); } /****************************************************************************** * Delete any wake-on-suspend alarm, optionally only for a specified event. */ void cancelRtcWake(QWidget* msgParent, const QString& eventId) { const QStringList wakeup = checkRtcWakeConfig(); if (!wakeup.isEmpty() && (eventId.isEmpty() || wakeup[0] == eventId)) { Private::instance()->mMsgParent = msgParent ? msgParent : MainWindow::mainMainWindow(); QTimer::singleShot(0, Private::instance(), &Private::cancelRtcWake); } } /****************************************************************************** * Delete any wake-on-suspend alarm. */ void Private::cancelRtcWake() { // setRtcWakeTime will only work with a parent window specified setRtcWakeTime(0, mMsgParent); deleteRtcWakeConfig(); KAMessageBox::information(mMsgParent, i18nc("info", "The scheduled Wake from Suspend has been cancelled.")); } /****************************************************************************** * Set the wakeup time for the system. * Set 'triggerTime' to zero to cancel the wakeup. * Reply = true if successful. */ bool setRtcWakeTime(unsigned triggerTime, QWidget* parent) { QVariantMap args; args[QStringLiteral("time")] = triggerTime; KAuth::Action action(QStringLiteral("org.kde.kalarm.rtcwake.settimer")); action.setHelperId(QStringLiteral("org.kde.kalarm.rtcwake")); action.setParentWidget(parent); action.setArguments(args); KAuth::ExecuteJob* job = action.execute(); if (!job->exec()) { QString errmsg = job->errorString(); qCDebug(KALARM_LOG) << "Error code=" << job->error() << errmsg; if (errmsg.isEmpty()) { int errcode = job->error(); switch (errcode) { case KAuth::ActionReply::AuthorizationDeniedError: case KAuth::ActionReply::UserCancelledError: qCDebug(KALARM_LOG) << "Authorization error:" << errcode; return false; // the user should already know about this default: break; } errmsg = i18nc("@info", "Error obtaining authorization (%1)", errcode); } KAMessageBox::information(parent, errmsg); return false; } return true; } } // namespace KAlarm namespace { /****************************************************************************** * Create a new template. * 'preset' is non-null to base it on an existing event or template; otherwise, * the alarm type is set to 'type'. */ void editNewTemplate(EditAlarmDlg::Type type, const KAEvent* preset, QWidget* parent) { if (CollectionControlModel::enabledCollections(CalEvent::TEMPLATE, true).isEmpty()) { KAMessageBox::sorry(parent, i18nc("@info", "You must enable a template calendar to save the template in")); return; } // 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 editDlg; if (preset) editDlg = EditAlarmDlg::create(true, preset, true, parent); else editDlg = EditAlarmDlg::create(true, type, parent); if (editDlg->exec() == QDialog::Accepted) { KAEvent event; Akonadi::Collection calendar; editDlg->getEvent(event, calendar); // Add the template to the displayed lists and to the calendar file KAlarm::addTemplate(event, &calendar, editDlg); Undo::saveAdd(event, calendar); } } } // namespace namespace KAlarm { /****************************************************************************** * Open the Edit Alarm dialog to edit the specified alarm. * If the alarm is read-only or archived, the dialog is opened read-only. */ void editAlarm(KAEvent* event, QWidget* parent) { if (event->expired() || AlarmCalendar::resources()->eventReadOnly(event->itemId())) { viewAlarm(event, parent); return; } EventId id(*event); // 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 editDlg = EditAlarmDlg::create(false, event, false, parent, EditAlarmDlg::RES_USE_EVENT_ID); if (editDlg->exec() == QDialog::Accepted) { if (!AlarmCalendar::resources()->event(id)) { // Event has been deleted while the user was editing the alarm, // so treat it as a new alarm. PrivateNewAlarmDlg().accept(editDlg); return; } KAEvent newEvent; Collection calendar; bool changeDeferral = !editDlg->getEvent(newEvent, calendar); // Update the event in the displays and in the calendar file Undo::Event undo(*event, calendar); if (changeDeferral) { // The only change has been to an existing deferral if (updateEvent(newEvent, editDlg, true) != UPDATE_OK) // keep the same event ID return; // failed to save event } else { UpdateResult status = modifyEvent(*event, newEvent, editDlg); if (status.status != UPDATE_OK && status.status <= UPDATE_KORG_ERR) displayKOrgUpdateError(editDlg, ERR_MODIFY, status); } Undo::saveEdit(undo, newEvent); outputAlarmWarnings(editDlg, &newEvent); } } /****************************************************************************** * Display the alarm edit dialog to edit the alarm with the specified ID. * An error occurs if the alarm is not found, if there is more than one alarm * with the same ID, or if it is read-only or expired. */ bool editAlarmById(const EventId& id, QWidget* parent) { const QString eventID(id.eventId()); KAEvent* event = AlarmCalendar::resources()->event(id, true); if (!event) { if (id.collectionId() != -1) qCWarning(KALARM_LOG) << "Event ID not found, or duplicated:" << eventID; else qCWarning(KALARM_LOG) << "Event ID not found:" << eventID; return false; } if (AlarmCalendar::resources()->eventReadOnly(event->itemId())) { qCCritical(KALARM_LOG) << eventID << ": read-only"; return false; } switch (event->category()) { case CalEvent::ACTIVE: case CalEvent::TEMPLATE: break; default: qCCritical(KALARM_LOG) << eventID << ": event not active or template"; return false; } editAlarm(event, parent); return true; } /****************************************************************************** * Open the Edit Alarm dialog to edit the specified template. * If the template is read-only, the dialog is opened read-only. */ void editTemplate(KAEvent* event, QWidget* parent) { if (AlarmCalendar::resources()->eventReadOnly(event->itemId())) { // The template is read-only, so make the dialogue read-only. // 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 editDlg = EditAlarmDlg::create(true, event, false, parent, EditAlarmDlg::RES_PROMPT, true); editDlg->exec(); return; } // 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 editDlg = EditAlarmDlg::create(true, event, false, parent, EditAlarmDlg::RES_USE_EVENT_ID); if (editDlg->exec() == QDialog::Accepted) { KAEvent newEvent; Akonadi::Collection calendar; editDlg->getEvent(newEvent, calendar); QString id = event->id(); newEvent.setEventId(id); newEvent.setCollectionId(event->collectionId()); newEvent.setItemId(event->itemId()); // Update the event in the displays and in the calendar file Undo::Event undo(*event, calendar); updateTemplate(newEvent, editDlg); Undo::saveEdit(undo, newEvent); } } /****************************************************************************** * Open the Edit Alarm dialog to view the specified alarm (read-only). */ void viewAlarm(const KAEvent* event, QWidget* parent) { // 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 editDlg = EditAlarmDlg::create(false, event, false, parent, EditAlarmDlg::RES_PROMPT, true); editDlg->exec(); } /****************************************************************************** * Called when OK is clicked in the alarm edit dialog invoked by the Edit button * in an alarm message window. * Updates the alarm calendar and closes the dialog. */ void updateEditedAlarm(EditAlarmDlg* editDlg, KAEvent& event, Collection& calendar) { qCDebug(KALARM_LOG); KAEvent newEvent; Akonadi::Collection cal; editDlg->getEvent(newEvent, cal); // Update the displayed lists and the calendar file UpdateResult status; if (AlarmCalendar::resources()->event(EventId(event))) { // The old alarm hasn't expired yet, so replace it Undo::Event undo(event, calendar); status = modifyEvent(event, newEvent, editDlg); Undo::saveEdit(undo, newEvent); } else { // The old event has expired, so simply create a new one status = addEvent(newEvent, &calendar, editDlg); Undo::saveAdd(newEvent, calendar); } if (status.status != UPDATE_OK && status.status <= UPDATE_KORG_ERR) displayKOrgUpdateError(editDlg, ERR_MODIFY, status); outputAlarmWarnings(editDlg, &newEvent); editDlg->close(); } /****************************************************************************** * Returns a list of all alarm templates. * If shell commands are disabled, command alarm templates are omitted. */ KAEvent::List templateList() { KAEvent::List templates; bool includeCmdAlarms = ShellProcess::authorised(); KAEvent::List events = AlarmCalendar::resources()->events(CalEvent::TEMPLATE); for (int i = 0, end = events.count(); i < end; ++i) { KAEvent* event = events[i]; if (includeCmdAlarms || !(event->actionTypes() & KAEvent::ACT_COMMAND)) templates.append(event); } return templates; } /****************************************************************************** * To be called after an alarm has been edited. * Prompt the user to re-enable alarms if they are currently disabled, and if * it's an email alarm, warn if no 'From' email address is configured. */ void outputAlarmWarnings(QWidget* parent, const KAEvent* event) { if (event && event->actionTypes() == KAEvent::ACT_EMAIL && Preferences::emailAddress().isEmpty()) KAMessageBox::information(parent, xi18nc("@info Please set the 'From' email address...", "%1Please set it in the Configuration dialog.", KAMail::i18n_NeedFromEmailAddress())); if (!theApp()->alarmsEnabled()) { if (KAMessageBox::warningYesNo(parent, xi18nc("@info", "Alarms are currently disabled.Do you want to enable alarms now?"), QString(), KGuiItem(i18nc("@action:button", "Enable")), KGuiItem(i18nc("@action:button", "Keep Disabled")), QStringLiteral("EditEnableAlarms")) == KMessageBox::Yes) theApp()->setAlarmsEnabled(true); } } /****************************************************************************** * Reload the calendar. */ void refreshAlarms() { qCDebug(KALARM_LOG); if (!refreshAlarmsQueued) { refreshAlarmsQueued = true; theApp()->processQueue(); } } /****************************************************************************** * This method must only be called from the main KAlarm queue processing loop, * to prevent asynchronous calendar operations interfering with one another. * * If refreshAlarms() has been called, reload the calendars. */ void refreshAlarmsIfQueued() { if (refreshAlarmsQueued) { qCDebug(KALARM_LOG); AlarmCalendar::resources()->reload(); // Close any message windows for alarms which are now disabled KAEvent::List events = AlarmCalendar::resources()->events(CalEvent::ACTIVE); for (int i = 0, end = events.count(); i < end; ++i) { KAEvent* event = events[i]; if (!event->enabled() && (event->actionTypes() & KAEvent::ACT_DISPLAY)) { MessageWin* win = MessageWin::findEvent(EventId(*event)); delete win; } } MainWindow::refresh(); refreshAlarmsQueued = false; } } /****************************************************************************** * Start KMail if it isn't already running, optionally minimised. * Reply = reason for failure to run KMail (which may be the empty string) * = null string if success. */ QString runKMail(bool minimise) { QDBusReply reply = QDBusConnection::sessionBus().interface()->isServiceRegistered(KMAIL_DBUS_SERVICE); if (!reply.isValid() || !reply.value()) { // Program is not already running, so start it QString errmsg; if (minimise && Private::startKMailMinimised()) return QString(); if (KToolInvocation::startServiceByDesktopName(QStringLiteral("org.kde.kmail2"), QString(), &errmsg)) { qCCritical(KALARM_LOG) << "Couldn't start KMail (" << errmsg << ")"; return xi18nc("@info", "Unable to start KMail(%1)", errmsg); } } return QString(); } /****************************************************************************** * Start KMail, minimised. * This code is taken from kstart in kdebase. */ bool Private::startKMailMinimised() { #pragma message("port QT5") #if 0 //PORT QT5 #if KDEPIM_HAVE_X11 NETRootInfo i(QX11Info::display(), NET::Supported); if (i.isSupported(NET::WM2KDETemporaryRules)) { qCDebug(KALARM_LOG) << "using rules"; KXMessages msg; QString message = QLatin1String("wmclass=kmail\nwmclassmatch=1\n" // 1 = exact match "wmclasscomplete=false\n" "minimize=true\nminimizerule=3\n" "type=") + QString().setNum(NET::Normal) + QLatin1String("\ntyperule=2"); msg.broadcastMessage("_KDE_NET_WM_TEMPORARY_RULES", message, -1); qApp->flush(); } else { // Connect to window add to get the NEW windows qCDebug(KALARM_LOG) << "connecting to window add"; connect(KWindowSystem::self(), SIGNAL(windowAdded(WId)), instance(), SLOT(windowAdded(WId))); } // Propagate the app startup notification info to the started app. // We are not using KApplication, so the env remained set. KStartupInfoId id = KStartupInfo::currentStartupIdEnv(); KProcess* proc = new KProcess; (*proc) << QStringLiteral("kmail"); int pid = proc->startDetached(); if (!pid) { KStartupInfo::sendFinish(id); // failed to start return false; } KStartupInfoData data; data.addPid(pid); data.setName(QLatin1String("kmail")); data.setBin(QLatin1String("kmail")); KStartupInfo::sendChange(id, data); return true; #else return false; #endif #else return false; #endif } /****************************************************************************** * Called when a window is created, to minimise it. * This code is taken from kstart in kdebase. */ void Private::windowAdded(WId w) { #pragma message("port QT5") #if 0 //Port QT5 #if KDEPIM_HAVE_X11 static const int SUPPORTED_TYPES = NET::NormalMask | NET::DesktopMask | NET::DockMask | NET::ToolbarMask | NET::MenuMask | NET::DialogMask | NET::OverrideMask | NET::TopMenuMask | NET::UtilityMask | NET::SplashMask; KWindowInfo kwinfo = KWindowSystem::windowInfo(w, NET::WMWindowType | NET::WMName); if (kwinfo.windowType(SUPPORTED_TYPES) == NET::TopMenu || kwinfo.windowType(SUPPORTED_TYPES) == NET::Toolbar || kwinfo.windowType(SUPPORTED_TYPES) == NET::Desktop) return; // always ignore these window types QX11Info qxinfo; XWithdrawWindow(QX11Info::display(), w, qxinfo.screen()); QApplication::flush(); NETWinInfo info(QX11Info::display(), w, QX11Info::appRootWindow(), NET::WMState); XWMHints* hints = XGetWMHints(QX11Info::display(), w); if (hints) { hints->flags |= StateHint; hints->initial_state = IconicState; XSetWMHints(QX11Info::display(), w, hints); XFree(hints); } info.setWindowType(NET::Normal); XSync(QX11Info::display(), False); XMapWindow(QX11Info::display(), w); XSync(QX11Info::display(), False); QApplication::flush(); #endif #endif } /****************************************************************************** * The "Don't show again" option for error messages is personal to the user on a * particular computer. For example, he may want to inhibit error messages only * on his laptop. So the status is not stored in the alarm calendar, but in the * user's local KAlarm data directory. ******************************************************************************/ /****************************************************************************** * Return the Don't-show-again error message tags set for a specified alarm ID. */ QStringList dontShowErrors(const EventId& eventId) { if (eventId.isEmpty()) return QStringList(); KConfig config(QStandardPaths::writableLocation(QStandardPaths::DataLocation) + QLatin1Char('/') + ALARM_OPTS_FILE); KConfigGroup group(&config, DONT_SHOW_ERRORS_GROUP); const QString id = QStringLiteral("%1:%2").arg(eventId.collectionId()).arg(eventId.eventId()); return group.readEntry(id, QStringList()); } /****************************************************************************** * Check whether the specified Don't-show-again error message tag is set for an * alarm ID. */ bool dontShowErrors(const EventId& eventId, const QString& tag) { if (tag.isEmpty()) return false; QStringList tags = dontShowErrors(eventId); return tags.indexOf(tag) >= 0; } /****************************************************************************** * Reset the Don't-show-again error message tags for an alarm ID. * If 'tags' is empty, the config entry is deleted. */ void setDontShowErrors(const EventId& eventId, const QStringList& tags) { if (eventId.isEmpty()) return; KConfig config(QStandardPaths::writableLocation(QStandardPaths::DataLocation) + QLatin1Char('/') + ALARM_OPTS_FILE); KConfigGroup group(&config, DONT_SHOW_ERRORS_GROUP); const QString id = QStringLiteral("%1:%2").arg(eventId.collectionId()).arg(eventId.eventId()); if (tags.isEmpty()) group.deleteEntry(id); else group.writeEntry(id, tags); group.sync(); } /****************************************************************************** * Set the specified Don't-show-again error message tag for an alarm ID. * Existing tags are unaffected. */ void setDontShowErrors(const EventId& eventId, const QString& tag) { if (eventId.isEmpty() || tag.isEmpty()) return; KConfig config(QStandardPaths::writableLocation(QStandardPaths::DataLocation) + QLatin1Char('/') + ALARM_OPTS_FILE); KConfigGroup group(&config, DONT_SHOW_ERRORS_GROUP); const QString id = QStringLiteral("%1:%2").arg(eventId.collectionId()).arg(eventId.eventId()); QStringList tags = group.readEntry(id, QStringList()); if (tags.indexOf(tag) < 0) { tags += tag; group.writeEntry(id, tags); group.sync(); } } /****************************************************************************** * Read the size for the specified window from the config file, for the * current screen resolution. * Reply = true if size set in the config file, in which case 'result' is set * = false if no size is set, in which case 'result' is unchanged. */ bool readConfigWindowSize(const char* window, QSize& result, int* splitterWidth) { KConfigGroup config(KSharedConfig::openConfig(), window); QWidget* desktop = qApp->desktop(); QSize s = QSize(config.readEntry(QStringLiteral("Width %1").arg(desktop->width()), (int)0), config.readEntry(QStringLiteral("Height %1").arg(desktop->height()), (int)0)); if (s.isEmpty()) return false; result = s; if (splitterWidth) *splitterWidth = config.readEntry(QStringLiteral("Splitter %1").arg(desktop->width()), -1); return true; } /****************************************************************************** * Write the size for the specified window to the config file, for the * current screen resolution. */ void writeConfigWindowSize(const char* window, const QSize& size, int splitterWidth) { KConfigGroup config(KSharedConfig::openConfig(), window); QWidget* desktop = qApp->desktop(); config.writeEntry(QStringLiteral("Width %1").arg(desktop->width()), size.width()); config.writeEntry(QStringLiteral("Height %1").arg(desktop->height()), size.height()); if (splitterWidth >= 0) config.writeEntry(QStringLiteral("Splitter %1").arg(desktop->width()), splitterWidth); config.sync(); } /****************************************************************************** * Check from its mime type whether a file appears to be a text or image file. * If a text file, its type is distinguished. * Reply = file type. */ FileType fileType(const QMimeType& mimetype) { if (mimetype.inherits(QStringLiteral("text/html"))) return TextFormatted; if (mimetype.inherits(QStringLiteral("application/x-executable"))) return TextApplication; if (mimetype.inherits(QStringLiteral("text/plain"))) return TextPlain; if (mimetype.name().startsWith(QLatin1String("image/"))) return Image; return Unknown; } /****************************************************************************** * Check that a file exists and is a plain readable file. * Updates 'filename' and 'url' even if an error occurs, since 'filename' may * be needed subsequently by showFileErrMessage(). * 'filename' is in user input format and may be a local file path or URL. */ FileErr checkFileExists(QString& filename, QUrl& url) { // Convert any relative file path to absolute // (using home directory as the default). // This also supports absolute paths and absolute urls. FileErr err = FileErr_None; url = QUrl::fromUserInput(filename, QDir::homePath(), QUrl::AssumeLocalFile); if (filename.isEmpty()) { url = QUrl(); err = FileErr_Blank; // blank file name } else if (!url.isValid()) err = FileErr_Nonexistent; else if (url.isLocalFile()) { // It's a local file filename = url.toLocalFile(); QFileInfo info(filename); if (info.isDir()) err = FileErr_Directory; else if (!info.exists()) err = FileErr_Nonexistent; else if (!info.isReadable()) err = FileErr_Unreadable; } else { filename = url.toDisplayString(); auto statJob = KIO::stat(url, KIO::StatJob::SourceSide, 2); KJobWidgets::setWindow(statJob, MainWindow::mainMainWindow()); if (!statJob->exec()) err = FileErr_Nonexistent; else { KFileItem fi(statJob->statResult(), url); if (fi.isDir()) err = FileErr_Directory; else if (!fi.isReadable()) err = FileErr_Unreadable; } } return err; } /****************************************************************************** * Display an error message appropriate to 'err'. * Display a Continue/Cancel error message if 'errmsgParent' non-null. * Reply = true to continue, false to cancel. */ bool showFileErrMessage(const QString& filename, FileErr err, FileErr blankError, QWidget* errmsgParent) { if (err != FileErr_None) { // If file is a local file, remove "file://" from name QString file = filename; QRegExp f(QStringLiteral("^file:/+")); if (f.indexIn(file) >= 0) file = file.mid(f.matchedLength() - 1); QString errmsg; switch (err) { case FileErr_Blank: if (blankError == FileErr_BlankDisplay) errmsg = i18nc("@info", "Please select a file to display"); else if (blankError == FileErr_BlankPlay) errmsg = i18nc("@info", "Please select a file to play"); else qFatal("Program error"); KAMessageBox::sorry(errmsgParent, errmsg); return false; case FileErr_Directory: KAMessageBox::sorry(errmsgParent, xi18nc("@info", "%1 is a folder", file)); return false; case FileErr_Nonexistent: errmsg = xi18nc("@info", "%1 not found", file); break; case FileErr_Unreadable: errmsg = xi18nc("@info", "%1 is not readable", file); break; case FileErr_NotTextImage: errmsg = xi18nc("@info", "%1 appears not to be a text or image file", file); break; default: break; } if (KAMessageBox::warningContinueCancel(errmsgParent, errmsg) == KMessageBox::Cancel) return false; } return true; } /****************************************************************************** * If a url string is a local file, strip off the 'file:/' prefix. */ QString pathOrUrl(const QString& url) { static const QRegExp localfile(QStringLiteral("^file:/+")); return (localfile.indexIn(url) >= 0) ? url.mid(localfile.matchedLength() - 1) : url; } /****************************************************************************** * Display a modal dialog to choose an existing file, initially highlighting * any specified file. * @param initialFile The file to initially highlight - must be a full path name or URL. * @param defaultDir The directory to start in if @p initialFile is empty. If empty, * the user's home directory will be used. Updated to the * directory containing the selected file, if a file is chosen. * @param existing true to return only existing files, false to allow new ones. * Reply = URL selected. * = empty, non-null string if no file was selected. * = null string if dialogue was deleted while visible (indicating that * the parent widget was probably also deleted). */ QString browseFile(const QString& caption, QString& defaultDir, const QString& initialFile, const QString& filter, bool existing, QWidget* parent) { QString initialDir = !initialFile.isEmpty() ? QString(initialFile).remove(QRegExp(QLatin1String("/[^/]*$"))) : !defaultDir.isEmpty() ? defaultDir : QDir::homePath(); // 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 fileDlg = new QFileDialog(parent, caption, initialDir, filter); fileDlg->setAcceptMode(existing ? QFileDialog::AcceptOpen : QFileDialog::AcceptSave); fileDlg->setFileMode(existing ? QFileDialog::ExistingFile : QFileDialog::AnyFile); if (!initialFile.isEmpty()) fileDlg->selectFile(initialFile); if (fileDlg->exec() != QDialog::Accepted) return fileDlg ? QStringLiteral("") : QString(); // return null only if dialog was deleted const QList urls = fileDlg->selectedUrls(); if (urls.isEmpty()) return QStringLiteral(""); // return empty, non-null string const QUrl& url = urls[0]; defaultDir = url.isLocalFile() ? KIO::upUrl(url).toLocalFile() : url.adjusted(QUrl::RemoveFilename).path(); bool localOnly = true; return localOnly ? url.toDisplayString(QUrl::PreferLocalFile) : url.toDisplayString(); } /****************************************************************************** * Return a prompt string to ask the user whether to convert the calendar to the * current format. * If 'whole' is true, the whole calendar needs to be converted; else only some * alarms may need to be converted. * * Note: This method is defined here to avoid duplicating the i18n string * definition between the Akonadi and KResources code. */ QString conversionPrompt(const QString& calendarName, const QString& calendarVersion, bool whole) { QString msg = whole ? xi18nc("@info", "Calendar %1 is in an old format (KAlarm version %2), " "and will be read-only unless you choose to update it to the current format.", calendarName, calendarVersion) : xi18nc("@info", "Some or all of the alarms in calendar %1 are in an old KAlarm format, " "and will be read-only unless you choose to update them to the current format.", calendarName); return xi18nc("@info", "%1" "Do not update the calendar if it is also used with an older version of KAlarm " "(e.g. on another computer). If you do so, the calendar may become unusable there." "Do you wish to update the calendar?", msg); } #ifndef NDEBUG /****************************************************************************** * Set up KAlarm test conditions based on environment variables. * KALARM_TIME: specifies current system time (format [[[yyyy-]mm-]dd-]hh:mm [TZ]). */ void setTestModeConditions() { const QByteArray newTime = qgetenv("KALARM_TIME"); if (!newTime.isEmpty()) { KADateTime dt; if (AlarmTime::convertTimeString(newTime, dt, KADateTime::realCurrentLocalDateTime(), true)) setSimulatedSystemTime(dt); } } /****************************************************************************** * Set the simulated system time. */ void setSimulatedSystemTime(const KADateTime& dt) { KADateTime::setSimulatedSystemTime(dt); qCDebug(KALARM_LOG) << "New time =" << qPrintable(KADateTime::currentLocalDateTime().toString(QStringLiteral("%Y-%m-%d %H:%M %:Z"))); } #endif } // namespace KAlarm namespace { /****************************************************************************** * Display an error message about an error when saving an event. */ void displayUpdateError(QWidget* parent, KAlarm::UpdateError code, const UpdateStatusData& status, bool showKOrgError) { QString errmsg; if (status.status.status > KAlarm::UPDATE_KORG_ERR) { switch (code) { case KAlarm::ERR_ADD: case KAlarm::ERR_MODIFY: errmsg = (status.warnErr > 1) ? i18nc("@info", "Error saving alarms") : i18nc("@info", "Error saving alarm"); break; case KAlarm::ERR_DELETE: errmsg = (status.warnErr > 1) ? i18nc("@info", "Error deleting alarms") : i18nc("@info", "Error deleting alarm"); break; case KAlarm::ERR_REACTIVATE: errmsg = (status.warnErr > 1) ? i18nc("@info", "Error saving reactivated alarms") : i18nc("@info", "Error saving reactivated alarm"); break; case KAlarm::ERR_TEMPLATE: errmsg = (status.warnErr > 1) ? i18nc("@info", "Error saving alarm templates") : i18nc("@info", "Error saving alarm template"); break; } KAMessageBox::error(parent, errmsg); } else if (showKOrgError) displayKOrgUpdateError(parent, code, status.status, status.warnKOrg); } /****************************************************************************** * Tell KOrganizer to put an alarm in its calendar. * It will be held by KOrganizer as a simple event, without alarms - KAlarm * is still responsible for alarming. */ KAlarm::UpdateResult sendToKOrganizer(const KAEvent& event) { Event::Ptr kcalEvent(new KCalCore::Event); event.updateKCalEvent(kcalEvent, KAEvent::UID_IGNORE); // Change the event ID to avoid duplicating the same unique ID as the original event QString uid = uidKOrganizer(event.id()); kcalEvent->setUid(uid); kcalEvent->clearAlarms(); QString userEmail; switch (event.actionTypes()) { case KAEvent::ACT_DISPLAY: case KAEvent::ACT_COMMAND: case KAEvent::ACT_DISPLAY_COMMAND: kcalEvent->setSummary(event.cleanText()); userEmail = Preferences::emailAddress(); break; case KAEvent::ACT_EMAIL: { QString from = event.emailFromId() ? Identities::identityManager()->identityForUoid(event.emailFromId()).fullEmailAddr() : Preferences::emailAddress(); AlarmText atext; atext.setEmail(event.emailAddresses(QStringLiteral(", ")), from, QString(), QString(), event.emailSubject(), QString()); kcalEvent->setSummary(atext.displayText()); userEmail = from; break; } case KAEvent::ACT_AUDIO: kcalEvent->setSummary(event.audioFile()); break; default: break; } Person::Ptr person(new Person(QString(), userEmail)); kcalEvent->setOrganizer(person); kcalEvent->setDuration(Duration(Preferences::kOrgEventDuration() * 60, Duration::Seconds)); // Translate the event into string format ICalFormat format; format.setTimeZone(Preferences::timeSpecAsZone()); QString iCal = format.toICalString(kcalEvent); // Send the event to KOrganizer KAlarm::UpdateResult status = runKOrganizer(); // start KOrganizer if it isn't already running, and create its D-Bus interface if (status != KAlarm::UPDATE_OK) return status; QList args; args << iCal; QDBusReply reply = korgInterface->callWithArgumentList(QDBus::Block, QStringLiteral("addIncidence"), args); if (!reply.isValid()) { if (reply.error().type() == QDBusError::UnknownObject) { status = KAlarm::UPDATE_KORG_ERRSTART; qCCritical(KALARM_LOG) << "addIncidence() D-Bus error: still starting"; } else { status.set(KAlarm::UPDATE_KORG_ERR, reply.error().message()); qCCritical(KALARM_LOG) << "addIncidence(" << uid << ") D-Bus call failed:" << status.message; } } else if (!reply.value()) { status = KAlarm::UPDATE_KORG_FUNCERR; qCDebug(KALARM_LOG) << "addIncidence(" << uid << ") D-Bus call returned false"; } else qCDebug(KALARM_LOG) << uid << ": success"; return status; } /****************************************************************************** * Tell KOrganizer to delete an event from its calendar. */ KAlarm::UpdateResult deleteFromKOrganizer(const QString& eventID) { const QString newID = uidKOrganizer(eventID); new CollectionSearch(KORG_MIME_TYPE, newID, true); // this auto-deletes when complete // Ignore errors return KAlarm::UpdateResult(KAlarm::UPDATE_OK); } /****************************************************************************** * Start KOrganizer if not already running, and create its D-Bus interface. */ KAlarm::UpdateResult runKOrganizer() { KAlarm::UpdateResult status; QString error, dbusService; int result = KDBusServiceStarter::self()->findServiceFor(QStringLiteral("DBUS/Organizer"), QString(), &error, &dbusService); if (result) { status.set(KAlarm::UPDATE_KORG_ERRINIT, error); qCWarning(KALARM_LOG) << "Unable to start DBUS/Organizer:" << status.message; return status; } // If Kontact is running, there is a load() method which needs to be called to // load KOrganizer into Kontact. But if KOrganizer is running independently, // the load() method doesn't exist. QDBusInterface iface(KORG_DBUS_SERVICE, QStringLiteral(KORG_DBUS_LOAD_PATH), QStringLiteral("org.kde.PIMUniqueApplication")); if (!iface.isValid()) { status.set(KAlarm::UPDATE_KORG_ERR, iface.lastError().message()); qCWarning(KALARM_LOG) << "Unable to access " KORG_DBUS_LOAD_PATH " D-Bus interface:" << status.message; return status; } QDBusReply reply = iface.call(QStringLiteral("load")); if ((!reply.isValid() || !reply.value()) && iface.lastError().type() != QDBusError::UnknownMethod) { status.set(KAlarm::UPDATE_KORG_ERR, iface.lastError().message()); qCWarning(KALARM_LOG) << "Loading KOrganizer failed:" << status.message; return status; } // KOrganizer has been started, but it may not have the necessary // D-Bus interface available yet. if (!korgInterface || !korgInterface->isValid()) { delete korgInterface; korgInterface = new QDBusInterface(KORG_DBUS_SERVICE, QStringLiteral(KORG_DBUS_PATH), KORG_DBUS_IFACE); if (!korgInterface->isValid()) { status.set(KAlarm::UPDATE_KORG_ERRSTART, korgInterface->lastError().message()); qCWarning(KALARM_LOG) << "Unable to access " KORG_DBUS_PATH " D-Bus interface:" << status.message; delete korgInterface; korgInterface = nullptr; } } return status; } /****************************************************************************** * Insert a KOrganizer string after the hyphen in the supplied event ID. */ QString uidKOrganizer(const QString& id) { QString result = id; int i = result.lastIndexOf(QLatin1Char('-')); if (i < 0) i = result.length(); return result.insert(i, KORGANIZER_UID); } } // namespace /****************************************************************************** * Case insensitive comparison for use by qSort(). */ bool caseInsensitiveLessThan(const QString& s1, const QString& s2) { return s1.toLower() < s2.toLower(); } // vim: et sw=4: diff --git a/src/functions.h b/src/functions.h index df41a1a1..1d014cd1 100644 --- a/src/functions.h +++ b/src/functions.h @@ -1,205 +1,205 @@ /* * functions.h - miscellaneous functions * Program: kalarm * Copyright © 2007-2017 by 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 FUNCTIONS_H #define FUNCTIONS_H /** @file functions.h - miscellaneous functions */ #include "editdlg.h" #include "eventid.h" #include #include #include #include #include #include #include #include #include using namespace KAlarmCal; namespace KCal { class Event; } class QWidget; class QAction; class QAction; class KToggleAction; class MainWindow; class AlarmListModel; namespace KAlarm { /** Return codes from fileType() */ enum FileType { Unknown, TextPlain, TextFormatted, TextApplication, Image }; /** Return codes from calendar update functions. * The codes are ordered by severity, so... * DO NOT CHANGE THE ORDER OF THESE VALUES! */ enum UpdateStatus { UPDATE_OK, // update succeeded UPDATE_KORG_FUNCERR, // update succeeded, but KOrganizer reported an error updating UPDATE_KORG_ERRSTART, // update succeeded, but KOrganizer update failed (KOrganizer not fully started) UPDATE_KORG_ERRINIT, // update succeeded, but KOrganizer update failed (KOrganizer not started) UPDATE_KORG_ERR, // update succeeded, but KOrganizer update failed UPDATE_ERROR, // update failed partially UPDATE_FAILED, // update failed completely SAVE_FAILED // calendar was updated in memory, but save failed }; /** Error codes supplied as parameter to displayUpdateError() */ enum UpdateError { ERR_ADD, ERR_MODIFY, ERR_DELETE, ERR_REACTIVATE, ERR_TEMPLATE }; /** Result of calendar update. */ struct UpdateResult { UpdateStatus status; // status code QString message; // error message if any UpdateResult() : status(UPDATE_OK) {} explicit UpdateResult(UpdateStatus s, const QString& m = QString()) : status(s), message(m) {} UpdateResult& operator=(UpdateStatus s) { status = s; message.clear(); return *this; } bool operator==(UpdateStatus s) const { return status == s; } bool operator!=(UpdateStatus s) const { return status != s; } void set(UpdateStatus s) { operator=(s); } void set(UpdateStatus s, const QString& m) { status = s; message = m; } }; /** Desktop identity, obtained from XDG_CURRENT_DESKTOP. */ enum class Desktop { Kde, //!< KDE (KDE 4 and Plasma both identify as "KDE") Unity, //!< Unity Other }; /** Display a main window with the specified event selected */ MainWindow* displayMainWindowSelected(Akonadi::Item::Id = -1); bool readConfigWindowSize(const char* window, QSize&, int* splitterWidth = nullptr); void writeConfigWindowSize(const char* window, const QSize&, int splitterWidth = -1); /** Check from its mime type whether a file appears to be a text or image file. * If a text file, its type is distinguished. */ FileType fileType(const QMimeType& mimetype); /** Check that a file exists and is a plain readable file, optionally a text/image file. * Display a Continue/Cancel error message if 'errmsgParent' non-null. */ enum FileErr { FileErr_None = 0, FileErr_Blank, // generic blank error FileErr_Nonexistent, FileErr_Directory, FileErr_Unreadable, FileErr_NotTextImage, FileErr_BlankDisplay, // blank error to use for file to display FileErr_BlankPlay // blank error to use for file to play }; FileErr checkFileExists(QString& filename, QUrl&); bool showFileErrMessage(const QString& filename, FileErr, FileErr blankError, QWidget* errmsgParent); /** If a url string is a local file, strip off the 'file:/' prefix. */ QString pathOrUrl(const QString& url); QString browseFile(const QString& caption, QString& defaultDir, const QString& initialFile = QString(), const QString& filter = QString(), bool existing = false, QWidget* parent = nullptr); bool editNewAlarm(const QString& templateName, QWidget* parent = nullptr); void editNewAlarm(EditAlarmDlg::Type, QWidget* parent = nullptr); void editNewAlarm(KAEvent::SubAction, QWidget* parent = nullptr, const AlarmText* = nullptr); void editNewAlarm(const KAEvent* preset, QWidget* parent = nullptr); void editAlarm(KAEvent*, QWidget* parent = nullptr); bool editAlarmById(const EventId& eventID, QWidget* parent = nullptr); void updateEditedAlarm(EditAlarmDlg*, KAEvent&, Akonadi::Collection&); void viewAlarm(const KAEvent*, QWidget* parent = nullptr); void editNewTemplate(EditAlarmDlg::Type, QWidget* parent = nullptr); void editNewTemplate(const KAEvent* preset, QWidget* parent = nullptr); void editTemplate(KAEvent*, QWidget* parent = nullptr); void execNewAlarmDlg(EditAlarmDlg*); /** Create a "New From Template" QAction */ KToggleAction* createAlarmEnableAction(QObject* parent); QAction* createStopPlayAction(QObject* parent); KToggleAction* createSpreadWindowsAction(QObject* parent); /** Returns a list of all alarm templates. * If shell commands are disabled, command alarm templates are omitted. */ KAEvent::List templateList(); void outputAlarmWarnings(QWidget* parent, const KAEvent* = nullptr); void refreshAlarms(); void refreshAlarmsIfQueued(); // must only be called from KAlarmApp::processQueue() QString runKMail(bool minimise); QStringList dontShowErrors(const EventId&); bool dontShowErrors(const EventId&, const QString& tag); void setDontShowErrors(const EventId&, const QStringList& tags = QStringList()); void setDontShowErrors(const EventId&, const QString& tag); void setDontShowErrors(const QString& eventId, const QString& tag); enum // 'options' parameter values for addEvent(). May be OR'ed together. { USE_EVENT_ID = 0x01, // use event ID if it's provided NO_RESOURCE_PROMPT = 0x02, // don't prompt for resource ALLOW_KORG_UPDATE = 0x04 // allow change to be sent to KOrganizer }; UpdateResult addEvent(KAEvent&, Akonadi::Collection* = nullptr, QWidget* msgParent = nullptr, int options = ALLOW_KORG_UPDATE, bool showKOrgErr = true); UpdateResult addEvents(QVector&, QWidget* msgParent = nullptr, bool allowKOrgUpdate = true, bool showKOrgErr = true); bool addArchivedEvent(KAEvent&, Akonadi::Collection* = nullptr); UpdateResult addTemplate(KAEvent&, Akonadi::Collection* = nullptr, QWidget* msgParent = nullptr); UpdateResult modifyEvent(KAEvent& oldEvent, KAEvent& newEvent, QWidget* msgParent = nullptr, bool showKOrgErr = true); UpdateResult updateEvent(KAEvent&, QWidget* msgParent = nullptr, bool archiveOnDelete = true); UpdateResult updateTemplate(KAEvent&, QWidget* msgParent = nullptr); UpdateResult deleteEvent(KAEvent&, bool archive = true, QWidget* msgParent = nullptr, bool showKOrgErr = true); UpdateResult deleteEvents(QVector&, bool archive = true, QWidget* msgParent = nullptr, bool showKOrgErr = true); UpdateResult deleteTemplates(const KAEvent::List& events, QWidget* msgParent = nullptr); inline UpdateResult deleteTemplate(KAEvent& event, QWidget* msgParent = nullptr) { KAEvent::List e; e += &event; return deleteTemplates(e, msgParent); } void deleteDisplayEvent(const QString& eventID); UpdateResult reactivateEvent(KAEvent&, Akonadi::Collection* = nullptr, QWidget* msgParent = nullptr, bool showKOrgErr = true); UpdateResult reactivateEvents(QVector&, QVector& ineligibleIDs, Akonadi::Collection* = nullptr, QWidget* msgParent = nullptr, bool showKOrgErr = true); UpdateResult enableEvents(QVector&, bool enable, QWidget* msgParent = nullptr); QVector getSortedActiveEvents(QObject* parent, AlarmListModel** model = nullptr); void purgeArchive(int purgeDays); // must only be called from KAlarmApp::processQueue() -void displayKOrgUpdateError(QWidget* parent, UpdateError, const UpdateResult &korgError, int nAlarms = 0); +void displayKOrgUpdateError(QWidget* parent, UpdateError, const UpdateResult& korgError, int nAlarms = 0); Desktop currentDesktopIdentity(); QString currentDesktopIdentityName(); QStringList checkRtcWakeConfig(bool checkEventExists = false); void deleteRtcWakeConfig(); void cancelRtcWake(QWidget* msgParent, const QString& eventId = QString()); bool setRtcWakeTime(unsigned triggerTime, QWidget* parent); /** Return a prompt string to ask the user whether to convert the calendar to the * current format. * @param calendarName The calendar name * @param calendarVersion The calendar version * @param whole If true, the whole calendar needs to be converted; else * only some alarms may need to be converted. */ QString conversionPrompt(const QString& calendarName, const QString& calendarVersion, bool whole); Akonadi::Collection invalidCollection(); // for use as a non-const default parameter #ifndef NDEBUG void setTestModeConditions(); void setSimulatedSystemTime(const KADateTime&); #endif } // namespace KAlarm bool caseInsensitiveLessThan(const QString& s1, const QString& s2); #endif // FUNCTIONS_H // vim: et sw=4: diff --git a/src/kalarmapp.cpp b/src/kalarmapp.cpp index 1c91841e..c241990f 100644 --- a/src/kalarmapp.cpp +++ b/src/kalarmapp.cpp @@ -1,2493 +1,2493 @@ /* * kalarmapp.cpp - the KAlarm application object * Program: kalarm * Copyright © 2001-2018 by 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 "kalarm.h" #include "kalarmapp.h" #include "alarmcalendar.h" #include "alarmlistview.h" #include "alarmtime.h" #include "commandoptions.h" #include "dbushandler.h" #include "editdlgtypes.h" #include "collectionmodel.h" #include "functions.h" #include "kamail.h" #include "mainwindow.h" #include "messagebox.h" #include "messagewin.h" #include "kalarmmigrateapplication.h" #include "preferences.h" #include "prefdlg.h" #include "shellprocess.h" #include "startdaytimer.h" #include "traywindow.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 #include static const int AKONADI_TIMEOUT = 30; // timeout (seconds) for Akonadi collections to be populated /****************************************************************************** * Find the maximum number of seconds late which a late-cancel alarm is allowed * to be. This is calculated as the late cancel interval, plus a few seconds * leeway to cater for any timing irregularities. */ static inline int maxLateness(int lateCancel) { static const int LATENESS_LEEWAY = 5; int lc = (lateCancel >= 1) ? (lateCancel - 1)*60 : 0; return LATENESS_LEEWAY + lc; } KAlarmApp* KAlarmApp::mInstance = nullptr; int KAlarmApp::mActiveCount = 0; int KAlarmApp::mFatalError = 0; QString KAlarmApp::mFatalMessage; /****************************************************************************** * Construct the application. */ KAlarmApp::KAlarmApp(int& argc, char** argv) : QApplication(argc, argv), mInitialised(false), mRedisplayAlarms(false), mQuitting(false), mReadOnly(false), mLoginAlarmsDone(false), mDBusHandler(new DBusHandler()), mTrayWindow(nullptr), mAlarmTimer(nullptr), mArchivedPurgeDays(-1), // default to not purging mPurgeDaysQueued(-1), mPendingQuit(false), mCancelRtcWake(false), mProcessingQueue(false), mAlarmsEnabled(true) { qCDebug(KALARM_LOG); 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()); if (initialise()) // initialise calendars and alarm timer { connect(AkonadiModel::instance(), &AkonadiModel::collectionAdded, this, &KAlarmApp::purgeNewArchivedDefault); connect(AkonadiModel::instance(), &Akonadi::EntityTreeModel::collectionTreeFetched, this, &KAlarmApp::checkWritableCalendar); connect(AkonadiModel::instance(), &AkonadiModel::migrationCompleted, this, &KAlarmApp::checkWritableCalendar); KConfigGroup config(KSharedConfig::openConfig(), "General"); mNoSystemTray = config.readEntry("NoSystemTray", false); mOldShowInSystemTray = wantShowInSystemTray(); DateTime::setStartOfDay(Preferences::startOfDay()); mPrefsArchivedColour = Preferences::archivedColour(); } // Check if KOrganizer is installed const QString korg = QStringLiteral("korganizer"); mKOrganizerEnabled = !QStandardPaths::findExecutable(korg).isEmpty(); if (!mKOrganizerEnabled) { qCDebug(KALARM_LOG) << "KOrganizer options disabled (KOrganizer not found)"; } // Check if the window manager can't handle keyboard focus transfer between windows mWindowFocusBroken = (KAlarm::currentDesktopIdentity() == KAlarm::Desktop::Unity); if (mWindowFocusBroken) { qCDebug(KALARM_LOG) << "Window keyboard focus broken"; } } /****************************************************************************** */ KAlarmApp::~KAlarmApp() { while (!mCommandProcesses.isEmpty()) { ProcData* pd = mCommandProcesses[0]; mCommandProcesses.pop_front(); delete pd; } AlarmCalendar::terminateCalendars(); } /****************************************************************************** * Return the one and only KAlarmApp instance. * If it doesn't already exist, it is created first. */ KAlarmApp* KAlarmApp::create(int& argc, char** argv) { if (!mInstance) { mInstance = new KAlarmApp(argc, argv); if (mFatalError) mInstance->quitFatal(); } return mInstance; } /****************************************************************************** * (Re)initialise 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::initialise() { if (!mAlarmTimer) { mAlarmTimer = new QTimer(this); mAlarmTimer->setSingleShot(true); connect(mAlarmTimer, &QTimer::timeout, this, &KAlarmApp::checkNextDueAlarm); } if (!AlarmCalendar::resources()) { qCDebug(KALARM_LOG) << "initialising calendars"; if (AlarmCalendar::initialiseCalendars()) { connect(AlarmCalendar::resources(), &AlarmCalendar::earliestAlarmChanged, this, &KAlarmApp::checkNextDueAlarm); connect(AlarmCalendar::resources(), &AlarmCalendar::atLoginEventAdded, this, &KAlarmApp::atLoginEventAdded); return true; } } return false; } /****************************************************************************** * Restore the saved session if required. */ bool KAlarmApp::restoreSession() { if (!isSessionRestored()) return false; if (mFatalError) { quitFatal(); return false; } // Process is being restored by session management. qCDebug(KALARM_LOG) << "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 == QStringLiteral("MainWindow")) { MainWindow* win = MainWindow::create(true); win->restore(i, false); if (win->isHiddenTrayParent()) trayParent = win; else win->show(); } else if (type == QStringLiteral("MessageWin")) { MessageWin* win = new MessageWin; win->restore(i, false); if (win->isValid()) { if (AkonadiModel::instance()->isCollectionTreeFetched()) 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) << "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); 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; bool processOptions = (!firstInstance || !isSessionRestored()); if (processOptions) { options->process(); #ifndef NDEBUG if (options->simulationTime().isValid()) KAlarm::setSimulatedSystemTime(options->simulationTime()); #endif command = options->command(); if (options->disableAll()) setAlarmsEnabled(false); // disable alarm monitoring // Handle options which exit with a terminal message, before // making the application a unique application, since a // unique application won't output to the terminal if another // instance is already running. switch (command) { case CommandOptions::CMD_ERROR: if (outputText) { *outputText = options->outputText(); delete options; return 1; } mReadOnly = true; // don't need write access to calendars exitCode = 1; break; case CommandOptions::EXIT: if (outputText) { *outputText = options->outputText(); delete options; return 0; } exitCode = -1; break; default: break; } } // Make this a unique application. KDBusService* s = new KDBusService(KDBusService::Unique, this); connect(this, &KAlarmApp::aboutToQuit, s, &KDBusService::deleteLater); connect(s, &KDBusService::activateRequested, this, &KAlarmApp::activateByDBus); if (processOptions) { switch (command) { case CommandOptions::TRIGGER_EVENT: case CommandOptions::CANCEL_EVENT: { // Display or delete the event with the specified event ID EventFunc function = (command == CommandOptions::TRIGGER_EVENT) ? EVENT_TRIGGER : EVENT_CANCEL; // Open the calendar, don't start processing execution queue yet, // and wait for the Akonadi collection to be populated. if (!initCheck(true, true, options->eventId().collectionId())) exitCode = 1; else { startProcessQueue(); // start processing the execution queue dontRedisplay = true; if (!handleEvent(options->eventId(), function, true)) { CommandOptions::printError(xi18nc("@info:shell", "%1: Event %2 not found, or not unique", QStringLiteral("--") + options->commandName(), options->eventId().eventId())); exitCode = 1; } } 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 Akonadi collections to be populated. mReadOnly = true; // don't need write access to calendars if (!initCheck(true, true)) exitCode = 1; else { dontRedisplay = true; QStringList alarms = scheduledAlarmList(); for (int i = 0, count = alarms.count(); i < count; ++i) std::cout << alarms[i].toUtf8().constData() << std::endl; } break; case CommandOptions::EDIT: // Edit a specified existing alarm. // Open the calendar and wait for the Akonadi collection to be populated. if (!initCheck(false, true, options->eventId().collectionId())) exitCode = 1; else if (!KAlarm::editAlarmById(options->eventId())) { CommandOptions::printError(xi18nc("@info:shell", "%1: Event %2 not found, or not editable", QStringLiteral("--") + options->commandName(), options->eventId().eventId())); exitCode = 1; } 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)) { KAEvent::Flags flags = options->flags(); 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); } 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()); } break; case CommandOptions::NEW: // Display a message or file, execute a command, or send an email if (!initCheck() || !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; break; case CommandOptions::TRAY: // Display only the system tray icon if (Preferences::showInSystemTray() && QSystemTrayIcon::isSystemTrayAvailable()) { if (!initCheck() // open the calendar, start processing execution queue || !displayTrayIcon(true)) exitCode = 1; break; } // fall through to NONE case CommandOptions::NONE: // No arguments - run interactively & display the main window #ifndef NDEBUG if (options->simulationTime().isValid() && !firstInstance) break; // simulating time: don't open main window if already running #endif if (!initCheck()) exitCode = 1; else { if (mTrayWindow && mTrayWindow->assocMainWindow() && !mTrayWindow->assocMainWindow()->isVisible()) mTrayWindow->showAssocMainWindow(); else { MainWindow* win = MainWindow::create(); if (command == CommandOptions::TRAY) win->setWindowState(win->windowState() | Qt::WindowMinimized); win->show(); } } break; default: break; } } if (options != CommandOptions::firstInstance()) delete options; // If this is the first time through, redisplay any alarm message windows // from last time. if (firstInstance && !dontRedisplay && !exitCode) { /* First time through, so redisplay alarm message windows from last time. * But it is possible for session restoration in some circumstances to * not create any windows, in which case the alarm calendars will have * been deleted - if so, don't try to do anything. (This has been known * to happen under the Xfce desktop.) */ if (AlarmCalendar::resources()) { if (AkonadiModel::instance()->isCollectionTreeFetched()) { 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. quitIf(exitCode >= 0 ? exitCode : 0); return -1; // continue executing the application instance } /****************************************************************************** * 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; 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 initialise() method, in case KAlarm is // started again before application exit completes! qCDebug(KALARM_LOG) << exitCode << ": quitting"; MessageWin::stopAudio(true); if (mCancelRtcWake) { KAlarm::setRtcWakeTime(0, nullptr); KAlarm::deleteRtcWakeConfig(); } delete mAlarmTimer; // prevent checking for alarms after deleting calendars mAlarmTimer = nullptr; mInitialised = false; // prevent processQueue() from running AlarmCalendar::terminateCalendars(); 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); 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; // fall through to '3' case 3: if (mInstance) mInstance->quitIf(1, true); break; } QTimer::singleShot(1000, this, &KAlarmApp::quitFatal); } /****************************************************************************** * Called by the alarm timer when the next alarm is due. * Also called when the execution queue has finished processing to check for the * next alarm. */ void KAlarmApp::checkNextDueAlarm() { if (!mAlarmsEnabled) return; // Find the first alarm due const KAEvent* nextEvent = AlarmCalendar::resources()->earliestAlarm(); if (!nextEvent) return; // there are no alarms pending const KADateTime nextDt = nextEvent->nextTrigger(KAEvent::ALL_TRIGGER).effectiveKDateTime(); const KADateTime now = KADateTime::currentDateTime(Preferences::timeSpec()); qint64 interval = now.msecsTo(nextDt); qCDebug(KALARM_LOG) << "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) << 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. #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) << 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) { EventId id(event); for (int i = 0, end = mActionQueue.count(); i < end; ++i) { if (mActionQueue[i].function == EVENT_HANDLE && mActionQueue[i].eventId == id) return; // the alarm is already queued } mActionQueue.enqueue(ActionQEntry(EVENT_HANDLE, id)); } /****************************************************************************** * Start processing the execution queue. */ void KAlarmApp::startProcessQueue() { if (!mInitialised) { qCDebug(KALARM_LOG); 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); mProcessingQueue = true; // Refresh alarms if that's been queued KAlarm::refreshAlarmsIfQueued(); if (!mLoginAlarmsDone) { // Queue all at-login alarms once only, at program start-up. // First, cancel any scheduled reminders or deferrals for them, // since these will be superseded by the new at-login trigger. KAEvent::List events = AlarmCalendar::resources()->atLoginAlarms(); for (int i = 0, end = events.count(); i < end; ++i) { KAEvent event = *events[i]; if (!cancelReminderAndDeferral(event)) { if (mAlarmsEnabled) queueAlarmId(event); } } mLoginAlarmsDone = true; } // Process queued events while (!mActionQueue.isEmpty()) { ActionQEntry& entry = mActionQueue.head(); if (entry.eventId.isEmpty()) { // It's a new alarm switch (entry.function) { case EVENT_TRIGGER: execAlarm(entry.event, entry.event.firstAlarm(), false); break; case EVENT_HANDLE: KAlarm::addEvent(entry.event, nullptr, nullptr, KAlarm::ALLOW_KORG_UPDATE | KAlarm::NO_RESOURCE_PROMPT); break; case EVENT_CANCEL: break; } } else handleEvent(entry.eventId, entry.function); mActionQueue.dequeue(); } // Purge the default archived alarms resource if it's time to do so if (mPurgeDaysQueued >= 0) { KAlarm::purgeArchive(mPurgeDaysQueued); mPurgeDaysQueued = -1; } // Now that the queue has been processed, quit if a quit was queued if (mPendingQuit) { if (quitIf(mPendingQuitCode)) return; // quitIf() can sometimes return, despite calling exit() } mProcessingQueue = false; // Schedule the application to be woken when the next alarm is due checkNextDueAlarm(); } } /****************************************************************************** * Called when a repeat-at-login alarm has been added externally. * Queues the alarm for triggering. * First, cancel any scheduled reminder or deferral for it, since these will be * superseded by the new at-login trigger. */ void KAlarmApp::atLoginEventAdded(const KAEvent& event) { KAEvent ev = event; if (!cancelReminderAndDeferral(ev)) { if (mAlarmsEnabled) { mActionQueue.enqueue(ActionQEntry(EVENT_HANDLE, EventId(ev))); 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); 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) << "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() { bool newShowInSysTray = wantShowInSystemTray(); if (newShowInSysTray != mOldShowInSystemTray) { // The system tray run mode has changed ++mActiveCount; // prevent the application from quitting MainWindow* win = mTrayWindow ? mTrayWindow->assocMainWindow() : nullptr; delete mTrayWindow; // remove the system tray icon if it is currently shown mTrayWindow = nullptr; mOldShowInSystemTray = newShowInSysTray; if (newShowInSysTray) { // Show the system tray icon displayTrayIcon(true); } else { // Stop showing the system tray icon if (win && win->isHidden()) { if (MainWindow::count() > 1) delete win; else { win->setWindowState(win->windowState() | Qt::WindowMinimized); win->show(); } } } --mActiveCount; } } /****************************************************************************** * Called when the start-of-day time preference setting has changed. * Change alarm times for date-only alarms. */ void KAlarmApp::changeStartOfDay() { DateTime::setStartOfDay(Preferences::startOfDay()); KAEvent::setStartOfDay(Preferences::startOfDay()); AlarmCalendar::resources()->adjustStartOfDay(); } /****************************************************************************** * Called when the default alarm message font preference setting has changed. * Notify KAEvent. */ void KAlarmApp::slotMessageFontChanged(const QFont& font) { KAEvent::setDefaultFont(font); } /****************************************************************************** * Called when the working time preference settings have changed. * Notify KAEvent. */ void KAlarmApp::slotWorkTimeChanged(const QTime& start, const QTime& end, const QBitArray& days) { KAEvent::setWorkTime(days, start, end); } /****************************************************************************** * Called when the holiday region preference setting has changed. * Notify KAEvent. */ void KAlarmApp::slotHolidaysChanged(const KHolidays::HolidayRegion& holidays) { KAEvent::setHolidays(holidays); } /****************************************************************************** * Called when the date for February 29th recurrences has changed in the * preferences settings. */ void KAlarmApp::slotFeb29TypeChanged(Preferences::Feb29Type type) { KARecurrence::Feb29Type rtype; switch (type) { default: case Preferences::Feb29_None: rtype = KARecurrence::Feb29_None; break; case Preferences::Feb29_Feb28: rtype = KARecurrence::Feb29_Feb28; break; case Preferences::Feb29_Mar1: rtype = KARecurrence::Feb29_Mar1; break; } KARecurrence::setDefaultFeb29Type(rtype); } /****************************************************************************** * Return whether the program is configured to be running in the system tray. */ bool KAlarmApp::wantShowInSystemTray() const { return Preferences::showInSystemTray() && QSystemTrayIcon::isSystemTrayAvailable(); } /****************************************************************************** * Called when all calendars have been fetched at startup. * 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 bool treeFetched = AkonadiModel::instance()->isCollectionTreeFetched(); if (treeFetched && mRedisplayAlarms) { mRedisplayAlarms = false; MessageWin::redisplayAlarms(); } if (!treeFetched || !AkonadiModel::instance()->isMigrationCompleted()) return; static bool done = false; if (done) return; done = true; qCDebug(KALARM_LOG); // Find whether there are any writable active alarm calendars bool active = !CollectionControlModel::enabledCollections(CalEvent::ACTIVE, true).isEmpty(); if (!active) { qCWarning(KALARM_LOG) << "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")); } } /****************************************************************************** * Called when a new collection has been added, or when a collection has been * set as the standard collection for its type. * If it is the default archived calendar, purge its old alarms if necessary. */ void KAlarmApp::purgeNewArchivedDefault(const Akonadi::Collection& collection) { Akonadi::Collection col(collection); if (CollectionControlModel::isStandard(col, CalEvent::ARCHIVED)) { // Allow time (1 minute) for AkonadiModel to be populated with the // collection's events before purging it. qCDebug(KALARM_LOG) << collection.id() << ": standard archived..."; QTimer::singleShot(60000, this, &KAlarmApp::purgeAfterDelay); } } /****************************************************************************** * Called after a delay, after the default archived calendar has been added to * AkonadiModel. * Purge old alarms from it if necessary. */ void KAlarmApp::purgeAfterDelay() { 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() { int newDays = Preferences::archivedKeepDays(); if (newDays != mArchivedPurgeDays) { 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() { QVector events = KAlarm::getSortedActiveEvents(this); QStringList alarms; for (int i = 0, count = events.count(); i < count; ++i) { const KAEvent* event = &events[i]; const KADateTime dateTime = event->nextTrigger(KAEvent::DISPLAY_TRIGGER).effectiveKDateTime().toLocalZone(); Akonadi::Collection c(event->collectionId()); AkonadiModel::instance()->refresh(c); QString text(c.resource() + 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 KCalCore::Duration &repeatInterval, int repeatCount, + const KARecurrence& recurrence, const KCalCore::Duration& repeatInterval, int repeatCount, uint mailFromID, const KCalCore::Person::List& mailAddresses, const QString& mailSubject, const QStringList& mailAttachments) { qCDebug(KALARM_LOG) << 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) { bool onceOnly = flags & KAEvent::REMINDER_ONCE; event.setReminder(reminderMinutes, onceOnly); } if (!audioFile.isEmpty()) event.setAudioFile(audioFile, audioVolume, -1, 0, (flags & KAEvent::REPEAT_SOUND) ? 0 : -1); if (!mailAddresses.isEmpty()) event.setEmail(mailFromID, mailAddresses, mailSubject, mailAttachments); event.setRecurrence(recurrence); event.setFirstRecurrence(); event.setRepetition(Repetition(repeatInterval, repeatCount - 1)); event.endChanges(); if (alarmTime <= now) { // Alarm is due for display already. // First execute it once without adding it to the calendar file. if (!mInitialised) mActionQueue.enqueue(ActionQEntry(event, EVENT_TRIGGER)); else execAlarm(event, event.firstAlarm(), false); // If it's a recurring alarm, reschedule it for its next occurrence if (!event.recurs() || event.setNextOccurrence(now) == KAEvent::NO_OCCURRENCE) return true; // It has recurrences in the future } // Queue the alarm for insertion into the calendar file mActionQueue.enqueue(ActionQEntry(event)); if (mInitialised) QTimer::singleShot(0, this, &KAlarmApp::processQueue); return true; } /****************************************************************************** * Called in response to a D-Bus request to trigger or cancel an event. * Optionally display the event. Delete the event from the calendar file and * from every main window instance. */ bool KAlarmApp::dbusHandleEvent(const EventId& eventID, EventFunc function) { qCDebug(KALARM_LOG) << eventID; mActionQueue.append(ActionQEntry(function, 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); return scheduledAlarmList().join(QStringLiteral("\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. * Reply = false if event ID not found, or if more than one event with the same * ID is found. */ bool KAlarmApp::handleEvent(const EventId& id, EventFunc function, bool checkDuplicates) { // Delete any expired wake-on-suspend config data KAlarm::checkRtcWakeConfig(); const QString eventID(id.eventId()); KAEvent* event = AlarmCalendar::resources()->event(id, checkDuplicates); if (!event) { if (id.collectionId() != -1) qCWarning(KALARM_LOG) << "Event ID not found, or duplicated:" << eventID; else qCWarning(KALARM_LOG) << "Event ID not found:" << eventID; return false; } switch (function) { case EVENT_CANCEL: qCDebug(KALARM_LOG) << eventID << ", CANCEL"; KAlarm::deleteEvent(*event, true); break; case EVENT_TRIGGER: // handle it if it's due, else execute it regardless case EVENT_HANDLE: // handle it if it's due { const KADateTime now = KADateTime::currentUtcDateTime(); qCDebug(KALARM_LOG) << eventID << "," << (function==EVENT_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(); 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) << "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) << "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) << "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) << "LATE_CANCEL"; bool cancel = false; if (alarm.dateTime().isDateOnly()) { // The alarm has no time, so cancel it if its date is too far past 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; 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. 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; 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) << "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) << "Alarm" << alarm.type() << ": skip"; } // If there is an alarm to execute, do this last after rescheduling/cancelling // any others. This ensures that the updated event is only saved once to the calendar. if (alarmToExecute.isValid()) execAlarm(*event, alarmToExecute, true, !alarmToExecute.repeatAtLogin()); else { if (function == EVENT_TRIGGER) { // 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. KAAlarm alarm = event->firstAlarm(); if (alarm.isValid()) execAlarm(*event, alarm, false); } if (updateCalAndDisplay) KAlarm::updateEvent(*event); // update the window lists and calendar file else if (function != EVENT_TRIGGER) { qCDebug(KALARM_LOG) << "No action"; } } 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. QString command = event.postAction(); qCDebug(KALARM_LOG) << 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) << "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()); 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 { 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) << "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 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); 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) << 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; } // 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 bool reminder = (alarm.type() & KAAlarm::REMINDER_ALARM); 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 (int i = 0, end = mCommandProcesses.count(); i < end; ++i) { ProcData* pd = mCommandProcesses[i]; if (pd->event->id() == event.id() && (pd->flags & ProcData::PRE_ACTION)) { qCDebug(KALARM_LOG) << "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. QString command = event.preAction(); qCDebug(KALARM_LOG) << "Pre-DISPLAY command:" << command; int flags = (reschedule ? ProcData::RESCHEDULE : 0) | (allowDefer ? ProcData::ALLOW_DEFER : 0); if (doShellCommand(command, event, &alarm, (flags | ProcData::PRE_ACTION))) { AlarmCalendar::resources()->setAlarmPending(&event); return result; // display the message after the command completes } // Error executing command if (event.extraActionOptions() & KAEvent::CancelOnPreActError) { // Cancel the rest of the alarm execution qCDebug(KALARM_LOG) << 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 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) << "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. 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) << (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. int flags = (event.commandXterm() ? ProcData::EXEC_IN_XTERM : 0) | (event.commandDisplay() ? ProcData::DISP_OUTPUT : 0); QString command = event.cleanText(); if (event.commandScript()) { // Store the command script in a temporary file for execution qCDebug(KALARM_LOG) << "Script"; 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) << 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) << 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) << "Command failed (no terminal selected)"; QStringList errors; 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()) { QString dateTime = alarm->dateTime().formatLocale(); heading.sprintf("\n******* KAlarm %s *******\n", dateTime.toLatin1().data()); } 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) << "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) << 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 [>|;]. 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 [>|;]. 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(); } 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); // Find this command in the command list for (int i = 0, end = mCommandProcesses.count(); i < end; ++i) { ProcData* pd = mCommandProcesses[i]; if (pd->process == proc) { // Found the command. Check its exit status. bool executeAlarm = pd->preAction(); ShellProcess::Status status = proc->status(); if (status == ShellProcess::SUCCESS && !proc->exitCode()) { qCDebug(KALARM_LOG) << 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) << pd->event->id() << ":" << errmsg << "exit status =" << status << ", code =" << proc->exitCode(); else qCWarning(KALARM_LOG) << pd->event->id() << ":" << errmsg << "exit status =" << status; if (pd->messageBoxParent) { // Close the existing informational KMessageBox for this process 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) << pd->event->id() << ": pre-action failed: cancelled"; if (pd->reschedule()) rescheduleAlarm(*pd->event, *pd->alarm, true); executeAlarm = false; } } if (pd->preAction()) AlarmCalendar::resources()->setAlarmPending(pd->event, false); if (executeAlarm) execAlarm(*pd->event, *pd->alarm, pd->reschedule(), pd->allowDefer(), true); mCommandProcesses.removeAt(i); delete pd; break; } } // If there are now no executing shell commands, quit if a quit was queued if (mPendingQuit && mCommandProcesses.isEmpty()) quitIf(mPendingQuitCode); } /****************************************************************************** * Output an error message for a shell command, and record the alarm's error status. */ void KAlarmApp::commandErrorMsg(const ShellProcess* proc, const KAEvent& event, const KAAlarm* alarm, int flags, const QStringList& errors) { KAEvent::CmdErrType cmderr; QString dontShowAgain; QStringList errmsgs = errors; if (flags & ProcData::PRE_ACTION) { if (event.extraActionOptions() & KAEvent::DontShowPreActError) return; // don't notify user of any errors for the alarm errmsgs += i18nc("@info", "Pre-alarm action:"); dontShowAgain = QStringLiteral("Pre"); cmderr = KAEvent::CMD_ERROR_PRE; } else if (flags & ProcData::POST_ACTION) { errmsgs += i18nc("@info", "Post-alarm action:"); dontShowAgain = QStringLiteral("Post"); cmderr = (event.commandError() == KAEvent::CMD_ERROR_PRE) ? KAEvent::CMD_ERROR_PRE_POST : KAEvent::CMD_ERROR_POST; } else { dontShowAgain = QStringLiteral("Exec"); cmderr = KAEvent::CMD_ERROR; } // Record the alarm's error status setEventCommandError(event, cmderr); // Display an error message if (proc) { errmsgs += proc->errorMessage(); if (!(flags & ProcData::TEMP_FILE)) errmsgs += proc->command(); dontShowAgain += QString::number(proc->status()); } MessageWin::showError(event, (alarm ? alarm->dateTime() : DateTime()), errmsgs, dontShowAgain); } /****************************************************************************** * Notes that an informational KMessageBox is displayed for this process. */ void KAlarmApp::commandMessage(ShellProcess* proc, QWidget* parent) { // Find this command in the command list for (int i = 0, end = mCommandProcesses.count(); i < end; ++i) { ProcData* pd = mCommandProcesses[i]; 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, bool waitForCollection, Akonadi::Collection::Id collectionId) { static bool firstTime = true; if (firstTime) qCDebug(KALARM_LOG) << "first time"; if (initialise() || firstTime) { /* Need to open the display calendar now, since otherwise if display * alarms are immediately due, they will often be processed while * MessageWin::redisplayAlarms() is executing open() (but before open() * completes), which causes problems!! */ AlarmCalendar::displayCalendar()->open(); if (!AlarmCalendar::resources()->open()) return false; } if (firstTime) { setArchivePurgeDays(); // Warn the user if there are no writable active alarm calendars checkWritableCalendar(); firstTime = false; } if (!calendarOnly) startProcessQueue(); // start processing the execution queue if (waitForCollection) { // Wait for one or all Akonadi collections to be populated if (!CollectionControlModel::instance()->waitUntilPopulated(collectionId, AKONADI_TIMEOUT)) return false; } 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 = AlarmCalendar::resources()->event(EventId(event)); if (ev && ev->commandError() != err) ev->setCommandError(err); AkonadiModel::instance()->updateCommandError(event); } /****************************************************************************** * Clear the command error for the specified alarm. */ void KAlarmApp::clearEventCommandError(const KAEvent& event, KAEvent::CmdErrType err) const { ProcData* pd = findCommandProcess(event.id()); if (pd && pd->eventDeleted) return; // the alarm has been deleted, so can't set error status KAEvent::CmdErrType newerr = static_cast(event.commandError() & ~err); event.setCommandError(newerr); KAEvent* ev = AlarmCalendar::resources()->event(EventId(event)); if (ev) { newerr = static_cast(ev->commandError() & ~err); ev->setCommandError(newerr); } AkonadiModel::instance()->updateCommandError(event); } /****************************************************************************** * Find the currently executing command process for an event ID, if any. */ KAlarmApp::ProcData* KAlarmApp::findCommandProcess(const QString& eventId) const { for (int i = 0, end = mCommandProcesses.count(); i < end; ++i) { ProcData* pd = mCommandProcesses[i]; 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 eff94a45..2b9f85b9 100644 --- a/src/kalarmapp.h +++ b/src/kalarmapp.h @@ -1,228 +1,228 @@ /* * kalarmapp.h - the KAlarm application object * Program: kalarm * Copyright © 2001-2018 by 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; } namespace Akonadi { class Collection; } class DBusHandler; class MainWindow; class TrayWindow; class ShellProcess; using namespace KAlarmCal; class KAlarmApp : public QApplication { Q_OBJECT public: ~KAlarmApp(); static KAlarmApp* create(int& argc, char** argv); 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 KCalCore::Duration &repeatInterval, int repeatCount, + const KCalCore::Duration& repeatInterval, int repeatCount, uint mailFromID = 0, const KCalCore::Person::List& mailAddresses = KCalCore::Person::List(), const QString& mailSubject = QString(), const QStringList& mailAttachments = QStringList()); bool dbusTriggerEvent(const EventId& eventID) { return dbusHandleEvent(eventID, EVENT_TRIGGER); } bool dbusDeleteEvent(const EventId& eventID) { return dbusHandleEvent(eventID, EVENT_CANCEL); } QString dbusList(); public Q_SLOTS: void activateByDBus(const QStringList& args, const QString& workingDirectory); void processQueue(); void setAlarmsEnabled(bool); void purgeNewArchivedDefault(const Akonadi::Collection&); 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 checkWritableCalendar(); void slotMessageFontChanged(const QFont&); void setArchivePurgeDays(); void slotPurge() { purge(mArchivedPurgeDays); } void purgeAfterDelay(); void slotCommandExited(ShellProcess*); private: enum EventFunc { EVENT_HANDLE, // if the alarm is due, execute it and then reschedule it EVENT_TRIGGER, // execute the alarm regardless, and then reschedule it if it already due EVENT_CANCEL // delete the alarm }; struct ProcData { ProcData(ShellProcess*, KAEvent*, KAAlarm*, int flags = 0); ~ProcData(); enum { PRE_ACTION = 0x01, POST_ACTION = 0x02, RESCHEDULE = 0x04, ALLOW_DEFER = 0x08, TEMP_FILE = 0x10, EXEC_IN_XTERM = 0x20, DISP_OUTPUT = 0x40 }; bool preAction() const { return flags & PRE_ACTION; } bool postAction() const { return flags & POST_ACTION; } bool reschedule() const { return flags & RESCHEDULE; } bool allowDefer() const { return flags & ALLOW_DEFER; } bool tempFile() const { return flags & TEMP_FILE; } bool execInXterm() const { return flags & EXEC_IN_XTERM; } bool dispOutput() const { return flags & DISP_OUTPUT; } ShellProcess* process; KAEvent* event; KAAlarm* alarm; QPointer messageBoxParent; QStringList tempFiles; int flags; bool eventDeleted; }; struct ActionQEntry { ActionQEntry(EventFunc f, const EventId& id) : function(f), eventId(id) { } ActionQEntry(const KAEvent& e, EventFunc f = EVENT_HANDLE) : function(f), event(e) { } ActionQEntry() { } EventFunc function; EventId eventId; KAEvent event; }; KAlarmApp(int& argc, char** argv); bool initialise(); int activateInstance(const QStringList& args, const QString& workingDirectory, QString* outputText); bool initCheck(bool calendarOnly = false, bool waitForCollection = false, Akonadi::Collection::Id = -1); bool quitIf(int exitCode, bool force = false); bool checkSystemTray(); void startProcessQueue(); void queueAlarmId(const KAEvent&); bool dbusHandleEvent(const EventId&, EventFunc); bool handleEvent(const EventId&, EventFunc, bool checkDuplicates = 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 bool mInitialised; // initialisation complete: ready to process execution queue bool mRedisplayAlarms; // need to redisplay alarms when collection tree fetched bool mQuitting; // a forced quit is in progress bool mReadOnly; // only read-only access to calendars is needed bool mLoginAlarmsDone; // alarms repeated at login have been processed QString mActivateArg0; // activate()'s first arg the first time it was called DBusHandler* mDBusHandler; // the parent of the main DCOP receiver object TrayWindow* mTrayWindow; // active system tray icon QTimer* mAlarmTimer; // activates KAlarm when next alarm is due QColor mPrefsArchivedColour; // archived alarms text colour int mArchivedPurgeDays; // how long to keep archived alarms, 0 = don't keep, -1 = keep indefinitely int mPurgeDaysQueued; // >= 0 to purge the archive calendar from KAlarmApp::processLoop() QList mCommandProcesses; // currently active command alarm processes QQueue mActionQueue; // queued commands and actions int mPendingQuitCode; // exit code for a pending quit bool mPendingQuit; // quit once the DCOP command and shell command queues have been processed bool mCancelRtcWake; // cancel RTC wake on quitting bool mProcessingQueue; // a mActionQueue entry is currently being processed bool mNoSystemTray; // no system tray exists bool mOldShowInSystemTray; // showing in system tray was selected bool mAlarmsEnabled; // alarms are enabled bool mKOrganizerEnabled; // KOrganizer options are enabled (korganizer exists) bool mWindowFocusBroken; // keyboard focus transfer between windows doesn't work }; inline KAlarmApp* theApp() { return KAlarmApp::instance(); } #endif // KALARMAPP_H // vim: et sw=4: diff --git a/src/resourceselector.cpp b/src/resourceselector.cpp index 1e9dd9e3..f4673a7d 100644 --- a/src/resourceselector.cpp +++ b/src/resourceselector.cpp @@ -1,637 +1,637 @@ /* * resourceselector.cpp - calendar resource selection widget * Program: kalarm * Copyright © 2006-2013 by David Jarvie * Based on KOrganizer's ResourceView class and KAddressBook's ResourceSelection class, * Copyright (C) 2003,2004 Cornelius Schumacher * Copyright (C) 2003-2004 Reinhold Kainhofer * Copyright (c) 2004 Tobias Koenig * * 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 "resourceselector.h" #include "kalarm.h" #include "alarmcalendar.h" #include "autoqpointer.h" #include "akonadiresourcecreator.h" #include "calendarmigrator.h" #include "kalarmapp.h" #include "messagebox.h" #include "packedlayout.h" #include "preferences.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "kalarm_debug.h" using namespace KCalCore; using namespace Akonadi; ResourceSelector::ResourceSelector(QWidget* parent) : QFrame(parent), mContextMenu(nullptr), mActionReload(nullptr), mActionShowDetails(nullptr), mActionSetColour(nullptr), mActionClearColour(nullptr), mActionEdit(nullptr), mActionUpdate(nullptr), mActionRemove(nullptr), mActionImport(nullptr), mActionExport(nullptr), mActionSetDefault(nullptr) { QBoxLayout* topLayout = new QVBoxLayout(this); QLabel* label = new QLabel(i18nc("@title:group", "Calendars"), this); topLayout->addWidget(label, 0, Qt::AlignHCenter); mAlarmType = new QComboBox(this); mAlarmType->addItem(i18nc("@item:inlistbox", "Active Alarms")); mAlarmType->addItem(i18nc("@item:inlistbox", "Archived Alarms")); mAlarmType->addItem(i18nc("@item:inlistbox", "Alarm Templates")); mAlarmType->setFixedHeight(mAlarmType->sizeHint().height()); mAlarmType->setWhatsThis(i18nc("@info:whatsthis", "Choose which type of data to show alarm calendars for")); topLayout->addWidget(mAlarmType); // No spacing between combo box and listview. CollectionFilterCheckListModel* model = new CollectionFilterCheckListModel(this); mListView = new CollectionView(model, this); connect(mListView->selectionModel(), &QItemSelectionModel::selectionChanged, this, &ResourceSelector::selectionChanged); mListView->setContextMenuPolicy(Qt::CustomContextMenu); connect(mListView, &CollectionView::customContextMenuRequested, this, &ResourceSelector::contextMenuRequested); mListView->setWhatsThis(i18nc("@info:whatsthis", "List of available calendars of the selected type. The checked state shows whether a calendar " "is enabled (checked) or disabled (unchecked). The default calendar is shown in bold.")); topLayout->addWidget(mListView, 1); PackedLayout* blayout = new PackedLayout(Qt::AlignHCenter); blayout->setMargin(0); topLayout->addLayout(blayout); mAddButton = new QPushButton(i18nc("@action:button", "Add..."), this); mEditButton = new QPushButton(i18nc("@action:button", "Edit..."), this); mDeleteButton = new QPushButton(i18nc("@action:button", "Remove"), this); blayout->addWidget(mAddButton); blayout->addWidget(mEditButton); blayout->addWidget(mDeleteButton); mEditButton->setWhatsThis(i18nc("@info:whatsthis", "Edit the highlighted calendar")); mDeleteButton->setWhatsThis(xi18nc("@info:whatsthis", "Remove the highlighted calendar from the list." "The calendar itself is left intact, and may subsequently be reinstated in the list if desired.")); mEditButton->setDisabled(true); mDeleteButton->setDisabled(true); connect(mAddButton, &QPushButton::clicked, this, &ResourceSelector::addResource); connect(mEditButton, &QPushButton::clicked, this, &ResourceSelector::editResource); connect(mDeleteButton, &QPushButton::clicked, this, &ResourceSelector::removeResource); connect(AkonadiModel::instance(), &AkonadiModel::collectionAdded, this, &ResourceSelector::slotCollectionAdded); connect(mAlarmType, static_cast(&QComboBox::activated), this, &ResourceSelector::alarmTypeSelected); QTimer::singleShot(0, this, SLOT(alarmTypeSelected())); Preferences::connect(SIGNAL(archivedKeepDaysChanged(int)), this, SLOT(archiveDaysChanged(int))); } /****************************************************************************** * Called when an alarm type has been selected. * Filter the resource list to show resources of the selected alarm type, and * add appropriate whatsThis texts to the list and to the Add button. */ void ResourceSelector::alarmTypeSelected() { QString addTip; switch (mAlarmType->currentIndex()) { case 0: mCurrentAlarmType = CalEvent::ACTIVE; addTip = i18nc("@info:tooltip", "Add a new active alarm calendar"); break; case 1: mCurrentAlarmType = CalEvent::ARCHIVED; addTip = i18nc("@info:tooltip", "Add a new archived alarm calendar"); break; case 2: mCurrentAlarmType = CalEvent::TEMPLATE; addTip = i18nc("@info:tooltip", "Add a new alarm template calendar"); break; } // WORKAROUND: Switch scroll bars off to avoid crash (see explanation // in reinstateAlarmTypeScrollBars() description). mListView->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); mListView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); mListView->collectionModel()->setEventTypeFilter(mCurrentAlarmType); mAddButton->setWhatsThis(addTip); mAddButton->setToolTip(addTip); // WORKAROUND: Switch scroll bars back on after allowing geometry to update ... QTimer::singleShot(0, this, &ResourceSelector::reinstateAlarmTypeScrollBars); selectionChanged(); // enable/disable buttons } /****************************************************************************** * WORKAROUND for crash due to presumed Qt bug. * Switch scroll bars off. This is to avoid a crash which can very occasionally * happen when changing from a list of calendars which requires vertical scroll * bars, to a list whose text is very slightly wider but which doesn't require * scroll bars at all. (The suspicion is that the width is such that it would * require horizontal scroll bars if the vertical scroll bars were still * present.) Presumably due to a Qt bug, this can result in a recursive call to * ResourceView::viewportEvent() with a Resize event. * * The crash only occurs if the ResourceSelector happens to have exactly (within * one pixel) the "right" width to create the crash. */ void ResourceSelector::reinstateAlarmTypeScrollBars() { mListView->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); mListView->setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded); } /****************************************************************************** * Prompt the user for a new resource to add to the list. */ void ResourceSelector::addResource() { AkonadiResourceCreator* creator = new AkonadiResourceCreator(mCurrentAlarmType, this); connect(creator, &AkonadiResourceCreator::finished, this, &ResourceSelector::resourceAdded); creator->createResource(); } /****************************************************************************** * Called when the job started by AkonadiModel::addCollection() has completed. */ void ResourceSelector::resourceAdded(AkonadiResourceCreator* creator, bool success) { if (success) { AgentInstance agent = creator->agentInstance(); if (agent.isValid()) { // Note that we're expecting the agent's Collection to be added mAddAgents += agent; } } delete creator; } /****************************************************************************** * Called when a collection is added to the AkonadiModel. */ void ResourceSelector::slotCollectionAdded(const Collection& collection) { if (collection.isValid()) { AgentInstance agent = AgentManager::self()->instance(collection.resource()); if (agent.isValid()) { int i = mAddAgents.indexOf(agent); if (i >= 0) { // The collection belongs to an agent created by addResource() CalEvent::Types types = CalEvent::types(collection.contentMimeTypes()); CollectionControlModel::setEnabled(collection, types, true); if (!(types & mCurrentAlarmType)) { // The user has selected alarm types for the resource // which don't include the currently displayed type. // Show a collection list which includes a selected type. int index = -1; if (types & CalEvent::ACTIVE) index = 0; else if (types & CalEvent::ARCHIVED) index = 1; else if (types & CalEvent::TEMPLATE) index = 2; if (index >= 0) { mAlarmType->setCurrentIndex(index); alarmTypeSelected(); } } mAddAgents.removeAt(i); } } } } /****************************************************************************** * Edit the currently selected resource. */ void ResourceSelector::editResource() { Collection collection = currentResource(); if (collection.isValid()) { AgentInstance instance = AgentManager::self()->instance(collection.resource()); if (instance.isValid()) instance.configure(this); } } /****************************************************************************** * Update the backend storage format for the currently selected resource in the * displayed list. */ void ResourceSelector::updateResource() { Collection collection = currentResource(); if (!collection.isValid()) return; AkonadiModel::instance()->refresh(collection); // update with latest data CalendarMigrator::updateToCurrentFormat(collection, true, this); } /****************************************************************************** * Remove the currently selected resource from the displayed list. */ void ResourceSelector::removeResource() { Collection collection = currentResource(); if (!collection.isValid()) return; QString name = collection.name(); // Check if it's the standard or only resource for at least one type. CalEvent::Types allTypes = AkonadiModel::types(collection); CalEvent::Types standardTypes = CollectionControlModel::standardTypes(collection, true); CalEvent::Type currentType = currentResourceType(); CalEvent::Type stdType = (standardTypes & CalEvent::ACTIVE) ? CalEvent::ACTIVE : (standardTypes & CalEvent::ARCHIVED) ? CalEvent::ARCHIVED : CalEvent::EMPTY; if (stdType == CalEvent::ACTIVE) { KAMessageBox::sorry(this, i18nc("@info", "You cannot remove your default active alarm calendar.")); return; } if (stdType == CalEvent::ARCHIVED && Preferences::archivedKeepDays()) { // Only allow the archived alarms standard resource to be removed if // we're not saving archived alarms. KAMessageBox::sorry(this, i18nc("@info", "You cannot remove your default archived alarm calendar " "while expired alarms are configured to be kept.")); return; } QString text; if (standardTypes) { // It's a standard resource for at least one alarm type if (allTypes != currentType) { // It also contains alarm types other than the currently displayed type QString stdTypes = CollectionControlModel::typeListForDisplay(standardTypes); QString otherTypes; CalEvent::Types nonStandardTypes(allTypes & ~standardTypes); if (nonStandardTypes != currentType) otherTypes = xi18nc("@info", "It also contains:%1", CollectionControlModel::typeListForDisplay(nonStandardTypes)); text = xi18nc("@info", "%1 is the default calendar for:%2%3" "Do you really want to remove it from all calendar lists?", name, stdTypes, otherTypes); } else text = xi18nc("@info", "Do you really want to remove your default calendar (%1) from the list?", name); } else if (allTypes != currentType) text = xi18nc("@info", "%1 contains:%2Do you really want to remove it from all calendar lists?", name, CollectionControlModel::typeListForDisplay(allTypes)); else text = xi18nc("@info", "Do you really want to remove the calendar %1 from the list?", name); if (KAMessageBox::warningContinueCancel(this, text, QString(), KStandardGuiItem::remove()) == KMessageBox::Cancel) return; AkonadiModel::instance()->removeCollection(collection); } /****************************************************************************** * Called when the current selection changes, to enable/disable the * Delete and Edit buttons accordingly. */ void ResourceSelector::selectionChanged() { bool state = mListView->selectionModel()->selectedRows().count(); mDeleteButton->setEnabled(state); mEditButton->setEnabled(state); } /****************************************************************************** * Initialise the button and context menu actions. */ void ResourceSelector::initActions(KActionCollection* actions) { mActionReload = new QAction(QIcon::fromTheme(QStringLiteral("view-refresh")), i18nc("@action Reload calendar", "Re&load"), this); actions->addAction(QStringLiteral("resReload"), mActionReload); connect(mActionReload, &QAction::triggered, this, &ResourceSelector::reloadResource); mActionShowDetails = new QAction(QIcon::fromTheme(QStringLiteral("help-about")), i18nc("@action", "Show &Details"), this); actions->addAction(QStringLiteral("resDetails"), mActionShowDetails); connect(mActionShowDetails, &QAction::triggered, this, &ResourceSelector::showInfo); mActionSetColour = new QAction(QIcon::fromTheme(QStringLiteral("color-picker")), i18nc("@action", "Set &Color..."), this); actions->addAction(QStringLiteral("resSetColour"), mActionSetColour); connect(mActionSetColour, &QAction::triggered, this, &ResourceSelector::setColour); mActionClearColour = new QAction(i18nc("@action", "Clear C&olor"), this); actions->addAction(QStringLiteral("resClearColour"), mActionClearColour); connect(mActionClearColour, &QAction::triggered, this, &ResourceSelector::clearColour); mActionEdit = new QAction(QIcon::fromTheme(QStringLiteral("document-properties")), i18nc("@action", "&Edit..."), this); actions->addAction(QStringLiteral("resEdit"), mActionEdit); connect(mActionEdit, &QAction::triggered, this, &ResourceSelector::editResource); mActionUpdate = new QAction(i18nc("@action", "&Update Calendar Format"), this); actions->addAction(QStringLiteral("resUpdate"), mActionUpdate); connect(mActionUpdate, &QAction::triggered, this, &ResourceSelector::updateResource); mActionRemove = new QAction(QIcon::fromTheme(QStringLiteral("edit-delete")), i18nc("@action", "&Remove"), this); actions->addAction(QStringLiteral("resRemove"), mActionRemove); connect(mActionRemove, &QAction::triggered, this, &ResourceSelector::removeResource); mActionSetDefault = new KToggleAction(this); actions->addAction(QStringLiteral("resDefault"), mActionSetDefault); connect(mActionSetDefault, &KToggleAction::triggered, this, &ResourceSelector::setStandard); QAction* action = new QAction(QIcon::fromTheme(QStringLiteral("document-new")), i18nc("@action", "&Add..."), this); actions->addAction(QStringLiteral("resAdd"), action); connect(action, &QAction::triggered, this, &ResourceSelector::addResource); mActionImport = new QAction(i18nc("@action", "Im&port..."), this); actions->addAction(QStringLiteral("resImport"), mActionImport); connect(mActionImport, &QAction::triggered, this, &ResourceSelector::importCalendar); mActionExport = new QAction(i18nc("@action", "E&xport..."), this); actions->addAction(QStringLiteral("resExport"), mActionExport); connect(mActionExport, &QAction::triggered, this, &ResourceSelector::exportCalendar); } void ResourceSelector::setContextMenu(QMenu* menu) { mContextMenu = menu; } /****************************************************************************** * Display the context menu for the selected calendar. */ void ResourceSelector::contextMenuRequested(const QPoint& viewportPos) { if (!mContextMenu) return; bool active = false; bool writable = false; bool updatable = false; Collection collection; if (mListView->selectionModel()->hasSelection()) { QModelIndex index = mListView->indexAt(viewportPos); if (index.isValid()) collection = mListView->collectionModel()->collection(index); else mListView->clearSelection(); } CalEvent::Type type = currentResourceType(); bool haveCalendar = collection.isValid(); if (haveCalendar) { // Note: the CollectionControlModel functions call AkonadiModel::refresh(collection) active = CollectionControlModel::isEnabled(collection, type); KACalendar::Compat compatibility; int rw = CollectionControlModel::isWritableEnabled(collection, type, compatibility); writable = (rw > 0); if (!rw && (compatibility & ~KACalendar::Converted) && !(compatibility & ~(KACalendar::Convertible | KACalendar::Converted))) updatable = true; // the calendar format is convertible to the current KAlarm format if (!(AkonadiModel::instance()->types(collection) & type)) type = CalEvent::EMPTY; } mActionReload->setEnabled(active); mActionShowDetails->setEnabled(haveCalendar); mActionSetColour->setEnabled(haveCalendar); mActionClearColour->setEnabled(haveCalendar); mActionClearColour->setVisible(AkonadiModel::instance()->backgroundColor(collection).isValid()); mActionEdit->setEnabled(haveCalendar); mActionUpdate->setEnabled(updatable); mActionRemove->setEnabled(haveCalendar); mActionImport->setEnabled(active && writable); mActionExport->setEnabled(active); QString text; switch (type) { case CalEvent::ACTIVE: text = i18nc("@action", "Use as &Default for Active Alarms"); break; case CalEvent::ARCHIVED: text = i18nc("@action", "Use as &Default for Archived Alarms"); break; case CalEvent::TEMPLATE: text = i18nc("@action", "Use as &Default for Alarm Templates"); break; default: break; } mActionSetDefault->setText(text); bool standard = CollectionControlModel::isStandard(collection, type); mActionSetDefault->setChecked(active && writable && standard); mActionSetDefault->setEnabled(active && writable); mContextMenu->popup(mListView->viewport()->mapToGlobal(viewportPos)); } /****************************************************************************** * Called from the context menu to reload the selected resource. */ void ResourceSelector::reloadResource() { Collection collection = currentResource(); if (collection.isValid()) AkonadiModel::instance()->reloadCollection(collection); } /****************************************************************************** * Called from the context menu to save the selected resource. */ void ResourceSelector::saveResource() { // Save resource is not applicable to Akonadi } /****************************************************************************** * Called when the length of time archived alarms are to be stored changes. * If expired alarms are now to be stored, set any single archived alarm * resource to be the default. */ void ResourceSelector::archiveDaysChanged(int days) { if (days) { if (!CollectionControlModel::getStandard(CalEvent::ARCHIVED).isValid()) { Collection::List cols = CollectionControlModel::enabledCollections(CalEvent::ARCHIVED, true); if (cols.count() == 1) { CollectionControlModel::setStandard(cols[0], CalEvent::ARCHIVED); theApp()->purgeNewArchivedDefault(cols[0]); } } } } /****************************************************************************** * Called from the context menu to set the selected resource as the default * for its alarm type. The resource is automatically made active. */ void ResourceSelector::setStandard() { Collection collection = currentResource(); if (collection.isValid()) { CalEvent::Type alarmType = currentResourceType(); bool standard = mActionSetDefault->isChecked(); if (standard) CollectionControlModel::setEnabled(collection, alarmType, true); CollectionControlModel::setStandard(collection, alarmType, standard); if (alarmType == CalEvent::ARCHIVED) theApp()->purgeNewArchivedDefault(collection); } } /****************************************************************************** * Called from the context menu to merge alarms from an external calendar into * the selected resource (if any). */ void ResourceSelector::importCalendar() { Collection collection = currentResource(); AlarmCalendar::importAlarms(this, (collection.isValid() ? &collection : nullptr)); } /****************************************************************************** * Called from the context menu to copy the selected resource's alarms to an * external calendar. */ void ResourceSelector::exportCalendar() { Collection calendar = currentResource(); if (calendar.isValid()) AlarmCalendar::exportAlarms(AlarmCalendar::resources()->events(calendar), this); } /****************************************************************************** * Called from the context menu to set a colour for the selected resource. */ void ResourceSelector::setColour() { Collection collection = currentResource(); if (collection.isValid()) { QColor colour = AkonadiModel::instance()->backgroundColor(collection); if (!colour.isValid()) colour = QApplication::palette().color(QPalette::Base); colour = QColorDialog::getColor(colour, this); if (colour.isValid()) AkonadiModel::instance()->setBackgroundColor(collection, colour); } } /****************************************************************************** * Called from the context menu to clear the display colour for the selected * resource. */ void ResourceSelector::clearColour() { Collection collection = currentResource(); if (collection.isValid()) AkonadiModel::instance()->setBackgroundColor(collection, QColor()); } /****************************************************************************** * Called from the context menu to display information for the selected resource. */ void ResourceSelector::showInfo() { Collection collection = currentResource(); if (collection.isValid()) { const QString name = collection.displayName(); QString id = collection.resource(); // resource name CalEvent::Type alarmType = currentResourceType(); QString calType = AgentManager::self()->instance(id).type().name(); QString storage = AkonadiModel::instance()->storageType(collection); QString location = collection.remoteId(); QUrl url = QUrl::fromUserInput(location, QString(), QUrl::AssumeLocalFile); if (url.isLocalFile()) location = url.path(); CalEvent::Types altypes = AkonadiModel::instance()->types(collection); QStringList alarmTypes; if (altypes & CalEvent::ACTIVE) alarmTypes << i18nc("@info", "Active alarms"); if (altypes & CalEvent::ARCHIVED) alarmTypes << i18nc("@info", "Archived alarms"); if (altypes & CalEvent::TEMPLATE) alarmTypes << i18nc("@info", "Alarm templates"); QString alarmTypeString = alarmTypes.join(i18nc("@info List separator", ", ")); QString perms = AkonadiModel::readOnlyTooltip(collection); if (perms.isEmpty()) perms = i18nc("@info", "Read-write"); QString enabled = CollectionControlModel::isEnabled(collection, alarmType) ? i18nc("@info", "Enabled") : i18nc("@info", "Disabled"); QString std = CollectionControlModel::isStandard(collection, alarmType) ? i18nc("@info Parameter in 'Default calendar: Yes/No'", "Yes") : i18nc("@info Parameter in 'Default calendar: Yes/No'", "No"); QString text = xi18nc("@info", "%1" "ID: %2" "Calendar type: %3" "Contents: %4" "%5: %6" "Permissions: %7" "Status: %8" "Default calendar: %9", name, id, calType, alarmTypeString, storage, location, perms, enabled, std); // Display the collection information. Because the user requested // the information, don't raise a KNotify event. - KAMessageBox::information(this, text, QString(), QString(), nullptr); + KAMessageBox::information(this, text, QString(), QString(), 0); } } /****************************************************************************** * Return the currently selected resource in the list. */ Collection ResourceSelector::currentResource() const { return mListView->collection(mListView->selectionModel()->currentIndex()); } /****************************************************************************** * Return the currently selected resource type. */ CalEvent::Type ResourceSelector::currentResourceType() const { switch (mAlarmType->currentIndex()) { case 0: return CalEvent::ACTIVE; case 1: return CalEvent::ARCHIVED; case 2: return CalEvent::TEMPLATE; default: return CalEvent::EMPTY; } } void ResourceSelector::resizeEvent(QResizeEvent* re) { Q_EMIT resized(re->oldSize(), re->size()); } // vim: et sw=4: