diff --git a/src/alarmcalendar.cpp b/src/alarmcalendar.cpp index 5ebcab8b..26d9c52b 100644 --- a/src/alarmcalendar.cpp +++ b/src/alarmcalendar.cpp @@ -1,1323 +1,1325 @@ /* * alarmcalendar.cpp - KAlarm calendar file access * Program: kalarm * Copyright © 2001-2020 David Jarvie * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "alarmcalendar.h" #include "kalarm.h" #include "functions.h" #include "kalarmapp.h" #include "mainwindow.h" #include "preferences.h" #include "resources/datamodel.h" #include "resources/resources.h" #include "lib/messagebox.h" #include "kalarm_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include using namespace KCalendarCore; using namespace KAlarmCal; static const QString displayCalendarName = QStringLiteral("displaying.ics"); static const ResourceId DISPLAY_RES_ID = -1; // resource ID used for displaying calendar ResourcesCalendar* ResourcesCalendar::mInstance = nullptr; DisplayCalendar* DisplayCalendar::mInstance = nullptr; /****************************************************************************** * Initialise backend calendar access. */ void AlarmCalendar::initialise() { KACalendar::setProductId(KALARM_NAME, KALARM_VERSION); CalFormat::setApplication(QStringLiteral(KALARM_NAME), QString::fromLatin1(KACalendar::icalProductId())); } /****************************************************************************** * Initialise the resource alarm calendars, and ensure that their file names are * different. The resources calendar contains the active alarms, archived alarms * and alarm templates; */ void ResourcesCalendar::initialise() { AlarmCalendar::initialise(); mInstance = new ResourcesCalendar(); } /****************************************************************************** * Initialise the display alarm calendar. * It is user-specific, and contains details of alarms which are currently being * displayed to that user and which have not yet been acknowledged; */ void DisplayCalendar::initialise() { QDir dir; dir.mkpath(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation)); const QString displayCal = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QLatin1Char('/') + displayCalendarName; mInstance = new DisplayCalendar(displayCal); } /****************************************************************************** * Terminate access to the resource calendars. */ void ResourcesCalendar::terminate() { delete mInstance; mInstance = nullptr; } /****************************************************************************** * Terminate access to the display calendar. */ void DisplayCalendar::terminate() { delete mInstance; mInstance = nullptr; } /****************************************************************************** * Return the display calendar, opening it first if necessary. */ DisplayCalendar* DisplayCalendar::instanceOpen() { if (mInstance->open()) return mInstance; qCCritical(KALARM_LOG) << "DisplayCalendar::instanceOpen: Open error"; return nullptr; } /****************************************************************************** * Find and return the event with the specified ID. * The calendar searched is determined by the calendar identifier in the ID. */ KAEvent* ResourcesCalendar::getEvent(const EventId& eventId) { if (eventId.eventId().isEmpty()) return nullptr; return mInstance->event(eventId); } /****************************************************************************** * Constructor for the resources calendar. */ AlarmCalendar::AlarmCalendar() { } /****************************************************************************** * Constructor for the resources calendar. */ ResourcesCalendar::ResourcesCalendar() : AlarmCalendar() { Resources* resources = Resources::instance(); connect(resources, &Resources::resourceAdded, this, &ResourcesCalendar::slotResourceAdded); connect(resources, &Resources::eventsAdded, this, &ResourcesCalendar::slotEventsAdded); connect(resources, &Resources::eventsToBeRemoved, this, &ResourcesCalendar::slotEventsToBeRemoved); connect(resources, &Resources::eventUpdated, this, &ResourcesCalendar::slotEventUpdated); connect(resources, &Resources::resourcesPopulated, this, &ResourcesCalendar::slotResourcesPopulated); connect(resources, &Resources::settingsChanged, this, &ResourcesCalendar::slotResourceSettingsChanged); // Fetch events from all resources which already exist. QVector allResources = Resources::enabledResources(); for (Resource& resource : allResources) slotResourceAdded(resource); } /****************************************************************************** * Constructor for the display calendar file. */ DisplayCalendar::DisplayCalendar(const QString& path) : AlarmCalendar() , mDisplayCalPath(path) , mDisplayICalPath(path) { mDisplayICalPath.replace(QStringLiteral("\\.vcs$"), QStringLiteral(".ics")); mCalType = (mDisplayCalPath == mDisplayICalPath) ? LOCAL_ICAL : LOCAL_VCAL; // is the calendar in ICal or VCal format? } ResourcesCalendar::~ResourcesCalendar() { close(); } DisplayCalendar::~DisplayCalendar() { close(); } /****************************************************************************** * Open the calendar if not already open, and load it into memory. */ bool DisplayCalendar::open() { if (isOpen()) return true; // Open the display calendar. qCDebug(KALARM_LOG) << "DisplayCalendar::open:" << mDisplayCalPath; if (!mCalendarStorage) { MemoryCalendar::Ptr calendar(new MemoryCalendar(Preferences::timeSpecAsZone())); mCalendarStorage = FileStorage::Ptr(new FileStorage(calendar, mDisplayCalPath)); } // Check for file's existence, assuming that it does exist when uncertain, // to avoid overwriting it. QFileInfo fi(mDisplayCalPath); if (!fi.exists() || !fi.isFile() || load() == 0) { // The calendar file doesn't yet exist, or it's zero length, so create a new one if (saveCal(mDisplayICalPath)) load(); } if (!mOpen) { mCalendarStorage->calendar().clear(); mCalendarStorage.clear(); } return isOpen(); } /****************************************************************************** * Load the calendar into memory. * Reply = 1 if success * = 0 if zero-length file exists. * = -1 if failure to load calendar file * = -2 if instance uninitialised. */ int DisplayCalendar::load() { // Load the display calendar. if (!mCalendarStorage) return -2; qCDebug(KALARM_LOG) << "DisplayCalendar::load:" << mDisplayCalPath; if (!mCalendarStorage->load()) { // Load error. Check if the file is zero length QFileInfo fi(mDisplayCalPath); if (fi.exists() && !fi.size()) return 0; // file is zero length qCCritical(KALARM_LOG) << "DisplayCalendar::load: Error loading calendar file '" << mDisplayCalPath <<"'"; KAMessageBox::error(MainWindow::mainMainWindow(), xi18nc("@info", "Error loading calendar:%1Please fix or delete the file.", mDisplayCalPath)); // load() could have partially populated the calendar, so clear it out mCalendarStorage->calendar()->close(); mCalendarStorage->calendar().clear(); mCalendarStorage.clear(); mOpen = false; return -1; } QString versionString; KACalendar::updateVersion(mCalendarStorage, versionString); // convert events to current KAlarm format for when calendar is saved updateKAEvents(); mOpen = true; return 1; } /****************************************************************************** * Reload the calendar resources into memory. */ bool ResourcesCalendar::reload() { qCDebug(KALARM_LOG) << "ResourcesCalendar::reload"; bool ok = true; QVector resources = Resources::enabledResources(); for (Resource& resource : resources) ok = ok && resource.reload(); return ok; } /****************************************************************************** * Reload the calendar file into memory. */ bool DisplayCalendar::reload() { if (!mCalendarStorage) return false; qCDebug(KALARM_LOG) << "DisplayCalendar::reload:" << mDisplayCalPath; close(); return open(); } /****************************************************************************** * Save the calendar. */ bool ResourcesCalendar::save() { bool ok = true; // Get all enabled, writable resources. QVector resources = Resources::enabledResources(CalEvent::EMPTY, true); for (Resource& resource : resources) ok = ok && resource.save(); return ok; } /****************************************************************************** * Save the calendar. */ bool DisplayCalendar::save() { return saveCal(); } /****************************************************************************** * Save the calendar from memory to file. * If a filename is specified, create a new calendar file. */ bool DisplayCalendar::saveCal(const QString& newFile) { if (!mCalendarStorage) return false; if (!mOpen && newFile.isEmpty()) return false; qCDebug(KALARM_LOG) << "DisplayCalendar::saveCal:" << "\"" << newFile; QString saveFilename = newFile.isEmpty() ? mDisplayCalPath : newFile; if (mCalType == LOCAL_VCAL && newFile.isNull()) saveFilename = mDisplayICalPath; mCalendarStorage->setFileName(saveFilename); mCalendarStorage->setSaveFormat(new ICalFormat); if (!mCalendarStorage->save()) { qCCritical(KALARM_LOG) << "DisplayCalendar::saveCal: Saving" << saveFilename << "failed."; KAMessageBox::error(MainWindow::mainMainWindow(), xi18nc("@info", "Failed to save calendar to %1", mDisplayICalPath)); return false; } if (mCalType == LOCAL_VCAL) { // The file was in vCalendar format, but has now been saved in iCalendar format. mDisplayCalPath = mDisplayICalPath; mCalType = LOCAL_ICAL; } return true; } /****************************************************************************** * Close display calendar file at program exit. */ void ResourcesCalendar::close() { // Resource map should be empty, but just in case... while (!mResourceMap.isEmpty()) removeKAEvents(mResourceMap.begin().key(), true, CalEvent::ACTIVE | CalEvent::ARCHIVED | CalEvent::TEMPLATE | CalEvent::DISPLAYING); } /****************************************************************************** * Close display calendar file at program exit. */ void DisplayCalendar::close() { if (mCalendarStorage) { mCalendarStorage->calendar()->close(); mCalendarStorage->calendar().clear(); mCalendarStorage.clear(); } // Flag as closed now to prevent removeKAEvents() doing silly things // when it's called again mOpen = false; // Resource map should be empty, but just in case... while (!mResourceMap.isEmpty()) removeKAEvents(mResourceMap.begin().key(), CalEvent::ACTIVE | CalEvent::ARCHIVED | CalEvent::TEMPLATE | CalEvent::DISPLAYING); } /****************************************************************************** * Create a KAEvent instance corresponding to each KCalendarCore::Event in the * display calendar, and store them in the event map in place of the old set. * Called after the display calendar has completed loading. */ void DisplayCalendar::updateKAEvents() { qCDebug(KALARM_LOG) << "DisplayCalendar::updateKAEvents"; const ResourceId key = DISPLAY_RES_ID; KAEvent::List& events = mResourceMap[key]; for (KAEvent* event : events) { mEventMap.remove(EventId(key, event->id())); delete event; } events.clear(); Calendar::Ptr cal = mCalendarStorage->calendar(); if (!cal) return; const Event::List kcalevents = cal->rawEvents(); for (Event::Ptr kcalevent : kcalevents) { if (kcalevent->alarms().isEmpty()) continue; // ignore events without alarms KAEvent* event = new KAEvent(kcalevent); if (!event->isValid()) { qCWarning(KALARM_LOG) << "DisplayCalendar::updateKAEvents: Ignoring unusable event" << kcalevent->uid(); delete event; continue; // ignore events without usable alarms } event->setResourceId(key); events += event; mEventMap[EventId(key, kcalevent->uid())] = event; } } /****************************************************************************** * Delete a calendar and all its KAEvent instances of specified alarm types from * the lists. * Called after the calendar is deleted or alarm types have been disabled, or * the AlarmCalendar is closed. */ bool AlarmCalendar::removeKAEvents(ResourceId key, CalEvent::Types types) { bool removed = false; ResourceMap::Iterator rit = mResourceMap.find(key); if (rit != mResourceMap.end()) { KAEvent::List retained; KAEvent::List& events = rit.value(); for (int i = 0, end = events.count(); i < end; ++i) { KAEvent* event = events[i]; bool remove = (event->resourceId() != key); if (remove) { if (key != DISPLAY_RES_ID) qCCritical(KALARM_LOG) << "AlarmCalendar::removeKAEvents: Event" << event->id() << ", resource" << event->resourceId() << "Indexed under resource" << key; } else remove = event->category() & types; if (remove) { mEventMap.remove(EventId(key, event->id())); delete event; removed = true; } else retained.push_back(event); } if (retained.empty()) mResourceMap.erase(rit); else events.swap(retained); } return removed; } bool ResourcesCalendar::removeKAEvents(ResourceId key, bool closing, CalEvent::Types types) { if (!AlarmCalendar::removeKAEvents(key, types)) return false; mEarliestAlarm.remove(key); // Emit signal only if we're not in the process of closing the calendar if (!closing) { Q_EMIT earliestAlarmChanged(); if (mHaveDisabledAlarms) checkForDisabledAlarms(); } return true; } /****************************************************************************** * Called when the enabled or read-only status of a resource has changed. * If the resource is now disabled, remove its events from the calendar. */ void ResourcesCalendar::slotResourceSettingsChanged(Resource& resource, ResourceType::Changes change) { if (change & ResourceType::Enabled) { if (resource.isValid()) { // For each alarm type which has been disabled, remove the // resource's events from the map, but not from the resource. const CalEvent::Types enabled = resource.enabledTypes(); const CalEvent::Types disabled = ~enabled & (CalEvent::ACTIVE | CalEvent::ARCHIVED | CalEvent::TEMPLATE); removeKAEvents(resource.id(), false, disabled); // For each alarm type which has been enabled, add the resource's // events to the map. if (enabled != CalEvent::EMPTY) slotEventsAdded(resource, resource.events()); } } } /****************************************************************************** * Called when all resources have been populated for the first time. */ void ResourcesCalendar::slotResourcesPopulated() { // Now that all calendars have been processed, all repeat-at-login alarms // will have been triggered. Prevent any new or updated repeat-at-login // alarms (e.g. when they are edited by the user) triggering from now on. mIgnoreAtLogin = true; } /****************************************************************************** * Called when a resource has been added. * Add its KAEvent instances to those held by AlarmCalendar. * All events must have their resource ID set. */ void ResourcesCalendar::slotResourceAdded(Resource& resource) { slotEventsAdded(resource, resource.events()); } /****************************************************************************** * Called when events have been added to a resource. * Add corresponding KAEvent instances to those held by AlarmCalendar. * All events must have their resource ID set. */ void ResourcesCalendar::slotEventsAdded(Resource& resource, const QList& events) { for (const KAEvent& event : events) slotEventUpdated(resource, event); } /****************************************************************************** * Called when an event has been changed in a resource. * Change the corresponding KAEvent instance held by AlarmCalendar. * The event must have its resource ID set. */ void ResourcesCalendar::slotEventUpdated(Resource& resource, const KAEvent& event) { bool added = true; bool updated = false; KAEventMap::Iterator it = mEventMap.find(EventId(event)); if (it != mEventMap.end()) { // The event ID already exists - remove the existing event first KAEvent* storedEvent = it.value(); if (event.category() == storedEvent->category()) { // The existing event is the same type - update it in place *storedEvent = event; addNewEvent(resource, storedEvent, true); updated = true; } else delete storedEvent; added = false; } if (!updated) addNewEvent(resource, new KAEvent(event)); if (event.category() == CalEvent::ACTIVE) { bool enabled = event.enabled(); checkForDisabledAlarms(!enabled, enabled); if (!mIgnoreAtLogin && added && enabled && event.repeatAtLogin()) Q_EMIT atLoginEventAdded(event); } } /****************************************************************************** * Called when events are about to be removed from a resource. * Remove the corresponding KAEvent instances held by AlarmCalendar. */ void ResourcesCalendar::slotEventsToBeRemoved(Resource& resource, const QList& events) { for (const KAEvent& event : events) { if (mEventMap.contains(EventId(event))) deleteEventInternal(event, resource, false); } } /****************************************************************************** * This method must only be called from the main KAlarm queue processing loop, * to prevent asynchronous calendar operations interfering with one another. * * Purge a list of archived events from the calendar. */ void ResourcesCalendar::purgeEvents(const KAEvent::List& events) { for (const KAEvent* event : events) { deleteEventInternal(*event); } if (mHaveDisabledAlarms) checkForDisabledAlarms(); } /****************************************************************************** * Add the specified event to the calendar. * If it is an active event and 'useEventID' is false, a new event ID is * created. In all other cases, the event ID is taken from 'evnt' (if non-null). * 'evnt' is updated with the actual event ID. * The event is added to 'resource' if specified; otherwise the default resource * is used or the user is prompted, depending on policy. If 'noPrompt' is true, * the user will not be prompted so that if no default resource is defined, the * function will fail. * Reply = true if 'evnt' was written to the calendar. 'evnt' is updated. * = false if an error occurred, in which case 'evnt' is unchanged. */ bool ResourcesCalendar::addEvent(KAEvent& evnt, QWidget* promptParent, bool useEventID, Resource* resourceptr, bool noPrompt, bool* cancelled) { if (cancelled) *cancelled = false; Resource nullresource; Resource& resource(resourceptr ? *resourceptr : nullresource); qCDebug(KALARM_LOG) << "ResourcesCalendar::addEvent:" << evnt.id() << ", resource" << resource.displayId(); // Check that the event type is valid for the calendar const CalEvent::Type type = evnt.category(); switch (type) { case CalEvent::ACTIVE: case CalEvent::ARCHIVED: case CalEvent::TEMPLATE: break; default: return false; } const ResourceId key = resource.id(); KAEvent* event = new KAEvent(evnt); QString id = event->id(); if (type == CalEvent::ACTIVE) { if (id.isEmpty()) useEventID = false; else if (!useEventID) id.clear(); } else useEventID = true; if (id.isEmpty()) id = CalFormat::createUniqueId(); if (useEventID) id = CalEvent::uid(id, type); // include the alarm type tag in the ID event->setEventId(id); bool ok = false; bool remove = false; Resource res; if (resource.isEnabled(type)) res = resource; else { res = Resources::destination(type, promptParent, noPrompt, cancelled); if (!res.isValid()) { const char* typeStr = (type == CalEvent::ACTIVE) ? "Active alarm" : (type == CalEvent::ARCHIVED) ? "Archived alarm" : "alarm Template"; qCWarning(KALARM_LOG) << "ResourcesCalendar::addEvent: Error! Cannot create" << typeStr << "(No default calendar is defined)"; } } if (res.isValid()) { - // Don't add event to mEventMap yet - its Akonadi item id is not yet known. - // It will be added once it is inserted into AkonadiDataModel. + // Don't add event to mEventMap yet - its ID is not yet known. + // It will be added after it is inserted into the data model, when + // the resource signals eventsAdded(). ok = res.addEvent(*event); remove = ok; // if success, delete the local event instance on exit if (ok && type == CalEvent::ACTIVE && !event->enabled()) checkForDisabledAlarms(true, false); } if (!ok) { if (remove) { // Adding to mCalendar failed, so undo AlarmCalendar::addEvent() mEventMap.remove(EventId(key, event->id())); KAEvent::List& events = mResourceMap[key]; int i = events.indexOf(event); if (i >= 0) events.remove(i); if (mEarliestAlarm[key] == event) findEarliestAlarm(key); } delete event; return false; } evnt = *event; if (remove) delete event; return true; } /****************************************************************************** * Add the specified event to the calendar. * Reply = true if 'evnt' was written to the calendar. 'evnt' is updated. * = false if an error occurred, in which case 'evnt' is unchanged. */ bool DisplayCalendar::addEvent(KAEvent& evnt) { if (!mOpen) return false; qCDebug(KALARM_LOG) << "DisplayCalendar::addEvent:" << evnt.id(); // Check that the event type is valid for the calendar const CalEvent::Type type = evnt.category(); if (type != CalEvent::DISPLAYING) return false; Event::Ptr kcalEvent(new Event); KAEvent* event = new KAEvent(evnt); QString id = event->id(); if (id.isEmpty()) id = kcalEvent->uid(); id = CalEvent::uid(id, type); // include the alarm type tag in the ID kcalEvent->setUid(id); event->setEventId(id); event->updateKCalEvent(kcalEvent, KAEvent::UID_IGNORE); bool ok = false; bool remove = false; const ResourceId key = DISPLAY_RES_ID; if (!mEventMap.contains(EventId(key, event->id()))) { addNewEvent(Resource(), event); ok = mCalendarStorage->calendar()->addEvent(kcalEvent); remove = !ok; } if (!ok) { if (remove) { // Adding to mCalendar failed, so undo AlarmCalendar::addEvent() mEventMap.remove(EventId(key, event->id())); KAEvent::List& events = mResourceMap[key]; int i = events.indexOf(event); if (i >= 0) events.remove(i); } delete event; return false; } evnt = *event; if (remove) delete event; return true; } /****************************************************************************** * Internal method to add an already checked event to the calendar. * mEventMap takes ownership of the KAEvent. * If 'replace' is true, an existing event is being updated (NOTE: its category() * must remain the same). */ void AlarmCalendar::addNewEvent(const Resource& resource, KAEvent* event, bool replace) { const ResourceId key = resource.id(); event->setResourceId(key); if (!replace) { mResourceMap[key] += event; mEventMap[EventId(key, event->id())] = event; } } /****************************************************************************** * Internal method to add an already checked event to the calendar. * mEventMap takes ownership of the KAEvent. * If 'replace' is true, an existing event is being updated (NOTE: its category() * must remain the same). */ void ResourcesCalendar::addNewEvent(const Resource& resource, KAEvent* event, bool replace) { AlarmCalendar::addNewEvent(resource, event, replace); if ((resource.alarmTypes() & CalEvent::ACTIVE) && event->category() == CalEvent::ACTIVE) { // Update the earliest alarm to trigger const ResourceId key = resource.id(); const KAEvent* earliest = mEarliestAlarm.value(key, (KAEvent*)nullptr); if (replace && earliest == event) findEarliestAlarm(key); else { const KADateTime dt = event->nextTrigger(KAEvent::ALL_TRIGGER).effectiveKDateTime(); if (dt.isValid() && (!earliest || dt < earliest->nextTrigger(KAEvent::ALL_TRIGGER))) { mEarliestAlarm[key] = event; Q_EMIT earliestAlarmChanged(); } } } } /****************************************************************************** * Modify the specified event in the calendar with its new contents. * The new event must have a different event ID from the old one. * It is assumed to be of the same event type as the old one (active, etc.) * Reply = true if 'newEvent' was written to the calendar. 'newEvent' is updated. * = false if an error occurred, in which case 'newEvent' is unchanged. */ bool ResourcesCalendar::modifyEvent(const EventId& oldEventId, KAEvent& newEvent) { const EventId newId(oldEventId.resourceId(), newEvent.id()); qCDebug(KALARM_LOG) << "ResourcesCalendar::modifyEvent:" << oldEventId << "->" << newId; bool noNewId = newId.isEmpty(); if (!noNewId && oldEventId == newId) { qCCritical(KALARM_LOG) << "ResourcesCalendar::modifyEvent: Same IDs"; return false; } // Set the event's ID, and update the old event in the resources calendar. const KAEvent* storedEvent = event(oldEventId); if (!storedEvent) { qCCritical(KALARM_LOG) << "ResourcesCalendar::modifyEvent: Old event not found"; return false; } if (noNewId) newEvent.setEventId(CalFormat::createUniqueId()); Resource resource = Resources::resource(oldEventId.resourceId()); if (!resource.isValid()) return false; - // Don't add new event to mEventMap yet - its Akonadi item id is not yet known + // Don't add new event to mEventMap yet - it will be added when the resource + // signals eventsAdded(). if (!resource.addEvent(newEvent)) return false; // Note: deleteEventInternal() will delete storedEvent before using the // event parameter, so need to pass a copy as the parameter. deleteEventInternal(KAEvent(*storedEvent), resource); if (mHaveDisabledAlarms) checkForDisabledAlarms(); return true; } /****************************************************************************** * Update the specified event in the calendar with its new contents. * The event retains the same ID. The event must be in the resource calendar. * Reply = event which has been updated * = 0 if error. */ KAEvent* ResourcesCalendar::updateEvent(const KAEvent& evnt) { KAEvent* kaevnt = event(EventId(evnt)); if (kaevnt) { Resource resource = Resources::resourceForEvent(evnt.id()); if (resource.updateEvent(evnt)) { *kaevnt = evnt; return kaevnt; } } qCDebug(KALARM_LOG) << "ResourcesCalendar::updateEvent: error"; return nullptr; } /****************************************************************************** * Delete the specified event from the resource calendar, if it exists. * The calendar is then optionally saved. */ bool ResourcesCalendar::deleteEvent(const KAEvent& event, bool saveit) { Q_UNUSED(saveit); const CalEvent::Type status = deleteEventInternal(event); if (mHaveDisabledAlarms) checkForDisabledAlarms(); return status != CalEvent::EMPTY; } /****************************************************************************** * Delete the specified event from the calendar, if it exists. * The calendar is then optionally saved. */ bool DisplayCalendar::deleteEvent(const QString& eventID, bool saveit) { if (mOpen) { Event::Ptr kcalEvent; if (mCalendarStorage) kcalEvent = mCalendarStorage->calendar()->event(eventID); // display calendar Resource resource; deleteEventBase(eventID, resource); CalEvent::Type status = CalEvent::EMPTY; if (kcalEvent) { status = CalEvent::status(kcalEvent); mCalendarStorage->calendar()->deleteEvent(kcalEvent); } if (status != CalEvent::EMPTY) { if (saveit) return save(); return true; } } return false; } /****************************************************************************** * Internal method to delete the specified event from the calendar and lists. * Reply = event status, if it was found in the resource calendar/calendar * resource or local calendar * = CalEvent::EMPTY otherwise. */ CalEvent::Type ResourcesCalendar::deleteEventInternal(const KAEvent& event, bool deleteFromResources) { Resource resource = Resources::resource(event.resourceId()); if (!resource.isValid()) return CalEvent::EMPTY; return deleteEventInternal(event.id(), event, resource, deleteFromResources); } CalEvent::Type ResourcesCalendar::deleteEventInternal(const KAEvent& event, Resource& resource, bool deleteFromResources) { if (!resource.isValid()) return CalEvent::EMPTY; if (event.resourceId() != resource.id()) { qCCritical(KALARM_LOG) << "ResourcesCalendar::deleteEventInternal: Event" << event.id() << ": resource" << event.resourceId() << "differs from 'resource'" << resource.id(); return CalEvent::EMPTY; } return deleteEventInternal(event.id(), event, resource, deleteFromResources); } CalEvent::Type ResourcesCalendar::deleteEventInternal(const QString& eventID, const KAEvent& event, Resource& resource, bool deleteFromResources) { // Make a copy of the KAEvent and the ID QString, since the supplied // references might be destructed when the event is deleted below. const QString id = eventID; const KAEvent paramEvent = event; const ResourceId key = resource.id(); KAEvent* ev = deleteEventBase(eventID, resource); if (ev) { if (mEarliestAlarm[key] == ev) findEarliestAlarm(resource); } else { for (EarliestMap::Iterator eit = mEarliestAlarm.begin(); eit != mEarliestAlarm.end(); ++eit) { ev = eit.value(); if (ev && ev->id() == id) { findEarliestAlarm(eit.key()); break; } } } CalEvent::Type status = CalEvent::EMPTY; if (deleteFromResources) { // Delete from the resources calendar CalEvent::Type s = paramEvent.category(); if (resource.deleteEvent(paramEvent)) status = s; } return status; } /****************************************************************************** * Internal method to delete the specified event from the calendar and lists. * Reply = event which was deleted, or null if not found. */ KAEvent* AlarmCalendar::deleteEventBase(const QString& eventID, Resource& resource) { // Make a copy of the ID QString, since the supplied reference might be // destructed when the event is deleted below. const QString id = eventID; const ResourceId key = resource.id(); KAEventMap::Iterator it = mEventMap.find(EventId(key, id)); if (it == mEventMap.end()) return nullptr; KAEvent* ev = it.value(); mEventMap.erase(it); KAEvent::List& events = mResourceMap[key]; int i = events.indexOf(ev); if (i >= 0) events.remove(i); delete ev; return ev; } /****************************************************************************** * Return the event with the specified ID. * If 'findUniqueId' is true, and the resource ID is invalid, if there is a * unique event with the given ID, it will be returned. */ KAEvent* AlarmCalendar::event(const EventId& uniqueID) { if (!isValid()) return nullptr; KAEventMap::ConstIterator it = mEventMap.constFind(uniqueID); if (it == mEventMap.constEnd()) return nullptr; return it.value(); } /****************************************************************************** * Return the event with the specified ID. * If 'findUniqueId' is true, and the resource ID is invalid, if there is a * unique event with the given ID, it will be returned. */ KAEvent* ResourcesCalendar::event(const EventId& uniqueID, bool findUniqueId) { if (!isValid()) return nullptr; if (uniqueID.resourceId() == -1 && findUniqueId) { // The resource isn't known, but use the event ID if it is unique among // all resources. const QString eventId = uniqueID.eventId(); const KAEvent::List list = events(eventId); if (list.count() > 1) { qCWarning(KALARM_LOG) << "ResourcesCalendar::event: Multiple events found with ID" << eventId; return nullptr; } if (list.isEmpty()) return nullptr; return list[0]; } KAEventMap::ConstIterator it = mEventMap.constFind(uniqueID); if (it == mEventMap.constEnd()) return nullptr; return it.value(); } /****************************************************************************** * Find the alarm template with the specified name. * Reply = 0 if not found. */ KAEvent* ResourcesCalendar::templateEvent(const QString& templateName) { if (templateName.isEmpty()) return nullptr; const KAEvent::List eventlist = events(CalEvent::TEMPLATE); for (KAEvent* event : eventlist) { if (event->templateName() == templateName) return event; } return nullptr; } /****************************************************************************** * Return all events with the specified ID, from all calendars. */ KAEvent::List ResourcesCalendar::events(const QString& uniqueId) const { KAEvent::List list; if (isValid()) { for (ResourceMap::ConstIterator rit = mResourceMap.constBegin(); rit != mResourceMap.constEnd(); ++rit) { const ResourceId id = rit.key(); KAEventMap::ConstIterator it = mEventMap.constFind(EventId(id, uniqueId)); if (it != mEventMap.constEnd()) list += it.value(); } } return list; } KAEvent::List ResourcesCalendar::events(const Resource& resource, CalEvent::Types type) const { return AlarmCalendar::events(type, resource); } KAEvent::List ResourcesCalendar::events(CalEvent::Types type) const { Resource resource; return AlarmCalendar::events(type, resource); } /****************************************************************************** * Return all events in the calendar which contain alarms. * Optionally the event type can be filtered, using an OR of event types. */ KAEvent::List DisplayCalendar::events(CalEvent::Types type) const { if (!mCalendarStorage) return KAEvent::List(); Resource resource; return AlarmCalendar::events(type, resource); } KAEvent::List AlarmCalendar::events(CalEvent::Types type, const Resource& resource) const { KAEvent::List list; if (resource.isValid()) { const ResourceId key = resource.id(); ResourceMap::ConstIterator rit = mResourceMap.constFind(key); if (rit == mResourceMap.constEnd()) return list; const KAEvent::List events = rit.value(); if (type == CalEvent::EMPTY) return events; for (KAEvent* const event : events) if (type & event->category()) list += event; } else { for (ResourceMap::ConstIterator rit = mResourceMap.constBegin(); rit != mResourceMap.constEnd(); ++rit) { const KAEvent::List events = rit.value(); if (type == CalEvent::EMPTY) list += events; else { for (KAEvent* const event : events) if (type & event->category()) list += event; } } } return list; } /****************************************************************************** * Return the event with the specified ID. * This method is for the display calendar only. */ Event::Ptr DisplayCalendar::kcalEvent(const QString& uniqueID) { if (!mCalendarStorage) return Event::Ptr(); return mCalendarStorage->calendar()->event(uniqueID); } /****************************************************************************** * Return all events in the calendar which contain usable alarms. * This method is for the display calendar only. * Optionally the event type can be filtered, using an OR of event types. */ Event::List DisplayCalendar::kcalEvents(CalEvent::Type type) { Event::List list; if (!mCalendarStorage) return list; list = mCalendarStorage->calendar()->rawEvents(); for (int i = 0; i < list.count(); ) { Event::Ptr event = list.at(i); if (event->alarms().isEmpty() || (type != CalEvent::EMPTY && !(type & CalEvent::status(event))) || !KAEvent(event).isValid()) list.remove(i); else ++i; } return list; } /****************************************************************************** * Return whether an event is read-only. * Display calendar events are always returned as read-only. */ bool ResourcesCalendar::eventReadOnly(const QString& eventId) const { KAEvent event; const Resource resource = Resources::resourceForEvent(eventId, event); return !event.isValid() || event.isReadOnly() || !resource.isWritable(event.category()); //TODO || compatibility(event) != KACalendar::Current; } /****************************************************************************** * Called when an alarm's enabled status has changed. */ void ResourcesCalendar::disabledChanged(const KAEvent* event) { if (event->category() == CalEvent::ACTIVE) { bool status = event->enabled(); checkForDisabledAlarms(!status, status); } } /****************************************************************************** * Check whether there are any individual disabled alarms, following an alarm * creation or modification. Must only be called for an ACTIVE alarm. */ void ResourcesCalendar::checkForDisabledAlarms(bool oldEnabled, bool newEnabled) { if (newEnabled != oldEnabled) { if (newEnabled && mHaveDisabledAlarms) checkForDisabledAlarms(); else if (!newEnabled && !mHaveDisabledAlarms) { mHaveDisabledAlarms = true; Q_EMIT haveDisabledAlarmsChanged(true); } } } /****************************************************************************** * Check whether there are any individual disabled alarms. */ void ResourcesCalendar::checkForDisabledAlarms() { bool disabled = false; const KAEvent::List eventlist = events(CalEvent::ACTIVE); for (const KAEvent* const event : eventlist) { if (!event->enabled()) { disabled = true; break; } } if (disabled != mHaveDisabledAlarms) { mHaveDisabledAlarms = disabled; Q_EMIT haveDisabledAlarmsChanged(disabled); } } /****************************************************************************** * Find and note the active alarm with the earliest trigger time for a calendar. */ void ResourcesCalendar::findEarliestAlarm(const Resource& resource) { if (!(resource.alarmTypes() & CalEvent::ACTIVE)) return; findEarliestAlarm(resource.id()); } void ResourcesCalendar::findEarliestAlarm(ResourceId key) { EarliestMap::Iterator eit = mEarliestAlarm.find(key); if (eit != mEarliestAlarm.end()) eit.value() = nullptr; if (key < 0) return; ResourceMap::ConstIterator rit = mResourceMap.constFind(key); if (rit == mResourceMap.constEnd()) return; const KAEvent::List& events = rit.value(); KAEvent* earliest = nullptr; KADateTime earliestTime; for (KAEvent* event : events) { if (event->category() != CalEvent::ACTIVE || mPendingAlarms.contains(event->id())) continue; const KADateTime dt = event->nextTrigger(KAEvent::ALL_TRIGGER).effectiveKDateTime(); if (dt.isValid() && (!earliest || dt < earliestTime)) { earliestTime = dt; earliest = event; } } mEarliestAlarm[key] = earliest; Q_EMIT earliestAlarmChanged(); } /****************************************************************************** * Return the active alarm with the earliest trigger time. * Reply = 0 if none. */ KAEvent* ResourcesCalendar::earliestAlarm() const { KAEvent* earliest = nullptr; KADateTime earliestTime; for (EarliestMap::ConstIterator eit = mEarliestAlarm.constBegin(); eit != mEarliestAlarm.constEnd(); ++eit) { KAEvent* event = eit.value(); if (!event) continue; const KADateTime dt = event->nextTrigger(KAEvent::ALL_TRIGGER).effectiveKDateTime(); if (dt.isValid() && (!earliest || dt < earliestTime)) { earliestTime = dt; earliest = event; } } return earliest; } /****************************************************************************** * Note that an alarm which has triggered is now being processed. While pending, * it will be ignored for the purposes of finding the earliest trigger time. */ void ResourcesCalendar::setAlarmPending(KAEvent* event, bool pending) { const QString id = event->id(); bool wasPending = mPendingAlarms.contains(id); qCDebug(KALARM_LOG) << "ResourcesCalendar::setAlarmPending:" << id << "," << pending << "(was" << wasPending << ")"; if (pending) { if (wasPending) return; mPendingAlarms += id; } else { if (!wasPending) return; mPendingAlarms.remove(id); } // Now update the earliest alarm to trigger for its calendar findEarliestAlarm(Resources::resourceForEvent(event->id())); } /****************************************************************************** * Called when the user changes the start-of-day time. * Adjust the start times of all date-only alarms' recurrences. */ void AlarmCalendar::adjustStartOfDay() { if (!isValid()) return; for (ResourceMap::ConstIterator rit = mResourceMap.constBegin(); rit != mResourceMap.constEnd(); ++rit) KAEvent::adjustStartOfDay(rit.value()); } // vim: et sw=4: diff --git a/src/functions.cpp b/src/functions.cpp index c62aae2e..e87d9141 100644 --- a/src/functions.cpp +++ b/src/functions.cpp @@ -1,1807 +1,1808 @@ /* * functions.cpp - miscellaneous functions * Program: kalarm * Copyright © 2001-2020 David Jarvie * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "functions.h" #include "functions_p.h" #include "akonadicollectionsearch.h" #include "alarmcalendar.h" #include "alarmtime.h" #include "editdlg.h" #include "kalarmapp.h" #include "kamail.h" #include "mainwindow.h" #include "messagewin.h" #include "preferences.h" #include "templatelistview.h" #include "templatemenuaction.h" #include "resources/calendarfunctions.h" #include "resources/datamodel.h" #include "resources/resources.h" #include "resources/eventmodel.h" #include "lib/autoqpointer.h" #include "lib/filedialog.h" #include "lib/messagebox.h" #include "lib/shellprocess.h" #include "config-kalarm.h" #include "kalarm_debug.h" #include #include #include #include #include #include #include #include using namespace KCalendarCore; #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace { bool refreshAlarmsQueued = false; QUrl lastImportUrl; // last URL for Import Alarms file dialogue +QUrl lastExportUrl; // last URL for Export Alarms file dialogue 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) { 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(const QString& 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 #pragma message("Don't hide unless necessary, since it moves the 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) 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, Resource* resource, QWidget* msgParent, int options, bool showKOrgErr) { qCDebug(KALARM_LOG) << "KAlarm::addEvent:" << 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 ResourcesCalendar* cal = ResourcesCalendar::instance(); // Note that AlarmCalendar::addEvent() updates 'event'. if (!cal->addEvent(event, msgParent, (options & USE_EVENT_ID), resource, (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) << "KAlarm::addEvents:" << events.count(); if (events.isEmpty()) return UpdateResult(UPDATE_OK); UpdateStatusData status; if (!theApp()->checkCalendar()) // ensure calendar is open status.status = UPDATE_FAILED; else { Resource resource = Resources::destination(CalEvent::ACTIVE, msgParent); if (!resource.isValid()) { qCDebug(KALARM_LOG) << "KAlarm::addEvents: No calendar"; status.status = UPDATE_FAILED; } else { ResourcesCalendar* cal = ResourcesCalendar::instance(); 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]; if (!cal->addEvent(event, msgParent, false, &resource)) { status.setError(UPDATE_ERROR); continue; } if (allowKOrgUpdate && event.copyToKOrganizer()) { UpdateResult st = sendToKOrganizer(event); // 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, Resource* resourceptr) { qCDebug(KALARM_LOG) << "KAlarm::addArchivedEvent:" << event.id(); bool archiving = (event.category() == CalEvent::ACTIVE); if (archiving && !Preferences::archivedKeepDays()) return false; // expired alarms aren't being kept ResourcesCalendar* cal = ResourcesCalendar::instance(); KAEvent newevent(event); 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, resourceptr)) 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, Resource* resourceptr, QWidget* msgParent) { qCDebug(KALARM_LOG) << "KAlarm::addTemplate:" << event.id(); UpdateStatusData status; // Add the template to the calendar file ResourcesCalendar* cal = ResourcesCalendar::instance(); KAEvent newev(event); if (!cal->addEvent(newev, msgParent, false, resourceptr)) 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) << "KAlarm::modifyEvent:" << 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 ResourcesCalendar* cal = ResourcesCalendar::instance(); 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) << "KAlarm::updateEvent:" << event.id(); if (!event.isValid()) deleteEvent(event, archiveOnDelete); else { // Update the event in the calendar file. ResourcesCalendar* cal = ResourcesCalendar::instance(); 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) { ResourcesCalendar* cal = ResourcesCalendar::instance(); const 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(status)); 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) << "KAlarm::deleteEvents:" << events.count(); if (events.isEmpty()) return UpdateResult(UPDATE_OK); UpdateStatusData status; ResourcesCalendar* cal = ResourcesCalendar::instance(); bool deleteWakeFromSuspendAlarm = false; const 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]; const 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) << "KAlarm::deleteTemplates:" << count; if (!count) return UpdateResult(UPDATE_OK); UpdateStatusData status; ResourcesCalendar* cal = ResourcesCalendar::instance(); for (const KAEvent* event : events) { // Update the window lists // Delete the template from the calendar file if (!cal->deleteEvent(*event, 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) << "KAlarm::deleteDisplayEvent:" << eventID; DisplayCalendar* cal = DisplayCalendar::instanceOpen(); if (cal) cal->deleteEvent(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. * Parameters: * calendar - the active alarms calendar to restore the alarms into, or null * to use the default way of determining the active alarm calendar. * ineligibleIDs - will be filled in with the IDs of any ineligible events. */ UpdateResult reactivateEvent(KAEvent& event, Resource* resourceptr, QWidget* msgParent, bool showKOrgErr) { QVector ids; QVector events(1, event); return reactivateEvents(events, ids, resourceptr, msgParent, showKOrgErr); } UpdateResult reactivateEvents(QVector& events, QVector& ineligibleIDs, Resource* resourceptr, QWidget* msgParent, bool showKOrgErr) { qCDebug(KALARM_LOG) << "KAlarm::reactivateEvents:" << events.count(); ineligibleIDs.clear(); if (events.isEmpty()) return UpdateResult(UPDATE_OK); UpdateStatusData status; Resource resource; if (resourceptr) resource = *resourceptr; if (!resource.isValid()) resource = Resources::destination(CalEvent::ACTIVE, msgParent); if (!resource.isValid()) { qCDebug(KALARM_LOG) << "KAlarm::reactivateEvents: No calendar"; status.setError(UPDATE_FAILED, events.count()); } else { int count = 0; ResourcesCalendar* cal = ResourcesCalendar::instance(); 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, &resource)) { 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) << "KAlarm::enableEvents:" << events.count(); if (events.isEmpty()) return UpdateResult(UPDATE_OK); UpdateStatusData status; ResourcesCalendar* cal = ResourcesCalendar::instance(); bool deleteWakeFromSuspendAlarm = false; const 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 const KAEvent* newev = cal->updateEvent(*event); if (!newev) qCCritical(KALARM_LOG) << "KAlarm::enableEvents: 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) << "KAlarm::purgeArchive:" << purgeDays; const QDate cutoff = KADateTime::currentLocalDate().addDays(-purgeDays); const Resource resource = Resources::getStandard(CalEvent::ARCHIVED); if (!resource.isValid()) return; KAEvent::List events = ResourcesCalendar::instance()->events(resource); for (int i = 0; i < events.count(); ) { if (purgeDays && events.at(i)->createdDateTime().date() >= cutoff) events.remove(i); else ++i; } if (!events.isEmpty()) ResourcesCalendar::instance()->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 = DataModel::createAlarmListModel(parent); (*model)->setEventTypeFilter(CalEvent::ACTIVE); (*model)->sort(AlarmListModel::TimeColumn); } QVector result; for (int i = 0, count = (*model)->rowCount(); i < count; ++i) { const KAEvent event = (*model)->event(i); if (event.enabled() && !event.expired()) result += event; } return result; } /****************************************************************************** * Import alarms from an external calendar and merge them into KAlarm's calendar. * The alarms are given new unique event IDs. * Parameters: parent = parent widget for error message boxes * Reply = true if all alarms in the calendar were successfully imported * = false if any alarms failed to be imported. */ bool importAlarms(Resource& resource, QWidget* parent) { qCDebug(KALARM_LOG) << "KAlarm::importAlarms" << resource.displayId(); const QList urls = QFileDialog::getOpenFileUrls( parent, i18nc("@title:window", "Import Calendar Files"), lastImportUrl, QStringLiteral("%1 (*.ics *.vcs)").arg(i18nc("@info", "Calendar Files"))); if (urls.isEmpty()) return false; lastImportUrl = urls[0].adjusted(QUrl::RemoveFilename); const CalEvent::Types alarmTypes = resource.isValid() ? resource.alarmTypes() : CalEvent::ACTIVE | CalEvent::ARCHIVED | CalEvent::TEMPLATE; // Read all the selected calendar files and extract their alarms. QHash> events; for (const QUrl& url : urls) { if (!url.isValid()) { qCDebug(KALARM_LOG) << "KAlarm::importAlarms: Invalid URL"; continue; } qCDebug(KALARM_LOG) << "KAlarm::importAlarms:" << url.toDisplayString(); importCalendarFile(url, alarmTypes, true, parent, events); } if (events.isEmpty()) return false; // Add the alarms to the destination resource. bool success = true; for (auto it = events.constBegin(); it != events.constEnd(); ++it) { Resource res; if (resource.isValid()) res = resource; else res = Resources::destination(it.key()); for (const KAEvent& event : qAsConst(it.value())) { if (!res.addEvent(event)) success = false; } } return success; } /****************************************************************************** * Export all selected alarms to an external calendar. * The alarms are given new unique event IDs. * Parameters: parent = parent widget for error message boxes * Reply = true if all alarms in the calendar were successfully exported * = false if any alarms failed to be exported. */ bool exportAlarms(const KAEvent::List& events, QWidget* parent) { bool append; -//TODO: exportalarms shows up afterwards in other file dialogues - QString file = FileDialog::getSaveFileName(QUrl(QStringLiteral("kfiledialog:///exportalarms")), + QString file = FileDialog::getSaveFileName(lastExportUrl, QStringLiteral("*.ics|%1").arg(i18nc("@info", "Calendar Files")), parent, i18nc("@title:window", "Choose Export Calendar"), &append); if (file.isEmpty()) return false; const QUrl url = QUrl::fromLocalFile(file); if (!url.isValid()) { qCDebug(KALARM_LOG) << "KAlarm::exportAlarms: Invalid URL" << url; return false; } + lastExportUrl = url.adjusted(QUrl::RemoveFilename); qCDebug(KALARM_LOG) << "KAlarm::exportAlarms:" << url.toDisplayString(); MemoryCalendar::Ptr calendar(new MemoryCalendar(Preferences::timeSpecAsZone())); FileStorage::Ptr calStorage(new FileStorage(calendar, file)); if (append && !calStorage->load()) { auto statJob = KIO::statDetails(url, KIO::StatJob::SourceSide, KIO::StatDetail::StatDefaultDetails); KJobWidgets::setWindow(statJob, parent); statJob->exec(); KFileItem fi(statJob->statResult(), url); if (fi.size()) { qCCritical(KALARM_LOG) << "KAlarm::exportAlarms: Error loading calendar file" << file << "for append"; KAMessageBox::error(MainWindow::mainMainWindow(), xi18nc("@info", "Error loading calendar to append to:%1", url.toDisplayString())); return false; } } KACalendar::setKAlarmVersion(calendar); // Add the alarms to the calendar bool success = true; bool exported = false; for (int i = 0, end = events.count(); i < end; ++i) { const KAEvent* event = events[i]; Event::Ptr kcalEvent(new Event); const CalEvent::Type type = event->category(); const QString id = CalEvent::uid(kcalEvent->uid(), type); kcalEvent->setUid(id); event->updateKCalEvent(kcalEvent, KAEvent::UID_IGNORE); if (calendar->addEvent(kcalEvent)) exported = true; else success = false; } if (exported) { // One or more alarms have been exported to the calendar. // Save the calendar to file. QTemporaryFile* tempFile = nullptr; bool local = url.isLocalFile(); if (!local) { tempFile = new QTemporaryFile; file = tempFile->fileName(); } calStorage->setFileName(file); calStorage->setSaveFormat(new ICalFormat); if (!calStorage->save()) { qCCritical(KALARM_LOG) << "KAlarm::exportAlarms:" << file << ": failed"; KAMessageBox::error(MainWindow::mainMainWindow(), xi18nc("@info", "Failed to save new calendar to:%1", url.toDisplayString())); success = false; } else if (!local) { QFile qFile(file); qFile.open(QIODevice::ReadOnly); auto uploadJob = KIO::storedPut(&qFile, url, -1); KJobWidgets::setWindow(uploadJob, parent); if (!uploadJob->exec()) { qCCritical(KALARM_LOG) << "KAlarm::exportAlarms:" << file << ": upload failed"; KAMessageBox::error(MainWindow::mainMainWindow(), xi18nc("@info", "Cannot upload new calendar to:%1", url.toDisplayString())); success = false; } } delete tempFile; } calendar->close(); return success; } /****************************************************************************** * Display an error message corresponding to a specified alarm update error code. */ 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; Resource resource; editDlg->getEvent(event, resource); // Add the alarm to the displayed lists and to the calendar file const UpdateResult status = addEvent(event, &resource, 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, resource); 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 = ResourcesCalendar::instance()->templateEvent(templateName); if (templateEvent->isValid()) { editNewAlarm(templateEvent, parent); return true; } qCWarning(KALARM_LOG) << "KAlarm::editNewAlarm:" << 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); } /****************************************************************************** * 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 resource ID, * [1] = event ID, * [2] = trigger time (int64 seconds since epoch). * = empty list if none or expired. */ QStringList checkRtcWakeConfig(bool checkEventExists) { KConfigGroup config(KSharedConfig::openConfig(), "General"); const QStringList params = config.readEntry("RtcWake", QStringList()); #if KALARMCAL_VERSION >= QT_VERSION_CHECK(5,12,1) if (params.count() == 3 && params[2].toLongLong() > KADateTime::currentUtcDateTime().toSecsSinceEpoch()) #else if (params.count() == 3 && params[2].toUInt() > KADateTime::currentUtcDateTime().toTime_t()) #endif { if (checkEventExists && !ResourcesCalendar::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) << "KAlarm::setRtcWakeTime: 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) << "KAlarm::setRtcWakeTime: 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 (Resources::enabledResources(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; Resource resource; editDlg->getEvent(event, resource); // Add the template to the displayed lists and to the calendar file KAlarm::addTemplate(event, &resource, editDlg); Undo::saveAdd(event, resource); } } } // 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() || ResourcesCalendar::instance()->eventReadOnly(event->id())) { viewAlarm(event, parent); return; } const 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 (!ResourcesCalendar::instance()->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; Resource resource; bool changeDeferral = !editDlg->getEvent(newEvent, resource); // Update the event in the displays and in the calendar file const Undo::Event undo(*event, resource); 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 { const 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 = ResourcesCalendar::instance()->event(id, true); if (!event) { if (id.resourceId() != -1) qCWarning(KALARM_LOG) << "KAlarm::editAlarmById: Event ID not found, or duplicated:" << eventID; else qCWarning(KALARM_LOG) << "KAlarm::editAlarmById: Event ID not found:" << eventID; return false; } if (ResourcesCalendar::instance()->eventReadOnly(event->id())) { qCCritical(KALARM_LOG) << "KAlarm::editAlarmById:" << eventID << ": read-only"; return false; } switch (event->category()) { case CalEvent::ACTIVE: case CalEvent::TEMPLATE: break; default: qCCritical(KALARM_LOG) << "KAlarm::editAlarmById:" << 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 (ResourcesCalendar::instance()->eventReadOnly(event->id())) { // 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; Resource resource; editDlg->getEvent(newEvent, resource); const QString id = event->id(); newEvent.setEventId(id); newEvent.setResourceId(event->resourceId()); // Update the event in the displays and in the calendar file const Undo::Event undo(*event, resource); 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, Resource& resource) { qCDebug(KALARM_LOG) << "KAlarm::updateEditedAlarm"; KAEvent newEvent; Resource res; editDlg->getEvent(newEvent, res); // Update the displayed lists and the calendar file UpdateResult status; if (ResourcesCalendar::instance()->event(EventId(event))) { // The old alarm hasn't expired yet, so replace it const Undo::Event undo(event, resource); status = modifyEvent(event, newEvent, editDlg); Undo::saveEdit(undo, newEvent); } else { // The old event has expired, so simply create a new one status = addEvent(newEvent, &resource, editDlg); Undo::saveAdd(newEvent, resource); } 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; const bool includeCmdAlarms = ShellProcess::authorised(); const KAEvent::List events = ResourcesCalendar::instance()->events(CalEvent::TEMPLATE); for (KAEvent* event : events) { 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) << "KAlarm::refreshAlarms"; 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) << "KAlarm::refreshAlarmsIfQueued"; ResourcesCalendar::instance()->reload(); // Close any message windows for alarms which are now disabled const KAEvent::List events = ResourcesCalendar::instance()->events(CalEvent::ACTIVE); for (KAEvent* event : events) { 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 * = null string if success. */ QString runKMail() { const QDBusReply reply = QDBusConnection::sessionBus().interface()->isServiceRegistered(KMAIL_DBUS_SERVICE); if (!reply.isValid() || !reply.value()) { // Program is not already running, so start it const QDBusReply startReply = QDBusConnection::sessionBus().interface()->startService(KMAIL_DBUS_SERVICE); if (!startReply.isValid()) { const QString errmsg = startReply.error().message(); qCCritical(KALARM_LOG) << "Couldn't start KMail (" << errmsg << ")"; return xi18nc("@info", "Unable to start KMail(%1)", errmsg); } } return QString(); } /****************************************************************************** * 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.resourceId()).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; const 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.resourceId()).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.resourceId()).arg(eventId.eventId()); QStringList tags = group.readEntry(id, QStringList()); if (tags.indexOf(tag) < 0) { tags += tag; group.writeEntry(id, tags); group.sync(); } } #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 KCalendarCore::Event); event.updateKCalEvent(kcalEvent, KAEvent::UID_IGNORE); // Change the event ID to avoid duplicating the same unique ID as the original event const 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: { const 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; } const Person 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()); const 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; QDBusInterface korgInterface(KORG_DBUS_SERVICE, QStringLiteral(KORG_DBUS_PATH), KORG_DBUS_IFACE); const QList 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) << "KAlarm::sendToKOrganizer: addIncidence() D-Bus error: still starting"; } else { status.set(KAlarm::UPDATE_KORG_ERR, reply.error().message()); qCCritical(KALARM_LOG) << "KAlarm::sendToKOrganizer: addIncidence(" << uid << ") D-Bus call failed:" << status.message; } } else if (!reply.value()) { status = KAlarm::UPDATE_KORG_FUNCERR; qCDebug(KALARM_LOG) << "KAlarm::sendToKOrganizer: addIncidence(" << uid << ") D-Bus call returned false"; } else qCDebug(KALARM_LOG) << "KAlarm::sendToKOrganizer:" << 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 AkonadiCollectionSearch(KORG_MIME_TYPE, QString(), 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; // 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. This call starts korganizer if needed, too. QDBusInterface iface(KORG_DBUS_SERVICE, QStringLiteral(KORG_DBUS_LOAD_PATH), QStringLiteral("org.kde.PIMUniqueApplication")); 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; } return status; } /****************************************************************************** * Insert a KOrganizer string after the hyphen in the supplied event ID. */ QString uidKOrganizer(const QString& id) { if (id.startsWith(KORGANIZER_UID)) return id; QString result = id; return result.insert(0, 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: