diff --git a/src/alarmcalendar.cpp b/src/alarmcalendar.cpp index e4817668..c9a55ab1 100644 --- a/src/alarmcalendar.cpp +++ b/src/alarmcalendar.cpp @@ -1,1571 +1,1571 @@ /* * alarmcalendar.cpp - KAlarm calendar file access * Program: kalarm * Copyright © 2001-2019 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/akonadidatamodel.h" +#include "resources/datamodel.h" #include "resources/resources.h" #include "lib/filedialog.h" #include "lib/messagebox.h" #include "kalarm_debug.h" #include #include #include #include #include #include #include #include #include #include #include using namespace KCalendarCore; using namespace KAlarmCal; static KACalendar::Compat fix(const KCalendarCore::FileStorage::Ptr&); static const QString displayCalendarName = QStringLiteral("displaying.ics"); static const ResourceId DISPLAY_COL_ID = -1; // resource ID used for displaying calendar AlarmCalendar* AlarmCalendar::mResourcesCalendar = nullptr; AlarmCalendar* AlarmCalendar::mDisplayCalendar = nullptr; QUrl AlarmCalendar::mLastImportUrl; /****************************************************************************** * Initialise the alarm calendars, and ensure that their file names are different. * There are 2 calendars: * 1) A resources calendar containing the active alarms, archived alarms and * alarm templates; * 2) A user-specific one which contains details of alarms which are currently * being displayed to that user and which have not yet been acknowledged; * Reply = true if success, false if calendar name error. */ bool AlarmCalendar::initialiseCalendars() { QDir dir; dir.mkpath(QStandardPaths::writableLocation(QStandardPaths::DataLocation)); QString displayCal = QStandardPaths::writableLocation(QStandardPaths::DataLocation) + QLatin1Char('/') + displayCalendarName; - AkonadiDataModel::instance(); + DataModel::initialise(); Preferences::setBackend(Preferences::Akonadi); Preferences::self()->save(); mResourcesCalendar = new AlarmCalendar(); mDisplayCalendar = new AlarmCalendar(displayCal, CalEvent::DISPLAYING); KACalendar::setProductId(KALARM_NAME, KALARM_VERSION); CalFormat::setApplication(QStringLiteral(KALARM_NAME), QString::fromLatin1(KACalendar::icalProductId())); return true; } /****************************************************************************** * Terminate access to all calendars. */ void AlarmCalendar::terminateCalendars() { delete mResourcesCalendar; mResourcesCalendar = nullptr; delete mDisplayCalendar; mDisplayCalendar = nullptr; } /****************************************************************************** * Return the display calendar, opening it first if necessary. */ AlarmCalendar* AlarmCalendar::displayCalendarOpen() { if (mDisplayCalendar->open()) return mDisplayCalendar; qCCritical(KALARM_LOG) << "AlarmCalendar::displayCalendarOpen: 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* AlarmCalendar::getEvent(const EventId& eventId) { if (eventId.eventId().isEmpty()) return nullptr; return mResourcesCalendar->event(eventId); } /****************************************************************************** * Constructor for the resources calendar. */ AlarmCalendar::AlarmCalendar() : mCalType(RESOURCES) , mEventType(CalEvent::EMPTY) { Resources* resources = Resources::instance(); connect(resources, &Resources::eventsAdded, this, &AlarmCalendar::slotEventsAdded); connect(resources, &Resources::eventsToBeRemoved, this, &AlarmCalendar::slotEventsToBeRemoved); connect(resources, &Resources::eventUpdated, this, &AlarmCalendar::slotEventUpdated); connect(resources, &Resources::resourcesPopulated, this, &AlarmCalendar::slotResourcesPopulated); connect(resources, &Resources::settingsChanged, this, &AlarmCalendar::slotResourceSettingsChanged); } /****************************************************************************** * Constructor for a calendar file. */ AlarmCalendar::AlarmCalendar(const QString& path, CalEvent::Type type) : mEventType(type) { switch (type) { case CalEvent::ACTIVE: case CalEvent::ARCHIVED: case CalEvent::TEMPLATE: case CalEvent::DISPLAYING: break; default: Q_ASSERT(false); // invalid event type for a calendar break; } mUrl = QUrl::fromUserInput(path, QString(), QUrl::AssumeLocalFile); QString icalPath = path; icalPath.replace(QStringLiteral("\\.vcs$"), QStringLiteral(".ics")); mICalUrl = QUrl::fromUserInput(icalPath, QString(), QUrl::AssumeLocalFile); mCalType = (path == icalPath) ? LOCAL_ICAL : LOCAL_VCAL; // is the calendar in ICal or VCal format? } AlarmCalendar::~AlarmCalendar() { close(); } /****************************************************************************** * Check whether the calendar is open. */ bool AlarmCalendar::isOpen() { return mOpen; } /****************************************************************************** * Open the calendar if not already open, and load it into memory. */ bool AlarmCalendar::open() { if (isOpen()) return true; if (mCalType == RESOURCES) { mOpen = true; } else { if (!mUrl.isValid()) return false; qCDebug(KALARM_LOG) << "AlarmCalendar::open:" << mUrl.toDisplayString(); if (!mCalendarStorage) { MemoryCalendar::Ptr calendar(new MemoryCalendar(Preferences::timeSpecAsZone())); mCalendarStorage = FileStorage::Ptr(new FileStorage(calendar)); } // Check for file's existence, assuming that it does exist when uncertain, // to avoid overwriting it. auto statJob = KIO::stat(mUrl, KIO::StatJob::SourceSide, 2); KJobWidgets::setWindow(statJob, MainWindow::mainMainWindow()); if (!statJob->exec() || load() == 0) { // The calendar file doesn't yet exist, or it's zero length, so create a new one bool created = false; if (mICalUrl.isLocalFile()) created = saveCal(mICalUrl.toLocalFile()); else { QTemporaryFile tmpFile; tmpFile.setAutoRemove(false); tmpFile.open(); created = saveCal(tmpFile.fileName()); } if (created) 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 AlarmCalendar::load() { if (mCalType == RESOURCES) { } else { if (!mCalendarStorage) return -2; QString filename; qCDebug(KALARM_LOG) << "AlarmCalendar::load:" << mUrl.toDisplayString(); if (!mUrl.isLocalFile()) { auto getJob = KIO::storedGet(mUrl); KJobWidgets::setWindow(getJob, MainWindow::mainMainWindow()); if (!getJob->exec()) { qCCritical(KALARM_LOG) << "AlarmCalendar::load: Download failure"; KAMessageBox::error(MainWindow::mainMainWindow(), xi18nc("@info", "Cannot download calendar: %1", mUrl.toDisplayString())); return -1; } QTemporaryFile tmpFile; tmpFile.setAutoRemove(false); tmpFile.write(getJob->data()); qCDebug(KALARM_LOG) << "--- Downloaded to" << tmpFile.fileName(); filename = tmpFile.fileName(); } else filename = mUrl.toLocalFile(); mCalendarStorage->calendar()->setTimeZone(Preferences::timeSpecAsZone()); mCalendarStorage->setFileName(filename); if (!mCalendarStorage->load()) { // Check if the file is zero length if (mUrl.isLocalFile()) { auto statJob = KIO::stat(KIO::upUrl(mUrl)); KJobWidgets::setWindow(statJob, MainWindow::mainMainWindow()); statJob->exec(); KFileItem fi(statJob->statResult(), mUrl); if (!fi.size()) return 0; // file is zero length } qCCritical(KALARM_LOG) << "AlarmCalendar::load: Error loading calendar file '" << filename <<"'"; KAMessageBox::error(MainWindow::mainMainWindow(), xi18nc("@info", "Error loading calendar:%1Please fix or delete the file.", mUrl.toDisplayString())); // load() could have partially populated the calendar, so clear it out mCalendarStorage->calendar()->close(); mCalendarStorage->calendar().clear(); mCalendarStorage.clear(); mOpen = false; return -1; } if (!mLocalFile.isEmpty()) { if (mLocalFile.startsWith(QDir::tempPath())) QFile::remove(mLocalFile); } mLocalFile = filename; fix(mCalendarStorage); // convert events to current KAlarm format for when calendar is saved updateDisplayKAEvents(); } mOpen = true; return 1; } /****************************************************************************** * Reload the calendar file into memory. */ bool AlarmCalendar::reload() { if (mCalType == RESOURCES) return true; if (!mCalendarStorage) return false; { qCDebug(KALARM_LOG) << "AlarmCalendar::reload:" << mUrl.toDisplayString(); close(); return open(); } } /****************************************************************************** * Save the calendar from memory to file. * If a filename is specified, create a new calendar file. */ bool AlarmCalendar::saveCal(const QString& newFile) { if (mCalType == RESOURCES) return true; if (!mCalendarStorage) return false; { if (!mOpen && newFile.isNull()) return false; qCDebug(KALARM_LOG) << "AlarmCalendar::saveCal:" << "\"" << newFile << "\"," << mEventType; QString saveFilename = newFile.isNull() ? mLocalFile : newFile; if (mCalType == LOCAL_VCAL && newFile.isNull() && mUrl.isLocalFile()) saveFilename = mICalUrl.toLocalFile(); mCalendarStorage->setFileName(saveFilename); mCalendarStorage->setSaveFormat(new ICalFormat); if (!mCalendarStorage->save()) { qCCritical(KALARM_LOG) << "AlarmCalendar::saveCal: Saving" << saveFilename << "failed."; KAMessageBox::error(MainWindow::mainMainWindow(), xi18nc("@info", "Failed to save calendar to %1", mICalUrl.toDisplayString())); return false; } if (!mICalUrl.isLocalFile()) { QFile file(saveFilename); file.open(QIODevice::ReadOnly); auto putJob = KIO::storedPut(&file, mICalUrl, -1); KJobWidgets::setWindow(putJob, MainWindow::mainMainWindow()); if (!putJob->exec()) { qCCritical(KALARM_LOG) << "AlarmCalendar::saveCal:" << saveFilename << "upload failed."; KAMessageBox::error(MainWindow::mainMainWindow(), xi18nc("@info", "Cannot upload calendar to %1", mICalUrl.toDisplayString())); return false; } } if (mCalType == LOCAL_VCAL) { // The file was in vCalendar format, but has now been saved in iCalendar format. mUrl = mICalUrl; mCalType = LOCAL_ICAL; } Q_EMIT calendarSaved(this); } mUpdateSave = false; return true; } /****************************************************************************** * Delete any temporary file at program exit. */ void AlarmCalendar::close() { if (mCalType != RESOURCES) { if (!mLocalFile.isEmpty()) { if (mLocalFile.startsWith(QDir::tempPath())) // removes it only if it IS a temporary file QFile::remove(mLocalFile); mLocalFile = QStringLiteral(""); } } // Flag as closed now to prevent removeKAEvents() doing silly things // when it's called again mOpen = false; if (mCalendarStorage) { mCalendarStorage->calendar()->close(); mCalendarStorage->calendar().clear(); mCalendarStorage.clear(); } // 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); } /****************************************************************************** * 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 AlarmCalendar::updateDisplayKAEvents() { if (mCalType == RESOURCES) return; qCDebug(KALARM_LOG) << "AlarmCalendar::updateDisplayKAEvents"; const ResourceId key = DISPLAY_COL_ID; KAEvent::List& events = mResourceMap[key]; for (KAEvent* event : events) { mEventMap.remove(EventId(key, event->id())); delete event; } events.clear(); mEarliestAlarm[key] = nullptr; 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) << "AlarmCalendar::updateDisplayKAEvents: Ignoring unusable event" << kcalevent->uid(); delete event; continue; // ignore events without usable alarms } - event->setCollectionId(key); + 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. */ void AlarmCalendar::removeKAEvents(ResourceId key, bool closing, 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->collectionId() != key); + bool remove = (event->resourceId() != key); if (remove) { if (key != DISPLAY_COL_ID) - qCCritical(KALARM_LOG) << "AlarmCalendar::removeKAEvents: Event" << event->id() << ", resource" << event->collectionId() << "Indexed under resource" << key; + 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); } if (removed) { mEarliestAlarm.remove(key); // Emit signal only if we're not in the process of closing the calendar if (!closing && mOpen) { Q_EMIT earliestAlarmChanged(); if (mHaveDisabledAlarms) checkForDisabledAlarms(); } } } /****************************************************************************** * 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 AlarmCalendar::slotResourceSettingsChanged(Resource& resource, ResourceType::Changes change) { if (change & ResourceType::Enabled) { if (resource.isValid()) { // For each alarm type which has been disabled, remove the - // collection's events from the map, but not from the resource. + // 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 collection's + // 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 AlarmCalendar::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 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 AlarmCalendar::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 AlarmCalendar::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 AlarmCalendar::slotEventsToBeRemoved(Resource& resource, const QList& events) { for (const KAEvent& event : events) { if (mEventMap.contains(EventId(event))) deleteEventInternal(event, resource, false); } } /****************************************************************************** * 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 AlarmCalendar::importAlarms(QWidget* parent, Resource* resourceptr) { if (mCalType != RESOURCES) return false; Resource nullresource; Resource& resource(resourceptr ? *resourceptr : nullresource); qCDebug(KALARM_LOG) << "AlarmCalendar::importAlarms"; const QUrl url = QFileDialog::getOpenFileUrl(parent, QString(), mLastImportUrl, QStringLiteral("%1 (*.vcs *.ics)").arg(i18nc("@info", "Calendar Files"))); if (url.isEmpty()) { qCCritical(KALARM_LOG) << "AlarmCalendar::importAlarms: Empty URL"; return false; } if (!url.isValid()) { qCDebug(KALARM_LOG) << "AlarmCalendar::importAlarms: Invalid URL"; return false; } mLastImportUrl = url.adjusted(QUrl::RemoveFilename); qCDebug(KALARM_LOG) << "AlarmCalendar::importAlarms:" << url.toDisplayString(); bool success = true; QString filename; bool local = url.isLocalFile(); if (local) { filename = url.toLocalFile(); if (!QFile::exists(filename)) { qCDebug(KALARM_LOG) << "AlarmCalendar::importAlarms: File '" << url.toDisplayString() <<"' not found"; KAMessageBox::error(parent, xi18nc("@info", "Could not load calendar %1.", url.toDisplayString())); return false; } } else { auto getJob = KIO::storedGet(url); KJobWidgets::setWindow(getJob, MainWindow::mainMainWindow()); if (!getJob->exec()) { qCCritical(KALARM_LOG) << "AlarmCalendar::importAlarms: Download failure"; KAMessageBox::error(parent, xi18nc("@info", "Cannot download calendar: %1", url.toDisplayString())); return false; } QTemporaryFile tmpFile; tmpFile.setAutoRemove(false); tmpFile.write(getJob->data()); tmpFile.seek(0); filename = tmpFile.fileName(); qCDebug(KALARM_LOG) << "--- Downloaded to" << filename; } // Read the calendar and add its alarms to the current calendars MemoryCalendar::Ptr cal(new MemoryCalendar(Preferences::timeSpecAsZone())); FileStorage::Ptr calStorage(new FileStorage(cal, filename)); success = calStorage->load(); if (!success) { qCDebug(KALARM_LOG) << "AlarmCalendar::importAlarms: Error loading calendar '" << filename <<"'"; KAMessageBox::error(parent, xi18nc("@info", "Could not load calendar %1.", url.toDisplayString())); } else { const KACalendar::Compat caltype = fix(calStorage); const CalEvent::Types wantedTypes = resource.alarmTypes(); const Event::List events = cal->rawEvents(); for (Event::Ptr event : events) { if (event->alarms().isEmpty() || !KAEvent(event).isValid()) continue; // ignore events without alarms, or usable alarms CalEvent::Type type = CalEvent::status(event); if (type == CalEvent::TEMPLATE) { // If we know the event was not created by KAlarm, don't treat it as a template if (caltype == KACalendar::Incompatible) type = CalEvent::ACTIVE; } Resource res; if (resource.isValid()) { if (!(type & wantedTypes)) continue; res = resource; } else { switch (type) { case CalEvent::ACTIVE: case CalEvent::ARCHIVED: case CalEvent::TEMPLATE: break; default: continue; } - res = Resources::destination(type); + res = Resources::destination(type); } Event::Ptr newev(new Event(*event)); // If there is a display alarm without display text, use the event // summary text instead. if (type == CalEvent::ACTIVE && !newev->summary().isEmpty()) { const Alarm::List& alarms = newev->alarms(); for (Alarm::Ptr alarm : alarms) { if (alarm->type() == Alarm::Display && alarm->text().isEmpty()) alarm->setText(newev->summary()); } newev->setSummary(QString()); // KAlarm only uses summary for template names } // Give the event a new ID and add it to the calendars newev->setUid(CalEvent::uid(CalFormat::createUniqueId(), type)); if (!res.addEvent(KAEvent(newev))) success = false; } } if (!local) QFile::remove(filename); 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 AlarmCalendar::exportAlarms(const KAEvent::List& events, QWidget* parent) { bool append; QString file = FileDialog::getSaveFileName(QUrl(QStringLiteral("kfiledialog:///exportalarms")), 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) << "AlarmCalendar::exportAlarms: Invalid URL" << url; return false; } qCDebug(KALARM_LOG) << "AlarmCalendar::exportAlarms:" << url.toDisplayString(); MemoryCalendar::Ptr calendar(new MemoryCalendar(Preferences::timeSpecAsZone())); FileStorage::Ptr calStorage(new FileStorage(calendar, file)); if (append && !calStorage->load()) { KIO::UDSEntry uds; auto statJob = KIO::stat(url, KIO::StatJob::SourceSide, 2); KJobWidgets::setWindow(statJob, parent); statJob->exec(); KFileItem fi(statJob->statResult(), url); if (fi.size()) { qCCritical(KALARM_LOG) << "AlarmCalendar::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) << "AlarmCalendar::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) << "AlarmCalendar::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; } /****************************************************************************** * Flag the start of a group of calendar update calls. * The purpose is to avoid multiple calendar saves during a group of operations. */ void AlarmCalendar::startUpdate() { ++mUpdateCount; } /****************************************************************************** * Flag the end of a group of calendar update calls. * The calendar is saved if appropriate. */ bool AlarmCalendar::endUpdate() { if (mUpdateCount > 0) --mUpdateCount; if (!mUpdateCount) { if (mUpdateSave) return saveCal(); } return true; } /****************************************************************************** * Save the calendar, or flag it for saving if in a group of calendar update calls. * Note that this method has no effect for Akonadi calendars. */ bool AlarmCalendar::save() { if (mUpdateCount) { mUpdateSave = true; return true; } else return saveCal(); } /****************************************************************************** * 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 AlarmCalendar::purgeEvents(const KAEvent::List& events) { for (const KAEvent* event : events) { deleteEventInternal(*event); } if (mHaveDisabledAlarms) checkForDisabledAlarms(); saveCal(); } /****************************************************************************** * 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 'event' (if non-null). * 'event' 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 'event' was written to the calendar, in which case (not * Akonadi) ownership of 'event' is taken by the calendar. 'event' * is updated. * = false if an error occurred, in which case 'event' is unchanged. */ bool AlarmCalendar::addEvent(KAEvent& evnt, QWidget* promptParent, bool useEventID, Resource* resourceptr, bool noPrompt, bool* cancelled) { if (cancelled) *cancelled = false; if (!mOpen) return false; // Check that the event type is valid for the calendar Resource nullresource; Resource& resource(resourceptr ? *resourceptr : nullresource); qCDebug(KALARM_LOG) << "AlarmCalendar::addEvent:" << evnt.id() << ", resource" << resource.id(); const CalEvent::Type type = evnt.category(); if (type != mEventType) { switch (type) { case CalEvent::ACTIVE: case CalEvent::ARCHIVED: case CalEvent::TEMPLATE: if (mEventType == CalEvent::EMPTY) break; // fall through to default Q_FALLTHROUGH(); default: return false; } } ResourceId key = resource.id(); Event::Ptr kcalEvent((mCalType == RESOURCES) ? (Event*)nullptr : new Event); 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 = (mCalType == RESOURCES) ? CalFormat::createUniqueId() : kcalEvent->uid(); if (useEventID) { id = CalEvent::uid(id, type); if (kcalEvent) kcalEvent->setUid(id); } event->setEventId(id); bool ok = false; bool remove = false; if (mCalType == RESOURCES) { Resource res; if (resource.isEnabled(type)) res = resource; else { - res = Resources::destination(type, promptParent, noPrompt, cancelled); + 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) << "AlarmCalendar::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. 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); } } else { // It's the display calendar event->updateKCalEvent(kcalEvent, KAEvent::UID_IGNORE); key = DISPLAY_COL_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); if (mEarliestAlarm[key] == event) findEarliestAlarm(key); } 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->setCollectionId(key); + event->setResourceId(key); if (!replace) { mResourceMap[key] += event; mEventMap[EventId(key, event->id())] = event; } if ((resource.alarmTypes() & CalEvent::ACTIVE) && event->category() == CalEvent::ACTIVE) { // Update the earliest alarm to trigger 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, in which case (not * Akonadi) ownership of 'newEvent' is taken by the calendar. * 'newEvent' is updated. * = false if an error occurred, in which case 'newEvent' is unchanged. */ bool AlarmCalendar::modifyEvent(const EventId& oldEventId, KAEvent& newEvent) { const EventId newId(oldEventId.collectionId(), newEvent.id()); qCDebug(KALARM_LOG) << "AlarmCalendar::modifyEvent:" << oldEventId << "->" << newId; bool noNewId = newId.isEmpty(); if (!noNewId && oldEventId == newId) { qCCritical(KALARM_LOG) << "AlarmCalendar::modifyEvent: Same IDs"; return false; } if (!mOpen) return false; if (mCalType == RESOURCES) { // Set the event's ID and Akonadi ID, and update the old // event in Akonadi. const KAEvent* storedEvent = event(oldEventId); if (!storedEvent) { qCCritical(KALARM_LOG) << "AlarmCalendar::modifyEvent: Old event not found"; return false; } if (noNewId) newEvent.setEventId(CalFormat::createUniqueId()); Resource resource = Resources::resource(oldEventId.collectionId()); if (!resource.isValid()) return false; // Don't add new event to mEventMap yet - its Akonadi item id is not yet known 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(); } else { // This functionality isn't needed for the display calendar. // The calendar would take ownership of newEvent. return false; } 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* AlarmCalendar::updateEvent(const KAEvent& evnt) { return updateEvent(&evnt); } KAEvent* AlarmCalendar::updateEvent(const KAEvent* evnt) { if (!mOpen || mCalType != RESOURCES) return nullptr; KAEvent* kaevnt = event(EventId(*evnt)); if (kaevnt) { Resource resource = Resources::resourceForEvent(evnt->id()); if (resource.updateEvent(*evnt)) { *kaevnt = *evnt; return kaevnt; } } qCDebug(KALARM_LOG) << "AlarmCalendar::updateEvent: error"; return nullptr; } /****************************************************************************** * Delete the specified event from the resource calendar, if it exists. * The calendar is then optionally saved. */ bool AlarmCalendar::deleteEvent(const KAEvent& event, bool saveit) { if (mOpen && mCalType == RESOURCES) { const CalEvent::Type status = deleteEventInternal(event); if (mHaveDisabledAlarms) checkForDisabledAlarms(); if (status != CalEvent::EMPTY) { if (saveit) return save(); return true; } } return false; } /****************************************************************************** * Delete the specified event from the calendar, if it exists. * The calendar is then optionally saved. */ bool AlarmCalendar::deleteDisplayEvent(const QString& eventID, bool saveit) { if (mOpen && mCalType != RESOURCES) { Resource resource; const CalEvent::Type status = deleteEventInternal(eventID, KAEvent(), resource, false); if (mHaveDisabledAlarms) checkForDisabledAlarms(); 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 AlarmCalendar::deleteEventInternal(const KAEvent& event, bool deleteFromAkonadi) { - Resource resource = Resources::resource(event.collectionId()); + Resource resource = Resources::resource(event.resourceId()); if (!resource.isValid()) return CalEvent::EMPTY; return deleteEventInternal(event.id(), event, resource, deleteFromAkonadi); } CalEvent::Type AlarmCalendar::deleteEventInternal(const KAEvent& event, Resource& resource, bool deleteFromAkonadi) { if (!resource.isValid()) return CalEvent::EMPTY; - if (event.collectionId() != resource.id()) + if (event.resourceId() != resource.id()) { - qCCritical(KALARM_LOG) << "AlarmCalendar::deleteEventInternal: Event" << event.id() << ": resource" << event.collectionId() << "differs from 'resource'" << resource.id(); + qCCritical(KALARM_LOG) << "AlarmCalendar::deleteEventInternal: Event" << event.id() << ": resource" << event.resourceId() << "differs from 'resource'" << resource.id(); return CalEvent::EMPTY; } return deleteEventInternal(event.id(), event, resource, deleteFromAkonadi); } CalEvent::Type AlarmCalendar::deleteEventInternal(const QString& eventID, const KAEvent& event, Resource& resource, bool deleteFromAkonadi) { // 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; Event::Ptr kcalEvent; if (mCalendarStorage) kcalEvent = mCalendarStorage->calendar()->event(id); const ResourceId key = resource.id(); KAEventMap::Iterator it = mEventMap.find(EventId(key, id)); if (it != mEventMap.end()) { 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; if (mEarliestAlarm[key] == ev) findEarliestAlarm(resource); } else { for (EarliestMap::Iterator eit = mEarliestAlarm.begin(); eit != mEarliestAlarm.end(); ++eit) { KAEvent* ev = eit.value(); if (ev && ev->id() == id) { findEarliestAlarm(eit.key()); break; } } } CalEvent::Type status = CalEvent::EMPTY; if (kcalEvent) { status = CalEvent::status(kcalEvent); mCalendarStorage->calendar()->deleteEvent(kcalEvent); } else if (deleteFromAkonadi) { // It's an Akonadi event CalEvent::Type s = paramEvent.category(); if (resource.deleteEvent(paramEvent)) status = s; } return status; } /****************************************************************************** * Return the event with the specified ID. * If 'checkDuplicates' 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, bool checkDuplicates) { if (!isValid()) return nullptr; const QString eventId = uniqueID.eventId(); if (uniqueID.collectionId() == -1 && checkDuplicates) { // The resource isn't known, but use the event ID if it is unique among // all resources. const KAEvent::List list = events(eventId); if (list.count() > 1) { qCWarning(KALARM_LOG) << "AlarmCalendar::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(); } /****************************************************************************** * Return the event with the specified ID. * For the Akonadi version, this method is for the display calendar only. */ Event::Ptr AlarmCalendar::kcalEvent(const QString& uniqueID) { Q_ASSERT(mCalType != RESOURCES); // only allowed for display calendar if (!mCalendarStorage) return Event::Ptr(); return mCalendarStorage->calendar()->event(uniqueID); } /****************************************************************************** * Find the alarm template with the specified name. * Reply = 0 if not found. */ KAEvent* AlarmCalendar::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 AlarmCalendar::events(const QString& uniqueId) const { KAEvent::List list; if (mCalType == RESOURCES && 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; } /****************************************************************************** * Return all events in the calendar which contain alarms. * Optionally the event type can be filtered, using an OR of event types. */ KAEvent::List AlarmCalendar::events(const Resource& resource, CalEvent::Types type) const { KAEvent::List list; if (mCalType != RESOURCES && (!mCalendarStorage || resource.isValid())) return 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 all events in the calendar which contain usable alarms. * For the Akonadi version, this method is for the display calendar only. * Optionally the event type can be filtered, using an OR of event types. */ Event::List AlarmCalendar::kcalEvents(CalEvent::Type type) { Event::List list; Q_ASSERT(mCalType != RESOURCES); // only allowed for display calendar 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 AlarmCalendar::eventReadOnly(const QString& eventId) const { if (mCalType != RESOURCES) return true; 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 AlarmCalendar::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 AlarmCalendar::checkForDisabledAlarms(bool oldEnabled, bool newEnabled) { if (mCalType == RESOURCES && 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 AlarmCalendar::checkForDisabledAlarms() { if (mCalType != RESOURCES) return; 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 AlarmCalendar::findEarliestAlarm(const Resource& resource) { if (mCalType != RESOURCES) return; if (!(resource.alarmTypes() & CalEvent::ACTIVE)) return; findEarliestAlarm(resource.id()); } void AlarmCalendar::findEarliestAlarm(ResourceId key) { EarliestMap::Iterator eit = mEarliestAlarm.find(key); if (eit != mEarliestAlarm.end()) eit.value() = nullptr; if (mCalType != RESOURCES || 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* AlarmCalendar::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 AlarmCalendar::setAlarmPending(KAEvent* event, bool pending) { const QString id = event->id(); bool wasPending = mPendingAlarms.contains(id); qCDebug(KALARM_LOG) << "AlarmCalendar::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()); } /****************************************************************************** * Find the version of KAlarm which wrote the calendar file, and do any * necessary conversions to the current format. */ KACalendar::Compat fix(const FileStorage::Ptr& fileStorage) { QString versionString; int version = KACalendar::updateVersion(fileStorage, versionString); if (version == KACalendar::IncompatibleFormat) return KACalendar::Incompatible; // calendar was created by another program, or an unknown version of KAlarm return KACalendar::Current; } // vim: et sw=4: diff --git a/src/editdlg.cpp b/src/editdlg.cpp index 4b7b8fbd..ea13f4c6 100644 --- a/src/editdlg.cpp +++ b/src/editdlg.cpp @@ -1,1478 +1,1478 @@ /* * editdlg.cpp - dialog to create or modify an alarm or alarm template * Program: kalarm * Copyright © 2001-2019 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 "editdlg.h" #include "editdlg_p.h" #include "editdlgtypes.h" #include "alarmcalendar.h" #include "alarmtimewidget.h" #include "deferdlg.h" #include "functions.h" #include "kalarmapp.h" #include "latecancel.h" #include "mainwindow.h" #include "preferences.h" #include "recurrenceedit.h" #include "reminder.h" -#include "resources/akonadidatamodel.h" +#include "resources/datamodel.h" #include "resources/resources.h" #include "lib/autoqpointer.h" #include "lib/buttongroup.h" #include "lib/checkbox.h" #include "lib/config.h" #include "lib/lineedit.h" #include "lib/messagebox.h" #include "lib/packedlayout.h" #include "lib/radiobutton.h" #include "lib/shellprocess.h" #include "lib/spinbox.h" #include "lib/stackedwidgets.h" #include "templatepickdlg.h" #include "lib/timeedit.h" #include "lib/timespinbox.h" #include "kalarm_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace KCal; using namespace KAlarmCal; static const char EDIT_DIALOG_NAME[] = "EditDialog"; static const char TEMPLATE_DIALOG_NAME[] = "EditTemplateDialog"; static const char EDIT_MORE_GROUP[] = "ShowOpts"; static const char EDIT_MORE_KEY[] = "EditMore"; static const int maxDelayTime = 99*60 + 59; // < 100 hours inline QString recurText(const KAEvent& event) { QString r; if (event.repetition()) r = QStringLiteral("%1 / %2").arg(event.recurrenceText()).arg(event.repetitionText()); else r = event.recurrenceText(); return i18nc("@title:tab", "Recurrence - [%1]", r); } QList EditAlarmDlg::mWindowList; // Collect these widget labels together to ensure consistent wording and // translations across different modules. QString EditAlarmDlg::i18n_chk_ShowInKOrganizer() { return i18nc("@option:check", "Show in KOrganizer"); } EditAlarmDlg* EditAlarmDlg::create(bool Template, Type type, QWidget* parent, GetResourceType getResource) { qCDebug(KALARM_LOG) << "EditAlarmDlg::create"; switch (type) { case DISPLAY: return new EditDisplayAlarmDlg(Template, parent, getResource); case COMMAND: return new EditCommandAlarmDlg(Template, parent, getResource); case EMAIL: return new EditEmailAlarmDlg(Template, parent, getResource); case AUDIO: return new EditAudioAlarmDlg(Template, parent, getResource); default: break; } return nullptr; } EditAlarmDlg* EditAlarmDlg::create(bool Template, const KAEvent* event, bool newAlarm, QWidget* parent, GetResourceType getResource, bool readOnly) { switch (event->actionTypes()) { case KAEvent::ACT_COMMAND: return new EditCommandAlarmDlg(Template, event, newAlarm, parent, getResource, readOnly); case KAEvent::ACT_DISPLAY_COMMAND: case KAEvent::ACT_DISPLAY: return new EditDisplayAlarmDlg(Template, event, newAlarm, parent, getResource, readOnly); case KAEvent::ACT_EMAIL: return new EditEmailAlarmDlg(Template, event, newAlarm, parent, getResource, readOnly); case KAEvent::ACT_AUDIO: return new EditAudioAlarmDlg(Template, event, newAlarm, parent, getResource, readOnly); default: break; } return nullptr; } /****************************************************************************** * Constructor. * Parameters: * Template = true to edit/create an alarm template * = false to edit/create an alarm. * event != to initialise the dialog to show the specified event's data. */ EditAlarmDlg::EditAlarmDlg(bool Template, KAEvent::SubAction action, QWidget* parent, GetResourceType getResource) : QDialog(parent) , mAlarmType(action) , mTemplate(Template) , mNewAlarm(true) , mDesiredReadOnly(false) , mReadOnly(false) { init(nullptr, getResource); mWindowList.append(this); } EditAlarmDlg::EditAlarmDlg(bool Template, const KAEvent* event, bool newAlarm, QWidget* parent, GetResourceType getResource, bool readOnly) : QDialog(parent) , mAlarmType(event->actionSubType()) , mEventId(newAlarm ? QString() : event->id()) , mTemplate(Template) , mNewAlarm(newAlarm) , mDesiredReadOnly(readOnly) , mReadOnly(readOnly) { init(event, getResource); mWindowList.append(this); } void EditAlarmDlg::init(const KAEvent* event, GetResourceType getResource) { switch (getResource) { case RES_USE_EVENT_ID: if (event) { mResourceEventId = event->id(); mUseResourceEventId = true; break; } Q_FALLTHROUGH(); // fall through to RES_PROMPT case RES_PROMPT: mResourceEventId.clear(); mUseResourceEventId = true; break; case RES_IGNORE: default: mResourceEventId.clear(); mUseResourceEventId = false; break; } } void EditAlarmDlg::init(const KAEvent* event) { setObjectName(mTemplate ? QStringLiteral("TemplEditDlg") : QStringLiteral("EditDlg")); // used by LikeBack QString caption; if (mReadOnly) caption = mTemplate ? i18nc("@title:window", "Alarm Template [read-only]") : event->expired() ? i18nc("@title:window", "Archived Alarm [read-only]") : i18nc("@title:window", "Alarm [read-only]"); else caption = type_caption(); setWindowTitle(caption); // Create button box now so that types can work with it, but don't insert // it into layout just yet mButtonBox = new QDialogButtonBox(this); if (mReadOnly) { mButtonBox->addButton(QDialogButtonBox::Cancel); mTryButton = mButtonBox->addButton(i18nc("@action:button", "Try"), QDialogButtonBox::ActionRole); mMoreLessButton = mButtonBox->addButton(QDialogButtonBox::RestoreDefaults); } else if (mTemplate) { mButtonBox->addButton(QDialogButtonBox::Ok); mButtonBox->addButton(QDialogButtonBox::Cancel); mTryButton = mButtonBox->addButton(i18nc("@action:button", "Try"), QDialogButtonBox::ActionRole); mMoreLessButton = mButtonBox->addButton(QDialogButtonBox::RestoreDefaults); } else { mButtonBox->addButton(QDialogButtonBox::Ok); mButtonBox->addButton(QDialogButtonBox::Cancel); mTryButton = mButtonBox->addButton(i18nc("@action:button", "Try"), QDialogButtonBox::ActionRole); mLoadTemplateButton = mButtonBox->addButton(i18nc("@action:button", "Load Template..."), QDialogButtonBox::HelpRole); mMoreLessButton = mButtonBox->addButton(QDialogButtonBox::RestoreDefaults); } connect(mButtonBox, &QDialogButtonBox::clicked, this, &EditAlarmDlg::slotButtonClicked); if (mButtonBox->button(QDialogButtonBox::Ok)) mButtonBox->button(QDialogButtonBox::Ok)->setWhatsThis(i18nc("@info:whatsthis", "Schedule the alarm at the specified time.")); QVBoxLayout* mainLayout = new QVBoxLayout(this); if (mTemplate) { QFrame* frame = new QFrame; QHBoxLayout* box = new QHBoxLayout(); frame->setLayout(box); box->setContentsMargins(0, 0, 0, 0); box->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); QLabel* label = new QLabel(i18nc("@label:textbox", "Template name:")); label->setFixedSize(label->sizeHint()); box->addWidget(label); mTemplateName = new QLineEdit(); mTemplateName->setReadOnly(mReadOnly); connect(mTemplateName, &QLineEdit::textEdited, this, &EditAlarmDlg::contentsChanged); label->setBuddy(mTemplateName); box->addWidget(mTemplateName); frame->setWhatsThis(i18nc("@info:whatsthis", "Enter the name of the alarm template")); frame->setFixedHeight(box->sizeHint().height()); mainLayout->addWidget(frame); } mTabs = new QTabWidget(this); mainLayout->addWidget(mTabs); mTabScrollGroup = new StackedScrollGroup(this, mTabs); StackedScrollWidget* mainScroll = new StackedScrollWidget(mTabScrollGroup); mTabs->addTab(mainScroll, i18nc("@title:tab", "Alarm")); mMainPageIndex = 0; PageFrame* mainPage = new PageFrame(mainScroll); mainScroll->setWidget(mainPage); // mainPage becomes the child of mainScroll connect(mainPage, &PageFrame::shown, this, &EditAlarmDlg::slotShowMainPage); QVBoxLayout* topLayout = new QVBoxLayout(mainPage); int dcm = style()->pixelMetric(QStyle::PM_DefaultChildMargin); topLayout->setContentsMargins(dcm, dcm, dcm, dcm); topLayout->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); // Recurrence tab StackedScrollWidget* recurScroll = new StackedScrollWidget(mTabScrollGroup); mTabs->addTab(recurScroll, QString()); mRecurPageIndex = 1; QFrame* recurTab = new QFrame; QVBoxLayout* recurTabLayout = new QVBoxLayout(); recurTabLayout->setContentsMargins(dcm, dcm, dcm, dcm); recurTab->setLayout(recurTabLayout); recurScroll->setWidget(recurTab); // recurTab becomes the child of recurScroll mRecurrenceEdit = new RecurrenceEdit(mReadOnly); recurTabLayout->addWidget(mRecurrenceEdit); connect(mRecurrenceEdit, &RecurrenceEdit::shown, this, &EditAlarmDlg::slotShowRecurrenceEdit); connect(mRecurrenceEdit, &RecurrenceEdit::typeChanged, this, &EditAlarmDlg::slotRecurTypeChange); connect(mRecurrenceEdit, &RecurrenceEdit::frequencyChanged, this, &EditAlarmDlg::slotRecurFrequencyChange); connect(mRecurrenceEdit, &RecurrenceEdit::repeatNeedsInitialisation, this, &EditAlarmDlg::slotSetSubRepetition); connect(mRecurrenceEdit, &RecurrenceEdit::contentsChanged, this, &EditAlarmDlg::contentsChanged); // Controls specific to the alarm type QGroupBox* actionBox = new QGroupBox(i18nc("@title:group", "Action"), mainPage); topLayout->addWidget(actionBox, 1); QVBoxLayout* layout = new QVBoxLayout(actionBox); layout->setContentsMargins(dcm, dcm, dcm, dcm); layout->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); type_init(actionBox, layout); if (!mTemplate) { // Deferred date/time: visible only for a deferred recurring event. mDeferGroup = new QGroupBox(i18nc("@title:group", "Deferred Alarm"), mainPage); topLayout->addWidget(mDeferGroup); QHBoxLayout* hlayout = new QHBoxLayout(mDeferGroup); hlayout->setContentsMargins(dcm, dcm, dcm, dcm); hlayout->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); QLabel* label = new QLabel(i18nc("@label", "Deferred to:"), mDeferGroup); label->setFixedSize(label->sizeHint()); hlayout->addWidget(label); mDeferTimeLabel = new QLabel(mDeferGroup); hlayout->addWidget(mDeferTimeLabel); mDeferChangeButton = new QPushButton(i18nc("@action:button", "Change..."), mDeferGroup); mDeferChangeButton->setFixedSize(mDeferChangeButton->sizeHint()); connect(mDeferChangeButton, &QPushButton::clicked, this, &EditAlarmDlg::slotEditDeferral); mDeferChangeButton->setWhatsThis(i18nc("@info:whatsthis", "Change the alarm's deferred time, or cancel the deferral")); hlayout->addWidget(mDeferChangeButton); //?? mDeferGroup->addSpace(0); } QHBoxLayout* hlayout = new QHBoxLayout(); hlayout->setContentsMargins(0, 0, 0, 0); topLayout->addLayout(hlayout); // Date and time entry if (mTemplate) { QGroupBox* templateTimeBox = new QGroupBox(i18nc("@title:group", "Time"), mainPage); topLayout->addWidget(templateTimeBox); QGridLayout* grid = new QGridLayout(templateTimeBox); grid->setContentsMargins(dcm, dcm, dcm, dcm); grid->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); mTemplateTimeGroup = new ButtonGroup(templateTimeBox); connect(mTemplateTimeGroup, &ButtonGroup::buttonSet, this, &EditAlarmDlg::slotTemplateTimeType); connect(mTemplateTimeGroup, &ButtonGroup::buttonSet, this, &EditAlarmDlg::contentsChanged); mTemplateDefaultTime = new RadioButton(i18nc("@option:radio", "Default time"), templateTimeBox); mTemplateDefaultTime->setFixedSize(mTemplateDefaultTime->sizeHint()); mTemplateDefaultTime->setReadOnly(mReadOnly); mTemplateDefaultTime->setWhatsThis(i18nc("@info:whatsthis", "Do not specify a start time for alarms based on this template. " "The normal default start time will be used.")); mTemplateTimeGroup->addButton(mTemplateDefaultTime); grid->addWidget(mTemplateDefaultTime, 0, 0, Qt::AlignLeft); QWidget* box = new QWidget(templateTimeBox); QHBoxLayout* layout = new QHBoxLayout(box); layout->setContentsMargins(0, 0, 0, 0); layout->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); mTemplateUseTime = new RadioButton(i18nc("@option:radio", "Time:"), box); mTemplateUseTime->setFixedSize(mTemplateUseTime->sizeHint()); mTemplateUseTime->setReadOnly(mReadOnly); mTemplateUseTime->setWhatsThis(i18nc("@info:whatsthis", "Specify a start time for alarms based on this template.")); layout->addWidget(mTemplateUseTime); mTemplateTimeGroup->addButton(mTemplateUseTime); mTemplateTime = new TimeEdit(); mTemplateTime->setFixedSize(mTemplateTime->sizeHint()); mTemplateTime->setReadOnly(mReadOnly); mTemplateTime->setWhatsThis(xi18nc("@info:whatsthis", "Enter the start time for alarms based on this template.%1", TimeSpinBox::shiftWhatsThis())); connect(mTemplateTime, &TimeEdit::valueChanged, this, &EditAlarmDlg::contentsChanged); layout->addWidget(mTemplateTime); layout->addStretch(1); grid->addWidget(box, 0, 1, Qt::AlignLeft); mTemplateAnyTime = new RadioButton(i18nc("@option:radio", "Date only"), templateTimeBox); mTemplateAnyTime->setFixedSize(mTemplateAnyTime->sizeHint()); mTemplateAnyTime->setReadOnly(mReadOnly); mTemplateAnyTime->setWhatsThis(xi18nc("@info:whatsthis", "Set the Any time option for alarms based on this template.")); mTemplateTimeGroup->addButton(mTemplateAnyTime); grid->addWidget(mTemplateAnyTime, 1, 0, Qt::AlignLeft); box = new QWidget(templateTimeBox); layout = new QHBoxLayout(box); layout->setContentsMargins(0, 0, 0, 0); layout->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); mTemplateUseTimeAfter = new RadioButton(i18nc("@option:radio", "Time from now:"), box); mTemplateUseTimeAfter->setFixedSize(mTemplateUseTimeAfter->sizeHint()); mTemplateUseTimeAfter->setReadOnly(mReadOnly); mTemplateUseTimeAfter->setWhatsThis(i18nc("@info:whatsthis", "Set alarms based on this template to start after the specified time " "interval from when the alarm is created.")); layout->addWidget(mTemplateUseTimeAfter); mTemplateTimeGroup->addButton(mTemplateUseTimeAfter); mTemplateTimeAfter = new TimeSpinBox(1, maxDelayTime); mTemplateTimeAfter->setValue(1439); mTemplateTimeAfter->setFixedSize(mTemplateTimeAfter->sizeHint()); mTemplateTimeAfter->setReadOnly(mReadOnly); connect(mTemplateTimeAfter, static_cast(&TimeSpinBox::valueChanged), this, &EditAlarmDlg::contentsChanged); mTemplateTimeAfter->setWhatsThis(xi18nc("@info:whatsthis", "%1%2", AlarmTimeWidget::i18n_TimeAfterPeriod(), TimeSpinBox::shiftWhatsThis())); layout->addWidget(mTemplateTimeAfter); box->setFixedHeight(box->sizeHint().height()); grid->addWidget(box, 1, 1, Qt::AlignLeft); hlayout->addStretch(); } else { mTimeWidget = new AlarmTimeWidget(i18nc("@title:group", "Time"), AlarmTimeWidget::AT_TIME, mainPage); connect(mTimeWidget, &AlarmTimeWidget::dateOnlyToggled, this, &EditAlarmDlg::slotAnyTimeToggled); connect(mTimeWidget, &AlarmTimeWidget::changed, this, &EditAlarmDlg::contentsChanged); topLayout->addWidget(mTimeWidget); } // Optional controls depending on More/Fewer Options button mMoreOptions = new QFrame(mainPage); mMoreOptions->setFrameStyle(QFrame::NoFrame); topLayout->addWidget(mMoreOptions); QVBoxLayout* moreLayout = new QVBoxLayout(mMoreOptions); moreLayout->setContentsMargins(0, 0, 0, 0); moreLayout->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); // Reminder mReminder = createReminder(mMoreOptions); if (mReminder) { mReminder->setFixedSize(mReminder->sizeHint()); connect(mReminder, &Reminder::changed, this, &EditAlarmDlg::contentsChanged); moreLayout->addWidget(mReminder, 0, Qt::AlignLeft); if (mTimeWidget) connect(mTimeWidget, &AlarmTimeWidget::changed, mReminder, &Reminder::setDefaultUnits); } // Late cancel selector - default = allow late display mLateCancel = new LateCancelSelector(true, mMoreOptions); connect(mLateCancel, &LateCancelSelector::changed, this, &EditAlarmDlg::contentsChanged); moreLayout->addWidget(mLateCancel, 0, Qt::AlignLeft); PackedLayout* playout = new PackedLayout(Qt::AlignJustify); playout->setSpacing(2 * style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); moreLayout->addLayout(playout); // Acknowledgement confirmation required - default = no confirmation CheckBox* confirmAck = type_createConfirmAckCheckbox(mMoreOptions); if (confirmAck) { confirmAck->setFixedSize(confirmAck->sizeHint()); connect(confirmAck, &CheckBox::toggled, this, &EditAlarmDlg::contentsChanged); playout->addWidget(confirmAck); } if (theApp()->korganizerEnabled()) { // Show in KOrganizer checkbox mShowInKorganizer = new CheckBox(i18n_chk_ShowInKOrganizer(), mMoreOptions); mShowInKorganizer->setFixedSize(mShowInKorganizer->sizeHint()); connect(mShowInKorganizer, &CheckBox::toggled, this, &EditAlarmDlg::contentsChanged); mShowInKorganizer->setWhatsThis(i18nc("@info:whatsthis", "Check to copy the alarm into KOrganizer's calendar")); playout->addWidget(mShowInKorganizer); } mainLayout->addWidget(mButtonBox); // Hide optional controls KConfigGroup config(KSharedConfig::openConfig(), EDIT_MORE_GROUP); showOptions(config.readEntry(EDIT_MORE_KEY, false)); // Initialise the state of all controls according to the specified event, if any initValues(event); if (mTemplateName) mTemplateName->setFocus(); if (!mNewAlarm) { // Save the initial state of all controls so that we can later tell if they have changed saveState((event && (mTemplate || !event->isTemplate())) ? event : nullptr); contentsChanged(); // enable/disable OK button } // Note the current desktop so that the dialog can be shown on it. // If a main window is visible, the dialog will by KDE default always appear on its // desktop. If the user invokes the dialog via the system tray on a different desktop, // that can cause confusion. mDesktop = KWindowSystem::currentDesktop(); if (theApp()->windowFocusBroken()) { const QList children = findChildren(); for (QWidget* w : children) w->installEventFilter(this); } } EditAlarmDlg::~EditAlarmDlg() { delete mButtonBox; mButtonBox = nullptr; // prevent text edit contentsChanged() signal triggering a crash delete mSavedEvent; mWindowList.removeAll(this); } /****************************************************************************** * Return the number of instances. */ int EditAlarmDlg::instanceCount() { return mWindowList.count(); } /****************************************************************************** * Initialise the dialog controls from the specified event. */ void EditAlarmDlg::initValues(const KAEvent* event) { setReadOnly(mDesiredReadOnly); mChanged = false; mOnlyDeferred = false; mExpiredRecurrence = false; mLateCancel->showAutoClose(false); bool deferGroupVisible = false; if (event) { // Set the values to those for the specified event if (mTemplate) mTemplateName->setText(event->templateName()); bool recurs = event->recurs(); if ((recurs || event->repetition()) && !mTemplate && event->deferred()) { deferGroupVisible = true; mDeferDateTime = event->deferDateTime(); mDeferTimeLabel->setText(mDeferDateTime.formatLocale()); mDeferGroup->show(); } if (mTemplate) { // Editing a template int afterTime = event->isTemplate() ? event->templateAfterTime() : -1; bool noTime = !afterTime; bool useTime = !event->mainDateTime().isDateOnly(); RadioButton* button = noTime ? mTemplateDefaultTime : (afterTime > 0) ? mTemplateUseTimeAfter : useTime ? mTemplateUseTime : mTemplateAnyTime; button->setChecked(true); mTemplateTimeAfter->setValue(afterTime > 0 ? afterTime : 1); if (!noTime && useTime) mTemplateTime->setValue(event->mainDateTime().kDateTime().time()); else mTemplateTime->setValue(0); } else { if (event->isTemplate()) { // Initialising from an alarm template: use current date const KADateTime now = KADateTime::currentDateTime(Preferences::timeSpec()); int afterTime = event->templateAfterTime(); if (afterTime >= 0) { mTimeWidget->setDateTime(now.addSecs(afterTime * 60)); mTimeWidget->selectTimeFromNow(); } else { KADateTime dt = event->startDateTime().kDateTime(); dt.setTimeSpec(Preferences::timeSpec()); QDate d = now.date(); if (!dt.isDateOnly() && now.time() >= dt.time()) d = d.addDays(1); // alarm time has already passed, so use tomorrow dt.setDate(d); mTimeWidget->setDateTime(dt); } } else { mExpiredRecurrence = recurs && event->mainExpired(); mTimeWidget->setDateTime(recurs || event->category() == CalEvent::ARCHIVED ? event->startDateTime() : event->mainExpired() ? event->deferDateTime() : event->mainDateTime()); } } KAEvent::SubAction action = event->actionSubType(); AlarmText altext; if (event->commandScript()) altext.setScript(event->cleanText()); else altext.setText(event->cleanText()); setAction(action, altext); mLateCancel->setMinutes(event->lateCancel(), event->startDateTime().isDateOnly(), TimePeriod::HoursMinutes); if (mShowInKorganizer) mShowInKorganizer->setChecked(event->copyToKOrganizer()); type_initValues(event); mRecurrenceEdit->set(*event); // must be called after mTimeWidget is set up, to ensure correct date-only enabling mTabs->setTabText(mRecurPageIndex, recurText(*event)); } else { // Set the values to their defaults const KADateTime defaultTime = KADateTime::currentUtcDateTime().addSecs(60).toTimeSpec(Preferences::timeSpec()); if (mTemplate) { mTemplateDefaultTime->setChecked(true); mTemplateTime->setValue(0); mTemplateTimeAfter->setValue(1); } else mTimeWidget->setDateTime(defaultTime); mLateCancel->setMinutes((Preferences::defaultLateCancel() ? 1 : 0), false, TimePeriod::HoursMinutes); if (mShowInKorganizer) mShowInKorganizer->setChecked(Preferences::defaultCopyToKOrganizer()); type_initValues(nullptr); mRecurrenceEdit->setDefaults(defaultTime); // must be called after mTimeWidget is set up, to ensure correct date-only enabling slotRecurFrequencyChange(); // update the Recurrence text } if (mReminder && mTimeWidget) mReminder->setDefaultUnits(mTimeWidget->getDateTime(nullptr, false, false)); if (!deferGroupVisible && mDeferGroup) mDeferGroup->hide(); bool empty = AlarmCalendar::resources()->events(CalEvent::TEMPLATE).isEmpty(); if (mLoadTemplateButton) mLoadTemplateButton->setEnabled(!empty); } /****************************************************************************** * Initialise various values in the New Alarm dialogue. */ void EditAlarmDlg::setTime(const DateTime& start) { mTimeWidget->setDateTime(start); } void EditAlarmDlg::setRecurrence(const KARecurrence& recur, const KCalendarCore::Duration& subRepeatInterval, int subRepeatCount) { KAEvent event; event.setTime(mTimeWidget->getDateTime(nullptr, false, false)); event.setRecurrence(recur); event.setRepetition(Repetition(subRepeatInterval, subRepeatCount - 1)); mRecurrenceEdit->set(event); } void EditAlarmDlg::setRepeatAtLogin() { mRecurrenceEdit->setRepeatAtLogin(); } void EditAlarmDlg::setLateCancel(int minutes) { mLateCancel->setMinutes(minutes, mTimeWidget->getDateTime(nullptr, false, false).isDateOnly(), TimePeriod::HoursMinutes); } void EditAlarmDlg::setShowInKOrganizer(bool show) { mShowInKorganizer->setChecked(show); } /****************************************************************************** * Set the read-only status of all non-template controls. */ void EditAlarmDlg::setReadOnly(bool readOnly) { mReadOnly = readOnly; if (mTimeWidget) mTimeWidget->setReadOnly(readOnly); mLateCancel->setReadOnly(readOnly); if (mDeferChangeButton) { if (readOnly) mDeferChangeButton->hide(); else mDeferChangeButton->show(); } if (mShowInKorganizer) mShowInKorganizer->setReadOnly(readOnly); } /****************************************************************************** * Save the state of all controls. */ void EditAlarmDlg::saveState(const KAEvent* event) { delete mSavedEvent; mSavedEvent = nullptr; if (event) mSavedEvent = new KAEvent(*event); if (mTemplate) { mSavedTemplateName = mTemplateName->text(); mSavedTemplateTimeType = mTemplateTimeGroup->checkedButton(); mSavedTemplateTime = mTemplateTime->time(); mSavedTemplateAfterTime = mTemplateTimeAfter->value(); } checkText(mSavedTextFileCommandMessage, false); if (mTimeWidget) mSavedDateTime = mTimeWidget->getDateTime(nullptr, false, false); mSavedLateCancel = mLateCancel->minutes(); if (mShowInKorganizer) mSavedShowInKorganizer = mShowInKorganizer->isChecked(); mSavedRecurrenceType = mRecurrenceEdit->repeatType(); mSavedDeferTime = mDeferDateTime.kDateTime(); } /****************************************************************************** * Check whether any of the controls has changed state since the dialog was * first displayed. * Reply = true if any non-deferral controls have changed, or if it's a new event. * = false if no non-deferral controls have changed. In this case, * mOnlyDeferred indicates whether deferral controls may have changed. */ bool EditAlarmDlg::stateChanged() const { mChanged = true; mOnlyDeferred = false; if (!mSavedEvent) return true; QString textFileCommandMessage; checkText(textFileCommandMessage, false); if (mTemplate) { if (mSavedTemplateName != mTemplateName->text() || mSavedTemplateTimeType != mTemplateTimeGroup->checkedButton() || (mTemplateUseTime->isChecked() && mSavedTemplateTime != mTemplateTime->time()) || (mTemplateUseTimeAfter->isChecked() && mSavedTemplateAfterTime != mTemplateTimeAfter->value())) return true; } else { const KADateTime dt = mTimeWidget->getDateTime(nullptr, false, false); if (mSavedDateTime.timeSpec() != dt.timeSpec() || mSavedDateTime != dt) return true; } if (mSavedLateCancel != mLateCancel->minutes() || (mShowInKorganizer && mSavedShowInKorganizer != mShowInKorganizer->isChecked()) || textFileCommandMessage != mSavedTextFileCommandMessage || mSavedRecurrenceType != mRecurrenceEdit->repeatType()) return true; if (type_stateChanged()) return true; if (mRecurrenceEdit->stateChanged()) return true; if (mSavedEvent && mSavedEvent->deferred()) mOnlyDeferred = true; mChanged = false; return false; } /****************************************************************************** * Called whenever any of the controls changes state. * Enable or disable the OK button depending on whether any controls have a * different state from their initial state. */ void EditAlarmDlg::contentsChanged() { // Don't do anything if it's a new alarm or we're still initialising // (i.e. mSavedEvent null). if (mSavedEvent && mButtonBox && mButtonBox->button(QDialogButtonBox::Ok)) mButtonBox->button(QDialogButtonBox::Ok)->setEnabled(stateChanged() || mDeferDateTime.kDateTime() != mSavedDeferTime); } /****************************************************************************** * Get the currently entered dialog data. * The data is returned in the supplied KAEvent instance. * Reply = false if the only change has been to an existing deferral. */ bool EditAlarmDlg::getEvent(KAEvent& event, Resource& resource) { resource = mResource; if (mChanged) { // It's a new event, or the edit controls have changed setEvent(event, mAlarmMessage, false); return true; } // Only the deferral time may have changed event = *mSavedEvent; if (mOnlyDeferred) { // Just modify the original event, to avoid expired recurring events // being returned as rubbish. if (mDeferDateTime.isValid()) event.defer(mDeferDateTime, event.reminderDeferral(), false); else event.cancelDefer(); } return false; } /****************************************************************************** * Extract the data in the dialog and set up a KAEvent from it. * If 'trial' is true, the event is set up for a simple one-off test, ignoring * recurrence, reminder, template etc. data. */ void EditAlarmDlg::setEvent(KAEvent& event, const QString& text, bool trial) { KADateTime dt; if (!trial) { if (!mTemplate) dt = mAlarmDateTime.effectiveKDateTime(); else if (mTemplateUseTime->isChecked()) dt = KADateTime(QDate(2000,1,1), mTemplateTime->time()); } int lateCancel = (trial || !mLateCancel->isEnabled()) ? 0 : mLateCancel->minutes(); type_setEvent(event, dt, text, lateCancel, trial); if (!trial) { if (mRecurrenceEdit->repeatType() != RecurrenceEdit::NO_RECUR) { mRecurrenceEdit->updateEvent(event, !mTemplate); const KADateTime now = KADateTime::currentDateTime(mAlarmDateTime.timeSpec()); bool dateOnly = mAlarmDateTime.isDateOnly(); if ((dateOnly && mAlarmDateTime.date() < now.date()) || (!dateOnly && mAlarmDateTime.kDateTime() < now)) { // A timed recurrence has an entered start date which has // already expired, so we must adjust the next repetition. event.setNextOccurrence(now); } mAlarmDateTime = event.startDateTime(); if (mDeferDateTime.isValid() && mDeferDateTime < mAlarmDateTime) { bool deferral = true; bool deferReminder = false; int reminder = mReminder ? mReminder->minutes() : 0; if (reminder) { DateTime remindTime = mAlarmDateTime.addMins(-reminder); if (mDeferDateTime >= remindTime) { if (remindTime > KADateTime::currentUtcDateTime()) deferral = false; // ignore deferral if it's after next reminder else if (mDeferDateTime > remindTime) deferReminder = true; // it's the reminder which is being deferred } } if (deferral) event.defer(mDeferDateTime, deferReminder, false); } } if (mTemplate) { int afterTime = mTemplateDefaultTime->isChecked() ? 0 : mTemplateUseTimeAfter->isChecked() ? mTemplateTimeAfter->value() : -1; event.setTemplate(mTemplateName->text(), afterTime); } } } /****************************************************************************** * Get the currently specified alarm flag bits. */ KAEvent::Flags EditAlarmDlg::getAlarmFlags() const { KAEvent::Flags flags{}; if (mShowInKorganizer && mShowInKorganizer->isEnabled() && mShowInKorganizer->isChecked()) flags |= KAEvent::COPY_KORGANIZER; if (mRecurrenceEdit->repeatType() == RecurrenceEdit::AT_LOGIN) flags |= KAEvent::REPEAT_AT_LOGIN; if (mTemplate ? mTemplateAnyTime->isChecked() : mAlarmDateTime.isDateOnly()) flags |= KAEvent::ANY_TIME; return flags; } /****************************************************************************** * Called when the dialog is displayed. * The first time through, sets the size to the same as the last time it was * displayed. */ void EditAlarmDlg::showEvent(QShowEvent* se) { QDialog::showEvent(se); if (!mDeferGroupHeight) { if (mDeferGroup) mDeferGroupHeight = mDeferGroup->height() + style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing); QSize s; if (Config::readWindowSize(mTemplate ? TEMPLATE_DIALOG_NAME : EDIT_DIALOG_NAME, s)) { bool defer = mDeferGroup && !mDeferGroup->isHidden(); s.setHeight(s.height() + (defer ? mDeferGroupHeight : 0)); if (!defer) mTabScrollGroup->setSized(); resize(s); } } slotResize(); KWindowSystem::setOnDesktop(winId(), mDesktop); // ensure it displays on the desktop expected by the user if (theApp()->needWindowFocusFix()) { QApplication::setActiveWindow(this); QTimer::singleShot(0, this, &EditAlarmDlg::focusFixTimer); } } /****************************************************************************** * Called when the window is first shown, to ensure that it initially becomes * the active window. * This is only required on Ubuntu's Unity desktop, which doesn't transfer * keyboard focus properly between Edit Alarm Dialog windows and MessageWin * windows. */ void EditAlarmDlg::focusFixTimer() { if (theApp()->needWindowFocusFix() && QApplication::focusWidget()->window() != this) { QApplication::setActiveWindow(this); QTimer::singleShot(0, this, &EditAlarmDlg::focusFixTimer); } } /****************************************************************************** * Called to detect when the mouse is pressed anywhere inside the window. * Activates this window if a MessageWin window is also active. * This is only required on Ubuntu's Unity desktop, which doesn't transfer * keyboard focus properly between Edit Alarm Dialog windows and MessageWin * windows. */ bool EditAlarmDlg::eventFilter(QObject*, QEvent* e) { if (theApp()->needWindowFocusFix()) { if (e->type() == QEvent::MouseButtonPress) QApplication::setActiveWindow(this); } return false; } /****************************************************************************** * Called when the dialog is closed. */ void EditAlarmDlg::closeEvent(QCloseEvent* ce) { Q_EMIT rejected(); QDialog::closeEvent(ce); } /****************************************************************************** * Update the tab sizes (again) and if the resized dialog height is greater * than the minimum, resize it again. This is necessary because (a) resizing * tabs doesn't always work properly the first time, and (b) resizing to the * minimum size hint doesn't always work either. */ void EditAlarmDlg::slotResize() { QSize s = mTabScrollGroup->adjustSize(true); s = minimumSizeHint(); if (height() > s.height()) { // Resize to slightly greater than the minimum height. // This is for some unknown reason necessary, since // sometimes resizing to the minimum height fails. resize(s.width(), s.height() + 2); } } /****************************************************************************** * Called when the dialog's size has changed. * Records the new size (adjusted to ignore the optional height of the deferred * time edit widget) in the config file. */ void EditAlarmDlg::resizeEvent(QResizeEvent* re) { if (isVisible() && mDeferGroupHeight) { QSize s = re->size(); s.setHeight(s.height() - (!mDeferGroup || mDeferGroup->isHidden() ? 0 : mDeferGroupHeight)); Config::writeWindowSize(mTemplate ? TEMPLATE_DIALOG_NAME : EDIT_DIALOG_NAME, s); } QDialog::resizeEvent(re); } /****************************************************************************** * Called when any button is clicked. */ void EditAlarmDlg::slotButtonClicked(QAbstractButton* button) { if (button == mTryButton) slotTry(); else if (button == mLoadTemplateButton) slotHelp(); else if (button == mMoreLessButton) slotDefault(); else if (button == mButtonBox->button(QDialogButtonBox::Ok)) { if (validate()) accept(); } else reject(); } /****************************************************************************** * Called when the OK button is clicked. * Validate the input data. */ bool EditAlarmDlg::validate() { if (!stateChanged()) { // No changes have been made except possibly to an existing deferral if (!mOnlyDeferred) reject(); return mOnlyDeferred; } RecurrenceEdit::RepeatType recurType = mRecurrenceEdit->repeatType(); if (mTimeWidget && mTabs->currentIndex() == mRecurPageIndex && recurType == RecurrenceEdit::AT_LOGIN) mTimeWidget->setDateTime(mRecurrenceEdit->endDateTime()); bool timedRecurrence = mRecurrenceEdit->isTimedRepeatType(); // does it recur other than at login? if (mTemplate) { // Check that the template name is not blank and is unique QString errmsg; QString name = mTemplateName->text(); if (name.isEmpty()) errmsg = i18nc("@info", "You must enter a name for the alarm template"); else if (name != mSavedTemplateName) { if (AlarmCalendar::resources()->templateEvent(name)) errmsg = i18nc("@info", "Template name is already in use"); } if (!errmsg.isEmpty()) { mTemplateName->setFocus(); KAMessageBox::sorry(this, errmsg); return false; } } else if (mTimeWidget) { QWidget* errWidget; mAlarmDateTime = mTimeWidget->getDateTime(nullptr, !timedRecurrence, false, &errWidget); if (errWidget) { // It's more than just an existing deferral being changed, so the time matters mTabs->setCurrentIndex(mMainPageIndex); errWidget->setFocus(); mTimeWidget->getDateTime(); // display the error message now return false; } } if (!type_validate(false)) return false; if (!mTemplate) { if (mChanged && mRecurrenceEdit->repeatType() != RecurrenceEdit::NO_RECUR) { // Check whether the start date/time must be adjusted // to match the recurrence specification. DateTime dt = mAlarmDateTime; // setEvent() changes mAlarmDateTime KAEvent event; setEvent(event, mAlarmMessage, false); mAlarmDateTime = dt; // restore KADateTime pre = dt.effectiveKDateTime(); bool dateOnly = dt.isDateOnly(); if (dateOnly) pre = pre.addDays(-1); else pre = pre.addSecs(-1); DateTime next; event.nextOccurrence(pre, next, KAEvent::IGNORE_REPETITION); if (next != dt) { QString prompt = dateOnly ? i18nc("@info The parameter is a date value", "The start date does not match the alarm's recurrence pattern, " "so it will be adjusted to the date of the next recurrence (%1).", QLocale().toString(next.date(), QLocale::ShortFormat)) : i18nc("@info The parameter is a date/time value", "The start date/time does not match the alarm's recurrence pattern, " "so it will be adjusted to the date/time of the next recurrence (%1).", QLocale().toString(next.effectiveDateTime(), QLocale::ShortFormat)); if (KAMessageBox::warningContinueCancel(this, prompt) != KMessageBox::Continue) return false; } } if (timedRecurrence) { KAEvent event; Resource res; getEvent(event, res); // this may adjust mAlarmDateTime const KADateTime now = KADateTime::currentDateTime(mAlarmDateTime.timeSpec()); bool dateOnly = mAlarmDateTime.isDateOnly(); if ((dateOnly && mAlarmDateTime.date() < now.date()) || (!dateOnly && mAlarmDateTime.kDateTime() < now)) { // A timed recurrence has an entered start date which // has already expired, so we must adjust it. if (event.nextOccurrence(now, mAlarmDateTime, KAEvent::ALLOW_FOR_REPETITION) == KAEvent::NO_OCCURRENCE) { KAMessageBox::sorry(this, i18nc("@info", "Recurrence has already expired")); return false; } if (event.workTimeOnly() && !event.nextTrigger(KAEvent::DISPLAY_TRIGGER).isValid()) { if (KAMessageBox::warningContinueCancel(this, i18nc("@info", "The alarm will never occur during working hours")) != KMessageBox::Continue) return false; } } } QString errmsg; QWidget* errWidget = mRecurrenceEdit->checkData(mAlarmDateTime.effectiveKDateTime(), errmsg); if (errWidget) { mTabs->setCurrentIndex(mRecurPageIndex); errWidget->setFocus(); KAMessageBox::sorry(this, errmsg); return false; } } if (recurType != RecurrenceEdit::NO_RECUR) { KAEvent recurEvent; int longestRecurMinutes = -1; int reminder = mReminder ? mReminder->minutes() : 0; if (reminder && !mReminder->isOnceOnly()) { mRecurrenceEdit->updateEvent(recurEvent, false); longestRecurMinutes = recurEvent.longestRecurrenceInterval().asSeconds() / 60; if (longestRecurMinutes && reminder >= longestRecurMinutes) { mTabs->setCurrentIndex(mMainPageIndex); mReminder->setFocusOnCount(); KAMessageBox::sorry(this, xi18nc("@info", "Reminder period must be less than the recurrence interval, unless %1 is checked.", Reminder::i18n_chk_FirstRecurrenceOnly())); return false; } } if (mRecurrenceEdit->subRepetition()) { if (longestRecurMinutes < 0) { mRecurrenceEdit->updateEvent(recurEvent, false); longestRecurMinutes = recurEvent.longestRecurrenceInterval().asSeconds() / 60; } if (longestRecurMinutes > 0 && recurEvent.repetition().intervalMinutes() * recurEvent.repetition().count() >= longestRecurMinutes - reminder) { KAMessageBox::sorry(this, i18nc("@info", "The duration of a repetition within the recurrence must be less than the recurrence interval minus any reminder period")); mRecurrenceEdit->activateSubRepetition(); // display the alarm repetition dialog again return false; } if (!recurEvent.repetition().isDaily() && ((mTemplate && mTemplateAnyTime->isChecked()) || (!mTemplate && mAlarmDateTime.isDateOnly()))) { KAMessageBox::sorry(this, i18nc("@info", "For a repetition within the recurrence, its period must be in units of days or weeks for a date-only alarm")); mRecurrenceEdit->activateSubRepetition(); // display the alarm repetition dialog again return false; } } } if (!checkText(mAlarmMessage)) return false; mResource = Resource(); // mUseResourceEventId = false indicates that the caller already knows // which resource to use. if (mUseResourceEventId) { if (!mResourceEventId.isEmpty()) { mResource = Resources::resourceForEvent(mResourceEventId); if (mResource.isValid()) { CalEvent::Type type = mTemplate ? CalEvent::TEMPLATE : CalEvent::ACTIVE; if (!(mResource.alarmTypes() & type)) mResource = Resource(); // event may have expired while dialog was open } } bool cancelled = false; CalEvent::Type type = mTemplate ? CalEvent::TEMPLATE : CalEvent::ACTIVE; if (!mResource.isWritable(type)) - mResource = Resources::destination(type, this, false, &cancelled); + mResource = Resources::destination(type, this, false, &cancelled); if (!mResource.isValid()) { if (!cancelled) KAMessageBox::sorry(this, i18nc("@info", "You must select a calendar to save the alarm in")); return false; } } return true; } /****************************************************************************** * Called when the Try button is clicked. * Display/execute the alarm immediately for the user to check its configuration. */ void EditAlarmDlg::slotTry() { QString text; if (checkText(text)) { if (!type_validate(true)) return; KAEvent event; setEvent(event, text, true); if (!mNewAlarm && !stateChanged()) { // It's an existing alarm which hasn't been changed yet: // enable KALARM_UID environment variable to be set. event.setEventId(mEventId); } type_aboutToTry(); void* result = theApp()->execAlarm(event, event.firstAlarm(), false, false); type_executedTry(text, result); } } /****************************************************************************** * Called when the Load Template button is clicked. * Prompt to select a template and initialise the dialog with its contents. */ void EditAlarmDlg::slotHelp() { KAEvent::Actions type; switch (mAlarmType) { case KAEvent::FILE: case KAEvent::MESSAGE: type = KAEvent::ACT_DISPLAY; break; case KAEvent::COMMAND: type = KAEvent::ACT_COMMAND; break; case KAEvent::EMAIL: type = KAEvent::ACT_EMAIL; break; case KAEvent::AUDIO: type = KAEvent::ACT_AUDIO; break; default: return; } // Use AutoQPointer to guard against crash on application exit while // the dialogue is still open. It prevents double deletion (both on // deletion of EditAlarmDlg, and on return from this function). AutoQPointer dlg = new TemplatePickDlg(type, this); if (dlg->exec() == QDialog::Accepted) { KAEvent event = dlg->selectedTemplate(); initValues(&event); } } /****************************************************************************** * Called when the More Options or Fewer Options buttons are clicked. * Show/hide the optional options and swap the More/Less buttons, and save the * new setting as the default from now on. */ void EditAlarmDlg::slotDefault() { showOptions(!mShowingMore); KConfigGroup config(KSharedConfig::openConfig(), EDIT_MORE_GROUP); config.writeEntry(EDIT_MORE_KEY, mShowingMore); } /****************************************************************************** * Show/hide the optional options and swap the More/Less buttons. */ void EditAlarmDlg::showOptions(bool more) { qCDebug(KALARM_LOG) << "EditAlarmDlg::showOptions:" << (more ? "More" : "Less"); if (more) { mMoreOptions->show(); mMoreLessButton->setText(i18nc("@action:Button", "Fewer Options <<")); } else { mMoreOptions->hide(); mMoreLessButton->setText(i18nc("@action:button", "More Options >>")); } if (mTimeWidget) mTimeWidget->showMoreOptions(more); type_showOptions(more); mRecurrenceEdit->showMoreOptions(more); mShowingMore = more; QTimer::singleShot(0, this, &EditAlarmDlg::slotResize); } /****************************************************************************** * Called when the Change deferral button is clicked. */ void EditAlarmDlg::slotEditDeferral() { if (!mTimeWidget) return; bool limit = true; const Repetition repetition = mRecurrenceEdit->subRepetition(); DateTime start = mSavedEvent->recurs() ? (mExpiredRecurrence ? DateTime() : mSavedEvent->mainDateTime()) : mTimeWidget->getDateTime(nullptr, !repetition, !mExpiredRecurrence); if (!start.isValid()) { if (!mExpiredRecurrence) return; limit = false; } const KADateTime now = KADateTime::currentUtcDateTime(); if (limit) { if (repetition && start < now) { // Sub-repetition - find the time of the next one int repeatNum = repetition.isDaily() ? (start.daysTo(now) + repetition.intervalDays() - 1) / repetition.intervalDays() : (start.secsTo(now) + repetition.intervalSeconds() - 1) / repetition.intervalSeconds(); if (repeatNum > repetition.count()) { mTimeWidget->getDateTime(); // output the appropriate error message return; } start = KADateTime(repetition.duration(repeatNum).end(start.qDateTime())); } } bool deferred = mDeferDateTime.isValid(); // Use AutoQPointer to guard against crash on application exit while // the dialogue is still open. It prevents double deletion (both on // deletion of EditAlarmDlg, and on return from this function). AutoQPointer deferDlg = new DeferAlarmDlg((deferred ? mDeferDateTime : DateTime(now.addSecs(60).toTimeSpec(start.timeSpec()))), start.isDateOnly(), deferred, this); deferDlg->setObjectName(QStringLiteral("EditDeferDlg")); // used by LikeBack if (limit) { // Don't allow deferral past the next recurrence int reminder = mReminder ? mReminder->minutes() : 0; if (reminder) { DateTime remindTime = start.addMins(-reminder); if (KADateTime::currentUtcDateTime() < remindTime) start = remindTime; } deferDlg->setLimit(start.addSecs(-60)); } if (deferDlg->exec() == QDialog::Accepted) { mDeferDateTime = deferDlg->getDateTime(); mDeferTimeLabel->setText(mDeferDateTime.isValid() ? mDeferDateTime.formatLocale() : QString()); contentsChanged(); } } /****************************************************************************** * Called when the main page is shown. * Sets the focus widget to the first edit field. */ void EditAlarmDlg::slotShowMainPage() { if (!mMainPageShown) { if (mTemplateName) mTemplateName->setFocus(); mMainPageShown = true; } else { // Set scroll position to top, since it otherwise jumps randomly StackedScrollWidget* main = static_cast(mTabs->widget(0)); main->verticalScrollBar()->setValue(0); } if (mTimeWidget) { if (!mReadOnly && mRecurPageShown && mRecurrenceEdit->repeatType() == RecurrenceEdit::AT_LOGIN) mTimeWidget->setDateTime(mRecurrenceEdit->endDateTime()); if (mReadOnly || mRecurrenceEdit->isTimedRepeatType()) mTimeWidget->setMinDateTime(); // don't set a minimum date/time else mTimeWidget->setMinDateTimeIsCurrent(); // set the minimum date/time to track the clock } } /****************************************************************************** * Called when the recurrence edit page is shown. * The recurrence defaults are set to correspond to the start date. * The first time, for a new alarm, the recurrence end date is set according to * the alarm start time. */ void EditAlarmDlg::slotShowRecurrenceEdit() { mRecurPageIndex = mTabs->currentIndex(); if (!mReadOnly && !mTemplate) { mAlarmDateTime = mTimeWidget->getDateTime(nullptr, false, false); const KADateTime now = KADateTime::currentDateTime(mAlarmDateTime.timeSpec()); bool expired = (mAlarmDateTime.effectiveKDateTime() < now); if (mRecurSetDefaultEndDate) { mRecurrenceEdit->setDefaultEndDate(expired ? now.date() : mAlarmDateTime.date()); mRecurSetDefaultEndDate = false; } mRecurrenceEdit->setStartDate(mAlarmDateTime.date(), now.date()); if (mRecurrenceEdit->repeatType() == RecurrenceEdit::AT_LOGIN) mRecurrenceEdit->setEndDateTime(expired ? now : mAlarmDateTime.kDateTime()); } mRecurPageShown = true; } /****************************************************************************** * Called when the recurrence type selection changes. * Enables/disables date-only alarms as appropriate. * Enables/disables controls depending on at-login setting. */ void EditAlarmDlg::slotRecurTypeChange(int repeatType) { bool atLogin = (mRecurrenceEdit->repeatType() == RecurrenceEdit::AT_LOGIN); if (!mTemplate) { bool recurs = (mRecurrenceEdit->repeatType() != RecurrenceEdit::NO_RECUR); if (mDeferGroup) mDeferGroup->setEnabled(recurs); mTimeWidget->enableAnyTime(!recurs || repeatType != RecurrenceEdit::SUBDAILY); if (atLogin) { mAlarmDateTime = mTimeWidget->getDateTime(nullptr, false, false); mRecurrenceEdit->setEndDateTime(mAlarmDateTime.kDateTime()); } if (mReminder) mReminder->enableOnceOnly(recurs && !atLogin); } if (mReminder) mReminder->setAfterOnly(atLogin); mLateCancel->setEnabled(!atLogin); if (mShowInKorganizer) mShowInKorganizer->setEnabled(!atLogin); slotRecurFrequencyChange(); } /****************************************************************************** * Called when the recurrence frequency selection changes, or the sub- * repetition interval changes. * Updates the recurrence frequency text. */ void EditAlarmDlg::slotRecurFrequencyChange() { slotSetSubRepetition(); KAEvent event; mRecurrenceEdit->updateEvent(event, false); mTabs->setTabText(mRecurPageIndex, recurText(event)); } /****************************************************************************** * Called when the Repetition within Recurrence button has been pressed to * display the sub-repetition dialog. * Alarm repetition has the following restrictions: * 1) Not allowed for a repeat-at-login alarm * 2) For a date-only alarm, the repeat interval must be a whole number of days. * 3) The overall repeat duration must be less than the recurrence interval. */ void EditAlarmDlg::slotSetSubRepetition() { bool dateOnly = mTemplate ? mTemplateAnyTime->isChecked() : mTimeWidget->anyTime(); mRecurrenceEdit->setSubRepetition((mReminder ? mReminder->minutes() : 0), dateOnly); } /****************************************************************************** * Called when one of the template time radio buttons is clicked, * to enable or disable the template time entry spin boxes. */ void EditAlarmDlg::slotTemplateTimeType(QAbstractButton*) { mTemplateTime->setEnabled(mTemplateUseTime->isChecked()); mTemplateTimeAfter->setEnabled(mTemplateUseTimeAfter->isChecked()); } /****************************************************************************** * Called when the "Any time" checkbox is toggled in the date/time widget. * Sets the advance reminder and late cancel units to days if any time is checked. */ void EditAlarmDlg::slotAnyTimeToggled(bool anyTime) { if (mReminder && mReminder->isReminder()) mReminder->setDateOnly(anyTime); mLateCancel->setDateOnly(anyTime); } bool EditAlarmDlg::dateOnly() const { return mTimeWidget ? mTimeWidget->anyTime() : mTemplateAnyTime->isChecked(); } bool EditAlarmDlg::isTimedRecurrence() const { return mRecurrenceEdit->isTimedRepeatType(); } void EditAlarmDlg::showMainPage() { mTabs->setCurrentIndex(mMainPageIndex); } #include "moc_editdlg.cpp" #include "moc_editdlg_p.cpp" // vim: et sw=4: diff --git a/src/functions.cpp b/src/functions.cpp index 8532b0c2..272e6106 100644 --- a/src/functions.cpp +++ b/src/functions.cpp @@ -1,1642 +1,1642 @@ /* * functions.cpp - miscellaneous functions * Program: kalarm * Copyright © 2001-2019 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/akonadidatamodel.h" +#include "resources/datamodel.h" #include "resources/resources.h" #include "resources/eventmodel.h" #include "lib/autoqpointer.h" #include "lib/messagebox.h" #include "lib/shellprocess.h" #include "config-kalarm.h" #include "kalarm_debug.h" #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 namespace { bool refreshAlarmsQueued = false; 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 AlarmCalendar* cal = AlarmCalendar::resources(); // 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); + Resource resource = Resources::destination(CalEvent::ACTIVE, msgParent); if (!resource.isValid()) { qCDebug(KALARM_LOG) << "KAlarm::addEvents: No calendar"; status.status = UPDATE_FAILED; } else { AlarmCalendar* cal = AlarmCalendar::resources(); for (int i = 0, end = events.count(); i < end; ++i) { // Save the event details in the calendar file, and get the new event ID 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 AlarmCalendar* cal = AlarmCalendar::resources(); 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 AlarmCalendar* cal = AlarmCalendar::resources(); 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 AlarmCalendar* cal = AlarmCalendar::resources(); if (!cal->modifyEvent(oldId, newEvent)) status.status = UPDATE_FAILED; else { if (!cal->save()) status.status = SAVE_FAILED; if (status.status == UPDATE_OK) { if (newEvent.copyToKOrganizer()) { UpdateResult st = sendToKOrganizer(newEvent); // tell KOrganizer to show the new event status.korgUpdate(st); } // Remove "Don't show error messages again" for the old alarm setDontShowErrors(oldId); } } } if (status.status != UPDATE_OK && msgParent) displayUpdateError(msgParent, ERR_MODIFY, status, showKOrgErr); return status.status; } /****************************************************************************** * Update an active (non-archived) alarm from the calendar file and from every * main window instance. * The new event will have the same event ID as the old one. * The event is not updated in KOrganizer, since this function is called when an * existing alarm is rescheduled (due to recurrence or deferral). */ UpdateResult updateEvent(KAEvent& event, QWidget* msgParent, bool archiveOnDelete) { qCDebug(KALARM_LOG) << "KAlarm::updateEvent:" << event.id(); if (!event.isValid()) deleteEvent(event, archiveOnDelete); else { // Update the event in the calendar file. AlarmCalendar* cal = AlarmCalendar::resources(); cal->updateEvent(event); if (!cal->save()) { if (msgParent) displayUpdateError(msgParent, ERR_ADD, UpdateStatusData(SAVE_FAILED)); return UpdateResult(SAVE_FAILED); } } return UpdateResult(UPDATE_OK); } /****************************************************************************** * Update a template in the calendar file and in every template list view. * If 'selectionView' is non-null, the selection highlight is moved to the * updated event in that listView instance. */ UpdateResult updateTemplate(KAEvent& event, QWidget* msgParent) { AlarmCalendar* cal = AlarmCalendar::resources(); 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(SAVE_FAILED)); return UpdateResult(status); } return UpdateResult(UPDATE_OK); } /****************************************************************************** * Delete alarms from the calendar file and from every main window instance. * If the events are archived, the events' IDs are changed to archived IDs if necessary. */ UpdateResult deleteEvent(KAEvent& event, bool archive, QWidget* msgParent, bool showKOrgErr) { QVector events(1, event); return deleteEvents(events, archive, msgParent, showKOrgErr); } UpdateResult deleteEvents(QVector& events, bool archive, QWidget* msgParent, bool showKOrgErr) { qCDebug(KALARM_LOG) << "KAlarm::deleteEvents:" << events.count(); if (events.isEmpty()) return UpdateResult(UPDATE_OK); UpdateStatusData status; AlarmCalendar* cal = AlarmCalendar::resources(); 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; AlarmCalendar* cal = AlarmCalendar::resources(); for (const KAEvent* event : events) { // Update the window lists // Delete the template from the calendar file AlarmCalendar* cal = AlarmCalendar::resources(); 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; AlarmCalendar* cal = AlarmCalendar::displayCalendarOpen(); if (cal) cal->deleteDisplayEvent(eventID, true); // save calendar after deleting } /****************************************************************************** * Undelete archived alarms, and update every main window instance. * The archive bit is set to ensure that they get re-archived if deleted again. * 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); + 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; AlarmCalendar* cal = AlarmCalendar::resources(); const KADateTime now = KADateTime::currentUtcDateTime(); for (int i = 0, end = events.count(); i < end; ++i) { // Delete the event from the archived resource KAEvent* event = &events[i]; if (event->category() != CalEvent::ARCHIVED || !event->occursAfter(now, true)) { ineligibleIDs += EventId(*event); continue; } ++count; KAEvent newevent(*event); KAEvent* const newev = &newevent; newev->setCategory(CalEvent::ACTIVE); // this changes the event ID if (newev->recurs() || newev->repetition()) newev->setNextOccurrence(now); // skip any recurrences in the past newev->setArchive(); // ensure that it gets re-archived if it is deleted // Save the event details in the calendar file. // This converts the event ID. if (!cal->addEvent(newevent, msgParent, true, &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; AlarmCalendar* cal = AlarmCalendar::resources(); 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 = AlarmCalendar::resources()->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()) AlarmCalendar::resources()->purgeEvents(events); // delete the events and save the calendar } /****************************************************************************** * Display an error message about an error when saving an event. * If 'model' is non-null, the AlarmListModel* which it points to is used; if * that is null, it is created. */ QVector getSortedActiveEvents(QObject* parent, AlarmListModel** model) { AlarmListModel* mdl = nullptr; if (!model) model = &mdl; if (!*model) { - *model = AlarmListModel::create(parent); + *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; } /****************************************************************************** * 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 = AlarmCalendar::resources()->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 && !AlarmCalendar::getEvent(EventId(params[0].toLongLong(), params[1]))) return QStringList(); return params; // config entry is valid } if (!params.isEmpty()) { config.deleteEntry("RtcWake"); // delete the expired config entry config.sync(); } return QStringList(); } /****************************************************************************** * Delete any wake-on-suspend alarm from the config. */ void deleteRtcWakeConfig() { KConfigGroup config(KSharedConfig::openConfig(), "General"); config.deleteEntry("RtcWake"); config.sync(); } /****************************************************************************** * Delete any wake-on-suspend alarm, optionally only for a specified event. */ void cancelRtcWake(QWidget* msgParent, const QString& eventId) { const QStringList wakeup = checkRtcWakeConfig(); if (!wakeup.isEmpty() && (eventId.isEmpty() || wakeup[0] == eventId)) { Private::instance()->mMsgParent = msgParent ? msgParent : MainWindow::mainMainWindow(); QTimer::singleShot(0, Private::instance(), &Private::cancelRtcWake); } } /****************************************************************************** * Delete any wake-on-suspend alarm. */ void Private::cancelRtcWake() { // setRtcWakeTime will only work with a parent window specified setRtcWakeTime(0, mMsgParent); deleteRtcWakeConfig(); KAMessageBox::information(mMsgParent, i18nc("info", "The scheduled Wake from Suspend has been cancelled.")); } /****************************************************************************** * Set the wakeup time for the system. * Set 'triggerTime' to zero to cancel the wakeup. * Reply = true if successful. */ bool setRtcWakeTime(unsigned triggerTime, QWidget* parent) { QVariantMap args; args[QStringLiteral("time")] = triggerTime; KAuth::Action action(QStringLiteral("org.kde.kalarm.rtcwake.settimer")); action.setHelperId(QStringLiteral("org.kde.kalarm.rtcwake")); action.setParentWidget(parent); action.setArguments(args); KAuth::ExecuteJob* job = action.execute(); if (!job->exec()) { QString errmsg = job->errorString(); qCDebug(KALARM_LOG) << "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() || AlarmCalendar::resources()->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 (!AlarmCalendar::resources()->event(id)) { // Event has been deleted while the user was editing the alarm, // so treat it as a new alarm. PrivateNewAlarmDlg().accept(editDlg); return; } KAEvent newEvent; 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 = AlarmCalendar::resources()->event(id, true); if (!event) { if (id.collectionId() != -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 (AlarmCalendar::resources()->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 (AlarmCalendar::resources()->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 (AlarmCalendar::resources()->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 = AlarmCalendar::resources()->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"; AlarmCalendar::resources()->reload(); // Close any message windows for alarms which are now disabled const KAEvent::List events = AlarmCalendar::resources()->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.collectionId()).arg(eventId.eventId()); return group.readEntry(id, QStringList()); } /****************************************************************************** * Check whether the specified Don't-show-again error message tag is set for an * alarm ID. */ bool dontShowErrors(const EventId& eventId, const QString& tag) { if (tag.isEmpty()) return false; 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.collectionId()).arg(eventId.eventId()); if (tags.isEmpty()) group.deleteEntry(id); else group.writeEntry(id, tags); group.sync(); } /****************************************************************************** * Set the specified Don't-show-again error message tag for an alarm ID. * Existing tags are unaffected. */ void setDontShowErrors(const EventId& eventId, const QString& tag) { if (eventId.isEmpty() || tag.isEmpty()) return; KConfig config(QStandardPaths::writableLocation(QStandardPaths::DataLocation) + QLatin1Char('/') + ALARM_OPTS_FILE); KConfigGroup group(&config, DONT_SHOW_ERRORS_GROUP); const QString id = QStringLiteral("%1:%2").arg(eventId.collectionId()).arg(eventId.eventId()); QStringList tags = group.readEntry(id, QStringList()); if (tags.indexOf(tag) < 0) { tags += tag; group.writeEntry(id, tags); group.sync(); } } #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: diff --git a/src/kalarmapp.cpp b/src/kalarmapp.cpp index 78e588e7..aebd3d2b 100644 --- a/src/kalarmapp.cpp +++ b/src/kalarmapp.cpp @@ -1,2513 +1,2513 @@ /* * kalarmapp.cpp - the KAlarm application object * Program: kalarm * Copyright © 2001-2019 David Jarvie * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kalarmapp.h" #include "alarmcalendar.h" #include "alarmtime.h" #include "commandoptions.h" #include "dbushandler.h" #include "editdlgtypes.h" #include "functions.h" #include "kamail.h" #include "mainwindow.h" #include "messagewin.h" #include "kalarmmigrateapplication.h" #include "preferences.h" #include "prefdlg.h" #include "startdaytimer.h" #include "traywindow.h" -#include "resources/akonadidatamodel.h" +#include "resources/datamodel.h" #include "resources/resources.h" #include "resources/eventmodel.h" #include "lib/desktop.h" #include "lib/messagebox.h" #include "lib/shellprocess.h" #include "kalarm_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include static const int AKONADI_TIMEOUT = 30; // timeout (seconds) for Akonadi collections to be populated /****************************************************************************** * Find the maximum number of seconds late which a late-cancel alarm is allowed * to be. This is calculated as the late cancel interval, plus a few seconds * leeway to cater for any timing irregularities. */ static inline int maxLateness(int lateCancel) { static const int LATENESS_LEEWAY = 5; int lc = (lateCancel >= 1) ? (lateCancel - 1)*60 : 0; return LATENESS_LEEWAY + lc; } KAlarmApp* KAlarmApp::mInstance = nullptr; int KAlarmApp::mActiveCount = 0; int KAlarmApp::mFatalError = 0; QString KAlarmApp::mFatalMessage; /****************************************************************************** * Construct the application. */ KAlarmApp::KAlarmApp(int& argc, char** argv) : QApplication(argc, argv) , mDBusHandler(new DBusHandler()) { qCDebug(KALARM_LOG) << "KAlarmApp:"; KAlarmMigrateApplication migrate; migrate.migrate(); #ifndef NDEBUG KAlarm::setTestModeConditions(); #endif setQuitOnLastWindowClosed(false); Preferences::self(); // read KAlarm configuration if (!Preferences::noAutoStart()) { // Strip out any "OnlyShowIn=KDE" list from kalarm.autostart.desktop Preferences::setNoAutoStart(false); // Enable kalarm.autostart.desktop to start KAlarm Preferences::setAutoStart(true); Preferences::self()->save(); } Preferences::connect(SIGNAL(startOfDayChanged(QTime)), this, SLOT(changeStartOfDay())); Preferences::connect(SIGNAL(workTimeChanged(QTime,QTime,QBitArray)), this, SLOT(slotWorkTimeChanged(QTime,QTime,QBitArray))); Preferences::connect(SIGNAL(holidaysChanged(KHolidays::HolidayRegion)), this, SLOT(slotHolidaysChanged(KHolidays::HolidayRegion))); Preferences::connect(SIGNAL(feb29TypeChanged(Feb29Type)), this, SLOT(slotFeb29TypeChanged(Feb29Type))); Preferences::connect(SIGNAL(showInSystemTrayChanged(bool)), this, SLOT(slotShowInSystemTrayChanged())); Preferences::connect(SIGNAL(archivedKeepDaysChanged(int)), this, SLOT(setArchivePurgeDays())); Preferences::connect(SIGNAL(messageFontChanged(QFont)), this, SLOT(slotMessageFontChanged(QFont))); slotFeb29TypeChanged(Preferences::defaultFeb29Type()); KAEvent::setStartOfDay(Preferences::startOfDay()); KAEvent::setWorkTime(Preferences::workDays(), Preferences::workDayStart(), Preferences::workDayEnd()); KAEvent::setHolidays(Preferences::holidays()); KAEvent::setDefaultFont(Preferences::messageFont()); if (initialise()) // initialise calendars and alarm timer { connect(Resources::instance(), &Resources::resourceAdded, this, &KAlarmApp::slotResourceAdded); connect(Resources::instance(), &Resources::resourcePopulated, this, &KAlarmApp::slotResourcePopulated); connect(Resources::instance(), &Resources::resourcePopulated, this, &KAlarmApp::purgeNewArchivedDefault); connect(Resources::instance(), &Resources::resourcesCreated, this, &KAlarmApp::checkWritableCalendar); - connect(AkonadiDataModel::instance(), &AkonadiDataModel::migrationCompleted, - this, &KAlarmApp::checkWritableCalendar); + connect(Resources::instance(), &Resources::migrationCompleted, + this, &KAlarmApp::checkWritableCalendar); KConfigGroup config(KSharedConfig::openConfig(), "General"); mNoSystemTray = config.readEntry("NoSystemTray", false); mOldShowInSystemTray = wantShowInSystemTray(); DateTime::setStartOfDay(Preferences::startOfDay()); mPrefsArchivedColour = Preferences::archivedColour(); } // Check if KOrganizer is installed const QString korg = QStringLiteral("korganizer"); mKOrganizerEnabled = !QStandardPaths::findExecutable(korg).isEmpty(); if (!mKOrganizerEnabled) { qCDebug(KALARM_LOG) << "KAlarmApp: KOrganizer options disabled (KOrganizer not found)"; } // Check if the window manager can't handle keyboard focus transfer between windows mWindowFocusBroken = (Desktop::currentIdentity() == Desktop::Unity); if (mWindowFocusBroken) { qCDebug(KALARM_LOG) << "KAlarmApp: Window keyboard focus broken"; } } /****************************************************************************** */ KAlarmApp::~KAlarmApp() { while (!mCommandProcesses.isEmpty()) { ProcData* pd = mCommandProcesses.at(0); mCommandProcesses.pop_front(); delete pd; } AlarmCalendar::terminateCalendars(); } /****************************************************************************** * Return the one and only KAlarmApp instance. * If it doesn't already exist, it is created first. */ KAlarmApp* KAlarmApp::create(int& argc, char** argv) { if (!mInstance) { mInstance = new KAlarmApp(argc, argv); if (mFatalError) mInstance->quitFatal(); } return mInstance; } /****************************************************************************** * (Re)initialise things which are tidied up/closed by quitIf(). * Reinitialisation can be necessary if session restoration finds nothing to * restore and starts quitting the application, but KAlarm then starts up again * before the application has exited. * Reply = true if calendars were initialised successfully, * false if they were already initialised, or if initialisation failed. */ bool KAlarmApp::initialise() { if (!mAlarmTimer) { mAlarmTimer = new QTimer(this); mAlarmTimer->setSingleShot(true); connect(mAlarmTimer, &QTimer::timeout, this, &KAlarmApp::checkNextDueAlarm); } if (!AlarmCalendar::resources()) { qCDebug(KALARM_LOG) << "KAlarmApp::initialise: initialising calendars"; if (AlarmCalendar::initialiseCalendars()) { connect(AlarmCalendar::resources(), &AlarmCalendar::earliestAlarmChanged, this, &KAlarmApp::checkNextDueAlarm); connect(AlarmCalendar::resources(), &AlarmCalendar::atLoginEventAdded, this, &KAlarmApp::atLoginEventAdded); return true; } } return false; } /****************************************************************************** * Restore the saved session if required. */ bool KAlarmApp::restoreSession() { if (!isSessionRestored()) return false; if (mFatalError) { quitFatal(); return false; } // Process is being restored by session management. qCDebug(KALARM_LOG) << "KAlarmApp::restoreSession: Restoring"; ++mActiveCount; // Create the session config object now. // This is necessary since if initCheck() below causes calendars to be updated, // the session config created after that points to an invalid file, resulting // in no windows being restored followed by a later crash. KConfigGui::sessionConfig(); // When KAlarm is session restored, automatically set start-at-login to true. Preferences::self()->load(); Preferences::setAutoStart(true); Preferences::setNoAutoStart(false); Preferences::setAskAutoStart(true); // cancel any start-at-login prompt suppression Preferences::self()->save(); if (!initCheck(true)) // open the calendar file (needed for main windows), don't process queue yet { --mActiveCount; quitIf(1, true); // error opening the main calendar - quit return false; } MainWindow* trayParent = nullptr; for (int i = 1; KMainWindow::canBeRestored(i); ++i) { const QString type = KMainWindow::classNameOfToplevel(i); if (type == QLatin1String("MainWindow")) { MainWindow* win = MainWindow::create(true); win->restore(i, false); if (win->isHiddenTrayParent()) trayParent = win; else win->show(); } else if (type == QLatin1String("MessageWin")) { MessageWin* win = new MessageWin; win->restore(i, false); if (win->isValid()) { if (Resources::allCreated()) win->show(); } else delete win; } } // Try to display the system tray icon if it is configured to be shown if (trayParent || wantShowInSystemTray()) { if (!MainWindow::count()) qCWarning(KALARM_LOG) << "KAlarmApp::restoreSession: no main window to be restored!?"; else { displayTrayIcon(true, trayParent); // Occasionally for no obvious reason, the main main window is // shown when it should be hidden, so hide it just to be sure. if (trayParent) trayParent->hide(); } } --mActiveCount; if (quitIf(0)) // quit if no windows are open return false; // quitIf() can sometimes return, despite calling exit() startProcessQueue(); // start processing the execution queue return true; } /****************************************************************************** * Called for a unique QApplication when a new instance of the application is * started. * Reply: exit code (>= 0), or -1 to continue execution. * If exit code >= 0, 'outputText' holds text to output before terminating. */ void KAlarmApp::activateByDBus(const QStringList& args, const QString& workingDirectory) { activateInstance(args, workingDirectory, nullptr); } /****************************************************************************** * Called to start a new instance of the application. * Reply: exit code (>= 0), or -1 to continue execution. * If exit code >= 0, 'outputText' holds text to output before terminating. */ int KAlarmApp::activateInstance(const QStringList& args, const QString& workingDirectory, QString* outputText) { Q_UNUSED(workingDirectory) qCDebug(KALARM_LOG) << "KAlarmApp::activateInstance"; if (outputText) outputText->clear(); if (mFatalError) { quitFatal(); return 1; } // The D-Bus call to activate a subsequent instance of KAlarm may not supply // any arguments, but we need one. if (!args.isEmpty() && mActivateArg0.isEmpty()) mActivateArg0 = args[0]; QStringList fixedArgs(args); if (args.isEmpty() && !mActivateArg0.isEmpty()) fixedArgs << mActivateArg0; // Parse and interpret command line arguments. QCommandLineParser parser; KAboutData::applicationData().setupCommandLine(&parser); parser.setApplicationDescription(QApplication::applicationDisplayName()); CommandOptions* options = new CommandOptions; const QStringList newArgs = options->setOptions(&parser, fixedArgs); options->parse(); KAboutData::applicationData().processCommandLine(&parser); ++mActiveCount; int exitCode = 0; // default = success static bool firstInstance = true; bool dontRedisplay = false; CommandOptions::Command command = CommandOptions::NONE; const bool processOptions = (!firstInstance || !isSessionRestored()); if (processOptions) { options->process(); #ifndef NDEBUG if (options->simulationTime().isValid()) KAlarm::setSimulatedSystemTime(options->simulationTime()); #endif command = options->command(); if (options->disableAll()) setAlarmsEnabled(false); // disable alarm monitoring // Handle options which exit with a terminal message, before // making the application a unique application, since a // unique application won't output to the terminal if another // instance is already running. switch (command) { case CommandOptions::CMD_ERROR: if (outputText) { *outputText = options->outputText(); delete options; return 1; } mReadOnly = true; // don't need write access to calendars exitCode = 1; break; case CommandOptions::EXIT: if (outputText) { *outputText = options->outputText(); delete options; return 0; } exitCode = -1; break; default: break; } } // Make this a unique application. KDBusService* s = new KDBusService(KDBusService::Unique, this); connect(this, &KAlarmApp::aboutToQuit, s, &KDBusService::deleteLater); connect(s, &KDBusService::activateRequested, this, &KAlarmApp::activateByDBus); if (processOptions) { switch (command) { case CommandOptions::TRIGGER_EVENT: case CommandOptions::CANCEL_EVENT: { // Display or delete the event with the specified event ID const EventFunc function = (command == CommandOptions::TRIGGER_EVENT) ? EVENT_TRIGGER : EVENT_CANCEL; // Open the calendar, don't start processing execution queue yet, // and wait for the calendar resources to be populated. if (!initCheck(true, true, options->eventId().collectionId())) exitCode = 1; else { startProcessQueue(); // start processing the execution queue dontRedisplay = true; if (!handleEvent(options->eventId(), function, true)) { CommandOptions::printError(xi18nc("@info:shell", "%1: Event %2 not found, or not unique", QStringLiteral("--") + options->commandName(), options->eventId().eventId())); exitCode = 1; } } break; } case CommandOptions::LIST: // Output a list of scheduled alarms to stdout. // Open the calendar, don't start processing execution queue yet, // and wait for all calendar resources to be populated. mReadOnly = true; // don't need write access to calendars mAlarmsEnabled = false; // prevent alarms being processed if (!initCheck(true, true)) exitCode = 1; else { dontRedisplay = true; const QStringList alarms = scheduledAlarmList(); for (const QString& alarm : alarms) std::cout << alarm.toUtf8().constData() << std::endl; } break; case CommandOptions::EDIT: // Edit a specified existing alarm. // Open the calendar and wait for the calendar resources to be populated. if (!initCheck(false, true, options->eventId().collectionId())) exitCode = 1; else if (!KAlarm::editAlarmById(options->eventId())) { CommandOptions::printError(xi18nc("@info:shell", "%1: Event %2 not found, or not editable", QStringLiteral("--") + options->commandName(), options->eventId().eventId())); exitCode = 1; } break; case CommandOptions::EDIT_NEW: { // Edit a new alarm, and optionally preset selected values if (!initCheck()) exitCode = 1; else { EditAlarmDlg* editDlg = EditAlarmDlg::create(false, options->editType(), MainWindow::mainMainWindow()); if (options->alarmTime().isValid()) editDlg->setTime(options->alarmTime()); if (options->recurrence()) editDlg->setRecurrence(*options->recurrence(), options->subRepeatInterval(), options->subRepeatCount()); else if (options->flags() & KAEvent::REPEAT_AT_LOGIN) editDlg->setRepeatAtLogin(); editDlg->setAction(options->editAction(), AlarmText(options->text())); if (options->lateCancel()) editDlg->setLateCancel(options->lateCancel()); if (options->flags() & KAEvent::COPY_KORGANIZER) editDlg->setShowInKOrganizer(true); switch (options->editType()) { case EditAlarmDlg::DISPLAY: { // EditAlarmDlg::create() always returns EditDisplayAlarmDlg for type = DISPLAY EditDisplayAlarmDlg* dlg = qobject_cast(editDlg); if (options->fgColour().isValid()) dlg->setFgColour(options->fgColour()); if (options->bgColour().isValid()) dlg->setBgColour(options->bgColour()); if (!options->audioFile().isEmpty() || options->flags() & (KAEvent::BEEP | KAEvent::SPEAK)) { const KAEvent::Flags flags = options->flags(); const Preferences::SoundType type = (flags & KAEvent::BEEP) ? Preferences::Sound_Beep : (flags & KAEvent::SPEAK) ? Preferences::Sound_Speak : Preferences::Sound_File; dlg->setAudio(type, options->audioFile(), options->audioVolume(), (flags & KAEvent::REPEAT_SOUND ? 0 : -1)); } if (options->reminderMinutes()) dlg->setReminder(options->reminderMinutes(), (options->flags() & KAEvent::REMINDER_ONCE)); if (options->flags() & KAEvent::CONFIRM_ACK) dlg->setConfirmAck(true); if (options->flags() & KAEvent::AUTO_CLOSE) dlg->setAutoClose(true); break; } case EditAlarmDlg::COMMAND: break; case EditAlarmDlg::EMAIL: { // EditAlarmDlg::create() always returns EditEmailAlarmDlg for type = EMAIL EditEmailAlarmDlg* dlg = qobject_cast(editDlg); if (options->fromID() || !options->addressees().isEmpty() || !options->subject().isEmpty() || !options->attachments().isEmpty()) dlg->setEmailFields(options->fromID(), options->addressees(), options->subject(), options->attachments()); if (options->flags() & KAEvent::EMAIL_BCC) dlg->setBcc(true); break; } case EditAlarmDlg::AUDIO: { // EditAlarmDlg::create() always returns EditAudioAlarmDlg for type = AUDIO EditAudioAlarmDlg* dlg = qobject_cast(editDlg); if (!options->audioFile().isEmpty() || options->audioVolume() >= 0) dlg->setAudio(options->audioFile(), options->audioVolume()); break; } case EditAlarmDlg::NO_TYPE: break; } // Execute the edit dialogue. Note that if no other instance of KAlarm is // running, this new instance will not exit after the dialogue is closed. // This is deliberate, since exiting would mean that KAlarm wouldn't // trigger the new alarm. KAlarm::execNewAlarmDlg(editDlg); } break; } case CommandOptions::EDIT_NEW_PRESET: // Edit a new alarm, preset with a template if (!initCheck()) exitCode = 1; else { // Execute the edit dialogue. Note that if no other instance of KAlarm is // running, this new instance will not exit after the dialogue is closed. // This is deliberate, since exiting would mean that KAlarm wouldn't // trigger the new alarm. KAlarm::editNewAlarm(options->templateName()); } break; case CommandOptions::NEW: // Display a message or file, execute a command, or send an email if (!initCheck() || !scheduleEvent(options->editAction(), options->text(), options->alarmTime(), options->lateCancel(), options->flags(), options->bgColour(), options->fgColour(), QFont(), options->audioFile(), options->audioVolume(), options->reminderMinutes(), (options->recurrence() ? *options->recurrence() : KARecurrence()), options->subRepeatInterval(), options->subRepeatCount(), options->fromID(), options->addressees(), options->subject(), options->attachments())) exitCode = 1; break; case CommandOptions::TRAY: // Display only the system tray icon if (Preferences::showInSystemTray() && QSystemTrayIcon::isSystemTrayAvailable()) { if (!initCheck() // open the calendar, start processing execution queue || !displayTrayIcon(true)) exitCode = 1; break; } Q_FALLTHROUGH(); // fall through to NONE case CommandOptions::NONE: // No arguments - run interactively & display the main window #ifndef NDEBUG if (options->simulationTime().isValid() && !firstInstance) break; // simulating time: don't open main window if already running #endif if (!initCheck()) exitCode = 1; else { if (mTrayWindow && mTrayWindow->assocMainWindow() && !mTrayWindow->assocMainWindow()->isVisible()) mTrayWindow->showAssocMainWindow(); else { MainWindow* win = MainWindow::create(); if (command == CommandOptions::TRAY) win->setWindowState(win->windowState() | Qt::WindowMinimized); win->show(); } } break; default: break; } } if (options != CommandOptions::firstInstance()) delete options; // If this is the first time through, redisplay any alarm message windows // from last time. if (firstInstance && !dontRedisplay && !exitCode) { /* First time through, so redisplay alarm message windows from last time. * But it is possible for session restoration in some circumstances to * not create any windows, in which case the alarm calendars will have * been deleted - if so, don't try to do anything. (This has been known * to happen under the Xfce desktop.) */ if (AlarmCalendar::resources()) { if (Resources::allCreated()) { mRedisplayAlarms = false; MessageWin::redisplayAlarms(); } else mRedisplayAlarms = true; } } --mActiveCount; firstInstance = false; // Quit the application if this was the last/only running "instance" of the program. // Executing 'return' doesn't work very well since the program continues to // run if no windows were created. if (quitIf(exitCode >= 0 ? exitCode : 0)) return exitCode; // exit this application instance return -1; // continue executing the application instance } /****************************************************************************** * Quit the program, optionally only if there are no more "instances" running. * Reply = true if program exited. */ bool KAlarmApp::quitIf(int exitCode, bool force) { if (force) { // Quit regardless, except for message windows mQuitting = true; MainWindow::closeAll(); mQuitting = false; displayTrayIcon(false); if (MessageWin::instanceCount(true)) // ignore always-hidden windows (e.g. audio alarms) return false; } else if (mQuitting) return false; // MainWindow::closeAll() causes quitIf() to be called again else { // Quit only if there are no more "instances" running mPendingQuit = false; if (mActiveCount > 0 || MessageWin::instanceCount(true)) // ignore always-hidden windows (e.g. audio alarms) return false; const int mwcount = MainWindow::count(); MainWindow* mw = mwcount ? MainWindow::firstWindow() : nullptr; if (mwcount > 1 || (mwcount && (!mw->isHidden() || !mw->isTrayParent()))) return false; // There are no windows left except perhaps a main window which is a hidden // tray icon parent, or an always-hidden message window. if (mTrayWindow) { // There is a system tray icon. // Don't exit unless the system tray doesn't seem to exist. if (checkSystemTray()) return false; } if (!mActionQueue.isEmpty() || !mCommandProcesses.isEmpty()) { // Don't quit yet if there are outstanding actions on the execution queue mPendingQuit = true; mPendingQuitCode = exitCode; return false; } } // This was the last/only running "instance" of the program, so exit completely. // NOTE: Everything which is terminated/deleted here must where applicable // be initialised in the initialise() method, in case KAlarm is // started again before application exit completes! qCDebug(KALARM_LOG) << "KAlarmApp::quitIf:" << exitCode << ": quitting"; MessageWin::stopAudio(true); if (mCancelRtcWake) { KAlarm::setRtcWakeTime(0, nullptr); KAlarm::deleteRtcWakeConfig(); } delete mAlarmTimer; // prevent checking for alarms after deleting calendars mAlarmTimer = nullptr; mInitialised = false; // prevent processQueue() from running AlarmCalendar::terminateCalendars(); exit(exitCode); return true; // sometimes we actually get to here, despite calling exit() } /****************************************************************************** * Called when the Quit menu item is selected. * Closes the system tray window and all main windows, but does not exit the * program if other windows are still open. */ void KAlarmApp::doQuit(QWidget* parent) { qCDebug(KALARM_LOG) << "KAlarmApp::doQuit"; if (KAMessageBox::warningCancelContinue(parent, i18nc("@info", "Quitting will disable alarms (once any alarm message windows are closed)."), QString(), KStandardGuiItem::quit(), KStandardGuiItem::cancel(), Preferences::QUIT_WARN ) != KMessageBox::Continue) return; if (!KAlarm::checkRtcWakeConfig(true).isEmpty()) { // A wake-on-suspend alarm is set if (KAMessageBox::warningCancelContinue(parent, i18nc("@info", "Quitting will cancel the scheduled Wake from Suspend."), QString(), KStandardGuiItem::quit() ) != KMessageBox::Continue) return; mCancelRtcWake = true; } if (!Preferences::autoStart()) { int option = KMessageBox::No; if (!Preferences::autoStartChangedByUser()) { option = KAMessageBox::questionYesNoCancel(parent, xi18nc("@info", "Do you want to start KAlarm at login?" "(Note that alarms will be disabled if KAlarm is not started.)"), QString(), KStandardGuiItem::yes(), KStandardGuiItem::no(), KStandardGuiItem::cancel(), Preferences::ASK_AUTO_START); } switch (option) { case KMessageBox::Yes: Preferences::setAutoStart(true); Preferences::setNoAutoStart(false); break; case KMessageBox::No: Preferences::setNoAutoStart(true); break; case KMessageBox::Cancel: default: return; } Preferences::self()->save(); } quitIf(0, true); } /****************************************************************************** * Display an error message for a fatal error. Prevent further actions since * the program state is unsafe. */ void KAlarmApp::displayFatalError(const QString& message) { if (!mFatalError) { mFatalError = 1; mFatalMessage = message; if (mInstance) QTimer::singleShot(0, mInstance, &KAlarmApp::quitFatal); } } /****************************************************************************** * Quit the program, once the fatal error message has been acknowledged. */ void KAlarmApp::quitFatal() { switch (mFatalError) { case 0: case 2: return; case 1: mFatalError = 2; KMessageBox::error(nullptr, mFatalMessage); // this is an application modal window mFatalError = 3; Q_FALLTHROUGH(); // fall through to '3' case 3: if (mInstance) mInstance->quitIf(1, true); break; } QTimer::singleShot(1000, this, &KAlarmApp::quitFatal); } /****************************************************************************** * Called by the alarm timer when the next alarm is due. * Also called when the execution queue has finished processing to check for the * next alarm. */ void KAlarmApp::checkNextDueAlarm() { if (!mAlarmsEnabled) return; // Find the first alarm due const KAEvent* nextEvent = AlarmCalendar::resources()->earliestAlarm(); if (!nextEvent) return; // there are no alarms pending const KADateTime nextDt = nextEvent->nextTrigger(KAEvent::ALL_TRIGGER).effectiveKDateTime(); const KADateTime now = KADateTime::currentDateTime(Preferences::timeSpec()); qint64 interval = now.msecsTo(nextDt); qCDebug(KALARM_LOG) << "KAlarmApp::checkNextDueAlarm: now:" << qPrintable(now.toString(QStringLiteral("%Y-%m-%d %H:%M %:Z"))) << ", next:" << qPrintable(nextDt.toString(QStringLiteral("%Y-%m-%d %H:%M %:Z"))) << ", due:" << interval; if (interval <= 0) { // Queue the alarm queueAlarmId(*nextEvent); qCDebug(KALARM_LOG) << "KAlarmApp::checkNextDueAlarm:" << nextEvent->id() << ": due now"; QTimer::singleShot(0, this, &KAlarmApp::processQueue); } else { // No alarm is due yet, so set timer to wake us when it's due. // Check for integer overflow before setting timer. #pragma message("TODO: use hibernation wakeup signal") #ifndef HIBERNATION_SIGNAL /* TODO: REPLACE THIS CODE WHEN A SYSTEM NOTIFICATION SIGNAL BECOMES * AVAILABLE FOR WAKEUP FROM HIBERNATION. * Re-evaluate the next alarm time every minute, in case the * system clock jumps. The most common case when the clock jumps * is when a laptop wakes from hibernation. If timers were left to * run, they would trigger late by the length of time the system * was asleep. */ if (interval > 60000) // 1 minute interval = 60000; #endif ++interval; // ensure we don't trigger just before the minute boundary if (interval > INT_MAX) interval = INT_MAX; qCDebug(KALARM_LOG) << "KAlarmApp::checkNextDueAlarm:" << nextEvent->id() << "wait" << interval/1000 << "seconds"; mAlarmTimer->start(static_cast(interval)); } } /****************************************************************************** * Called by the alarm timer when the next alarm is due. * Also called when the execution queue has finished processing to check for the * next alarm. */ void KAlarmApp::queueAlarmId(const KAEvent& event) { const EventId id(event); for (const ActionQEntry& entry : qAsConst(mActionQueue)) { if (entry.function == EVENT_HANDLE && entry.eventId == id) return; // the alarm is already queued } mActionQueue.enqueue(ActionQEntry(EVENT_HANDLE, id)); } /****************************************************************************** * Start processing the execution queue. */ void KAlarmApp::startProcessQueue() { if (!mInitialised) { qCDebug(KALARM_LOG) << "KAlarmApp::startProcessQueue"; mInitialised = true; QTimer::singleShot(0, this, &KAlarmApp::processQueue); // process anything already queued } } /****************************************************************************** * The main processing loop for KAlarm. * All KAlarm operations involving opening or updating calendar files are called * from this loop to ensure that only one operation is active at any one time. * This precaution is necessary because KAlarm's activities are mostly * asynchronous, being in response to D-Bus calls from other programs or timer * events, any of which can be received in the middle of performing another * operation. If a calendar file is opened or updated while another calendar * operation is in progress, the program has been observed to hang, or the first * calendar call has failed with data loss - clearly unacceptable!! */ void KAlarmApp::processQueue() { if (mInitialised && !mProcessingQueue) { qCDebug(KALARM_LOG) << "KAlarmApp::processQueue"; mProcessingQueue = true; // Refresh alarms if that's been queued KAlarm::refreshAlarmsIfQueued(); // Process queued events while (!mActionQueue.isEmpty()) { ActionQEntry& entry = mActionQueue.head(); if (entry.eventId.isEmpty()) { // It's a new alarm switch (entry.function) { case EVENT_TRIGGER: execAlarm(entry.event, entry.event.firstAlarm(), false); break; case EVENT_HANDLE: KAlarm::addEvent(entry.event, nullptr, nullptr, KAlarm::ALLOW_KORG_UPDATE | KAlarm::NO_RESOURCE_PROMPT); break; case EVENT_CANCEL: break; } } else handleEvent(entry.eventId, entry.function); mActionQueue.dequeue(); } // Purge the default archived alarms resource if it's time to do so if (mPurgeDaysQueued >= 0) { KAlarm::purgeArchive(mPurgeDaysQueued); mPurgeDaysQueued = -1; } // Now that the queue has been processed, quit if a quit was queued if (mPendingQuit) { if (quitIf(mPendingQuitCode)) return; // quitIf() can sometimes return, despite calling exit() } mProcessingQueue = false; // Schedule the application to be woken when the next alarm is due checkNextDueAlarm(); } } /****************************************************************************** * Called when a repeat-at-login alarm has been added externally. * Queues the alarm for triggering. * First, cancel any scheduled reminder or deferral for it, since these will be * superseded by the new at-login trigger. */ void KAlarmApp::atLoginEventAdded(const KAEvent& event) { KAEvent ev = event; if (!cancelReminderAndDeferral(ev)) { if (mAlarmsEnabled) { mActionQueue.enqueue(ActionQEntry(EVENT_HANDLE, EventId(ev))); if (mInitialised) QTimer::singleShot(0, this, &KAlarmApp::processQueue); } } } /****************************************************************************** * Called when the system tray main window is closed. */ void KAlarmApp::removeWindow(TrayWindow*) { mTrayWindow = nullptr; } /****************************************************************************** * Display or close the system tray icon. */ bool KAlarmApp::displayTrayIcon(bool show, MainWindow* parent) { qCDebug(KALARM_LOG) << "KAlarmApp::displayTrayIcon"; static bool creating = false; if (show) { if (!mTrayWindow && !creating) { if (!QSystemTrayIcon::isSystemTrayAvailable()) return false; if (!MainWindow::count()) { // We have to have at least one main window to act // as parent to the system tray icon (even if the // window is hidden). creating = true; // prevent main window constructor from creating an additional tray icon parent = MainWindow::create(); creating = false; } mTrayWindow = new TrayWindow(parent ? parent : MainWindow::firstWindow()); connect(mTrayWindow, &TrayWindow::deleted, this, &KAlarmApp::trayIconToggled); Q_EMIT trayIconToggled(); if (!checkSystemTray()) quitIf(0); // exit the application if there are no open windows } } else { delete mTrayWindow; mTrayWindow = nullptr; } return true; } /****************************************************************************** * Check whether the system tray icon has been housed in the system tray. */ bool KAlarmApp::checkSystemTray() { if (!mTrayWindow) return true; if (QSystemTrayIcon::isSystemTrayAvailable() == mNoSystemTray) { qCDebug(KALARM_LOG) << "KAlarmApp::checkSystemTray: changed ->" << mNoSystemTray; mNoSystemTray = !mNoSystemTray; // Store the new setting in the config file, so that if KAlarm exits it will // restart with the correct default. KConfigGroup config(KSharedConfig::openConfig(), "General"); config.writeEntry("NoSystemTray", mNoSystemTray); config.sync(); // Update other settings slotShowInSystemTrayChanged(); } return !mNoSystemTray; } /****************************************************************************** * Return the main window associated with the system tray icon. */ MainWindow* KAlarmApp::trayMainWindow() const { return mTrayWindow ? mTrayWindow->assocMainWindow() : nullptr; } /****************************************************************************** * Called when the show-in-system-tray preference setting has changed, to show * or hide the system tray icon. */ void KAlarmApp::slotShowInSystemTrayChanged() { const bool newShowInSysTray = wantShowInSystemTray(); if (newShowInSysTray != mOldShowInSystemTray) { // The system tray run mode has changed ++mActiveCount; // prevent the application from quitting MainWindow* win = mTrayWindow ? mTrayWindow->assocMainWindow() : nullptr; delete mTrayWindow; // remove the system tray icon if it is currently shown mTrayWindow = nullptr; mOldShowInSystemTray = newShowInSysTray; if (newShowInSysTray) { // Show the system tray icon displayTrayIcon(true); } else { // Stop showing the system tray icon if (win && win->isHidden()) { if (MainWindow::count() > 1) delete win; else { win->setWindowState(win->windowState() | Qt::WindowMinimized); win->show(); } } } --mActiveCount; } } /****************************************************************************** * Called when the start-of-day time preference setting has changed. * Change alarm times for date-only alarms. */ void KAlarmApp::changeStartOfDay() { DateTime::setStartOfDay(Preferences::startOfDay()); KAEvent::setStartOfDay(Preferences::startOfDay()); AlarmCalendar::resources()->adjustStartOfDay(); } /****************************************************************************** * Called when the default alarm message font preference setting has changed. * Notify KAEvent. */ void KAlarmApp::slotMessageFontChanged(const QFont& font) { KAEvent::setDefaultFont(font); } /****************************************************************************** * Called when the working time preference settings have changed. * Notify KAEvent. */ void KAlarmApp::slotWorkTimeChanged(const QTime& start, const QTime& end, const QBitArray& days) { KAEvent::setWorkTime(days, start, end); } /****************************************************************************** * Called when the holiday region preference setting has changed. * Notify KAEvent. */ void KAlarmApp::slotHolidaysChanged(const KHolidays::HolidayRegion& holidays) { KAEvent::setHolidays(holidays); } /****************************************************************************** * Called when the date for February 29th recurrences has changed in the * preferences settings. */ void KAlarmApp::slotFeb29TypeChanged(Preferences::Feb29Type type) { KARecurrence::Feb29Type rtype; switch (type) { default: case Preferences::Feb29_None: rtype = KARecurrence::Feb29_None; break; case Preferences::Feb29_Feb28: rtype = KARecurrence::Feb29_Feb28; break; case Preferences::Feb29_Mar1: rtype = KARecurrence::Feb29_Mar1; break; } KARecurrence::setDefaultFeb29Type(rtype); } /****************************************************************************** * Return whether the program is configured to be running in the system tray. */ bool KAlarmApp::wantShowInSystemTray() const { return Preferences::showInSystemTray() && QSystemTrayIcon::isSystemTrayAvailable(); } /****************************************************************************** * Called when all calendars have been fetched at startup. * Check whether there are any writable active calendars, and if not, warn the * user. */ void KAlarmApp::checkWritableCalendar() { if (mReadOnly) return; // don't need write access to calendars const bool treeFetched = Resources::allCreated(); if (treeFetched && mRedisplayAlarms) { mRedisplayAlarms = false; MessageWin::redisplayAlarms(); } if (!treeFetched - || !AkonadiDataModel::instance()->isMigrationComplete()) + || !DataModel::isMigrationComplete()) return; static bool done = false; if (done) return; done = true; qCDebug(KALARM_LOG) << "KAlarmApp::checkWritableCalendar"; // Check for, and remove, any duplicate Akonadi resources, i.e. those which // use the same calendar file/directory. - AkonadiResource::removeDuplicateResources(); + DataModel::removeDuplicateResources(); // Find whether there are any writable active alarm calendars const bool active = !Resources::enabledResources(CalEvent::ACTIVE, true).isEmpty(); if (!active) { qCWarning(KALARM_LOG) << "KAlarmApp::checkWritableCalendar: No writable active calendar"; KAMessageBox::information(MainWindow::mainMainWindow(), xi18nc("@info", "Alarms cannot be created or updated, because no writable active alarm calendar is enabled." "To fix this, use View | Show Calendars to check or change calendar statuses."), QString(), QStringLiteral("noWritableCal")); } } /****************************************************************************** * Called when a new resource has been added, to note the possible need to purge * its old alarms if it is the default archived calendar. */ void KAlarmApp::slotResourceAdded(const Resource& resource) { if (resource.alarmTypes() & CalEvent::ARCHIVED) mPendingPurges += resource.id(); } /****************************************************************************** * Called when a resource has been populated, to purge its old alarms if it is * the default archived calendar. */ void KAlarmApp::slotResourcePopulated(const Resource& resource) { if (mPendingPurges.removeAll(resource.id()) > 0) purgeNewArchivedDefault(resource); } /****************************************************************************** * Called when a new resource has been populated, or when a resource has been * set as the standard resource for its type. * If it is the default archived calendar, purge its old alarms if necessary. */ void KAlarmApp::purgeNewArchivedDefault(const Resource& resource) { if (Resources::isStandard(resource, CalEvent::ARCHIVED)) { qCDebug(KALARM_LOG) << "KAlarmApp::purgeNewArchivedDefault:" << resource.id() << ": standard archived..."; if (mArchivedPurgeDays >= 0) purge(mArchivedPurgeDays); else setArchivePurgeDays(); } } /****************************************************************************** * Called when the length of time to keep archived alarms changes in KAlarm's * preferences. * Set the number of days to keep archived alarms. * Alarms which are older are purged immediately, and at the start of each day. */ void KAlarmApp::setArchivePurgeDays() { const int newDays = Preferences::archivedKeepDays(); if (newDays != mArchivedPurgeDays) { const int oldDays = mArchivedPurgeDays; mArchivedPurgeDays = newDays; if (mArchivedPurgeDays <= 0) StartOfDayTimer::disconnect(this); if (mArchivedPurgeDays < 0) return; // keep indefinitely, so don't purge if (oldDays < 0 || mArchivedPurgeDays < oldDays) { // Alarms are now being kept for less long, so purge them purge(mArchivedPurgeDays); if (!mArchivedPurgeDays) return; // don't archive any alarms } // Start the purge timer to expire at the start of the next day // (using the user-defined start-of-day time). StartOfDayTimer::connect(this, SLOT(slotPurge())); } } /****************************************************************************** * Purge all archived events from the calendar whose end time is longer ago than * 'daysToKeep'. All events are deleted if 'daysToKeep' is zero. */ void KAlarmApp::purge(int daysToKeep) { if (mPurgeDaysQueued < 0 || daysToKeep < mPurgeDaysQueued) mPurgeDaysQueued = daysToKeep; // Do the purge once any other current operations are completed processQueue(); } /****************************************************************************** * Output a list of pending alarms, with their next scheduled occurrence. */ QStringList KAlarmApp::scheduledAlarmList() { QStringList alarms; const QVector events = KAlarm::getSortedActiveEvents(this); for (const KAEvent& event : events) { const KADateTime dateTime = event.nextTrigger(KAEvent::DISPLAY_TRIGGER).effectiveKDateTime().toLocalZone(); const Resource resource = Resources::resource(event.resourceId()); QString text(resource.configName() + QLatin1String(":")); text += event.id() + QLatin1Char(' ') + dateTime.toString(QStringLiteral("%Y%m%dT%H%M ")) + AlarmText::summary(event, 1); alarms << text; } return alarms; } /****************************************************************************** * Enable or disable alarm monitoring. */ void KAlarmApp::setAlarmsEnabled(bool enabled) { if (enabled != mAlarmsEnabled) { mAlarmsEnabled = enabled; Q_EMIT alarmEnabledToggled(enabled); if (!enabled) KAlarm::cancelRtcWake(nullptr); else if (!mProcessingQueue) checkNextDueAlarm(); } } /****************************************************************************** * Spread or collect alarm message and error message windows. */ void KAlarmApp::spreadWindows(bool spread) { spread = MessageWin::spread(spread); Q_EMIT spreadWindowsToggled(spread); } /****************************************************************************** * Called when the spread status of message windows changes. * Set the 'spread windows' action state. */ void KAlarmApp::setSpreadWindowsState(bool spread) { Q_EMIT spreadWindowsToggled(spread); } /****************************************************************************** * Check whether the window manager's handling of keyboard focus transfer * between application windows is broken. This is true for Ubuntu's Unity * desktop, where MessageWin windows steal keyboard focus from EditAlarmDlg * windows. */ bool KAlarmApp::windowFocusBroken() const { return mWindowFocusBroken; } /****************************************************************************** * Check whether window/keyboard focus currently needs to be fixed manually due * to the window manager not handling it correctly. This will occur if there are * both EditAlarmDlg and MessageWin windows currently active. */ bool KAlarmApp::needWindowFocusFix() const { return mWindowFocusBroken && MessageWin::instanceCount(true) && EditAlarmDlg::instanceCount(); } /****************************************************************************** * Called to schedule a new alarm, either in response to a DCOP notification or * to command line options. * Reply = true unless there was a parameter error or an error opening calendar file. */ bool KAlarmApp::scheduleEvent(KAEvent::SubAction action, const QString& text, const KADateTime& dateTime, int lateCancel, KAEvent::Flags flags, const QColor& bg, const QColor& fg, const QFont& font, const QString& audioFile, float audioVolume, int reminderMinutes, const KARecurrence& recurrence, const KCalendarCore::Duration& repeatInterval, int repeatCount, uint mailFromID, const KCalendarCore::Person::List& mailAddresses, const QString& mailSubject, const QStringList& mailAttachments) { qCDebug(KALARM_LOG) << "KAlarmApp::scheduleEvent:" << text; if (!dateTime.isValid()) return false; const KADateTime now = KADateTime::currentUtcDateTime(); if (lateCancel && dateTime < now.addSecs(-maxLateness(lateCancel))) return true; // alarm time was already archived too long ago KADateTime alarmTime = dateTime; // Round down to the nearest minute to avoid scheduling being messed up if (!dateTime.isDateOnly()) alarmTime.setTime(QTime(alarmTime.time().hour(), alarmTime.time().minute(), 0)); KAEvent event(alarmTime, text, bg, fg, font, action, lateCancel, flags, true); if (reminderMinutes) { const bool onceOnly = flags & KAEvent::REMINDER_ONCE; event.setReminder(reminderMinutes, onceOnly); } if (!audioFile.isEmpty()) event.setAudioFile(audioFile, audioVolume, -1, 0, (flags & KAEvent::REPEAT_SOUND) ? 0 : -1); if (!mailAddresses.isEmpty()) event.setEmail(mailFromID, mailAddresses, mailSubject, mailAttachments); event.setRecurrence(recurrence); event.setFirstRecurrence(); event.setRepetition(Repetition(repeatInterval, repeatCount - 1)); event.endChanges(); if (alarmTime <= now) { // Alarm is due for display already. // First execute it once without adding it to the calendar file. if (!mInitialised) mActionQueue.enqueue(ActionQEntry(event, EVENT_TRIGGER)); else execAlarm(event, event.firstAlarm(), false); // If it's a recurring alarm, reschedule it for its next occurrence if (!event.recurs() || event.setNextOccurrence(now) == KAEvent::NO_OCCURRENCE) return true; // It has recurrences in the future } // Queue the alarm for insertion into the calendar file mActionQueue.enqueue(ActionQEntry(event)); if (mInitialised) QTimer::singleShot(0, this, &KAlarmApp::processQueue); return true; } /****************************************************************************** * Called in response to a D-Bus request to trigger or cancel an event. * Optionally display the event. Delete the event from the calendar file and * from every main window instance. */ bool KAlarmApp::dbusHandleEvent(const EventId& eventID, EventFunc function) { qCDebug(KALARM_LOG) << "KAlarmApp::dbusHandleEvent:" << eventID; mActionQueue.append(ActionQEntry(function, eventID)); if (mInitialised) QTimer::singleShot(0, this, &KAlarmApp::processQueue); return true; } /****************************************************************************** * Called in response to a D-Bus request to list all pending alarms. */ QString KAlarmApp::dbusList() { qCDebug(KALARM_LOG) << "KAlarmApp::dbusList"; return scheduledAlarmList().join(QLatin1Char('\n')) + QLatin1Char('\n'); } /****************************************************************************** * Either: * a) Display the event and then delete it if it has no outstanding repetitions. * b) Delete the event. * c) Reschedule the event for its next repetition. If none remain, delete it. * If the event is deleted, it is removed from the calendar file and from every * main window instance. * Reply = false if event ID not found, or if more than one event with the same * ID is found. */ bool KAlarmApp::handleEvent(const EventId& id, EventFunc function, bool checkDuplicates) { // Delete any expired wake-on-suspend config data KAlarm::checkRtcWakeConfig(); const QString eventID(id.eventId()); KAEvent* event = AlarmCalendar::resources()->event(id, checkDuplicates); if (!event) { if (id.collectionId() != -1) qCWarning(KALARM_LOG) << "KAlarmApp::handleEvent: Event ID not found, or duplicated:" << eventID; else qCWarning(KALARM_LOG) << "KAlarmApp::handleEvent: Event ID not found:" << eventID; return false; } switch (function) { case EVENT_CANCEL: qCDebug(KALARM_LOG) << "KAlarmApp::handleEvent:" << eventID << ", CANCEL"; KAlarm::deleteEvent(*event, true); break; case EVENT_TRIGGER: // handle it if it's due, else execute it regardless case EVENT_HANDLE: // handle it if it's due { const KADateTime now = KADateTime::currentUtcDateTime(); qCDebug(KALARM_LOG) << "KAlarmApp::handleEvent:" << eventID << "," << (function==EVENT_TRIGGER?"TRIGGER:":"HANDLE:") << qPrintable(now.qDateTime().toString(QStringLiteral("yyyy-MM-dd hh:mm"))) << "UTC"; bool updateCalAndDisplay = false; bool alarmToExecuteValid = false; KAAlarm alarmToExecute; bool restart = false; // Check all the alarms in turn. // Note that the main alarm is fetched before any other alarms. for (KAAlarm alarm = event->firstAlarm(); alarm.isValid(); alarm = (restart ? event->firstAlarm() : event->nextAlarm(alarm)), restart = false) { // Check if the alarm is due yet. const KADateTime nextDT = alarm.dateTime(true).effectiveKDateTime(); const int secs = nextDT.secsTo(now); if (secs < 0) { // The alarm appears to be in the future. // Check if it's an invalid local time during a daylight // saving time shift, which has actually passed. if (alarm.dateTime().timeSpec() != KADateTime::LocalZone || nextDT > now.toTimeSpec(KADateTime::LocalZone)) { // This alarm is definitely not due yet qCDebug(KALARM_LOG) << "KAlarmApp::handleEvent: Alarm" << alarm.type() << "at" << nextDT.qDateTime() << ": not due"; continue; } } bool reschedule = false; bool rescheduleWork = false; if ((event->workTimeOnly() || event->holidaysExcluded()) && !alarm.deferred()) { // The alarm is restricted to working hours and/or non-holidays // (apart from deferrals). This needs to be re-evaluated every // time it triggers, since working hours could change. if (alarm.dateTime().isDateOnly()) { KADateTime dt(nextDT); dt.setDateOnly(true); reschedule = !event->isWorkingTime(dt); } else reschedule = !event->isWorkingTime(nextDT); rescheduleWork = reschedule; if (reschedule) qCDebug(KALARM_LOG) << "KAlarmApp::handleEvent: Alarm" << alarm.type() << "at" << nextDT.qDateTime() << ": not during working hours"; } if (!reschedule && alarm.repeatAtLogin()) { // Alarm is to be displayed at every login. qCDebug(KALARM_LOG) << "KAlarmApp::handleEvent: REPEAT_AT_LOGIN"; // Check if the main alarm is already being displayed. // (We don't want to display both at the same time.) if (alarmToExecute.isValid()) continue; // Set the time to display if it's a display alarm alarm.setTime(now); } if (!reschedule && event->lateCancel()) { // Alarm is due, and it is to be cancelled if too late. qCDebug(KALARM_LOG) << "KAlarmApp::handleEvent: LATE_CANCEL"; bool cancel = false; if (alarm.dateTime().isDateOnly()) { // The alarm has no time, so cancel it if its date is too far past const int maxlate = event->lateCancel() / 1440; // maximum lateness in days KADateTime limit(DateTime(nextDT.addDays(maxlate + 1)).effectiveKDateTime()); if (now >= limit) { // It's too late to display the scheduled occurrence. // Find the last previous occurrence of the alarm. DateTime next; const KAEvent::OccurType type = event->previousOccurrence(now, next, true); switch (type & ~KAEvent::OCCURRENCE_REPEAT) { case KAEvent::FIRST_OR_ONLY_OCCURRENCE: case KAEvent::RECURRENCE_DATE: case KAEvent::RECURRENCE_DATE_TIME: case KAEvent::LAST_RECURRENCE: limit.setDate(next.date().addDays(maxlate + 1)); if (now >= limit) { if (type == KAEvent::LAST_RECURRENCE || (type == KAEvent::FIRST_OR_ONLY_OCCURRENCE && !event->recurs())) cancel = true; // last occurrence (and there are no repetitions) else reschedule = true; } break; case KAEvent::NO_OCCURRENCE: default: reschedule = true; break; } } } else { // The alarm is timed. Allow it to be the permitted amount late before cancelling it. const int maxlate = maxLateness(event->lateCancel()); if (secs > maxlate) { // It's over the maximum interval late. // Find the most recent occurrence of the alarm. DateTime next; const KAEvent::OccurType type = event->previousOccurrence(now, next, true); switch (type & ~KAEvent::OCCURRENCE_REPEAT) { case KAEvent::FIRST_OR_ONLY_OCCURRENCE: case KAEvent::RECURRENCE_DATE: case KAEvent::RECURRENCE_DATE_TIME: case KAEvent::LAST_RECURRENCE: if (next.effectiveKDateTime().secsTo(now) > maxlate) { if (type == KAEvent::LAST_RECURRENCE || (type == KAEvent::FIRST_OR_ONLY_OCCURRENCE && !event->recurs())) cancel = true; // last occurrence (and there are no repetitions) else reschedule = true; } break; case KAEvent::NO_OCCURRENCE: default: reschedule = true; break; } } } if (cancel) { // All recurrences are finished, so cancel the event event->setArchive(); if (cancelAlarm(*event, alarm.type(), false)) return true; // event has been deleted updateCalAndDisplay = true; continue; } } if (reschedule) { // The latest repetition was too long ago, so schedule the next one switch (rescheduleAlarm(*event, alarm, false, (rescheduleWork ? nextDT : KADateTime()))) { case 1: // A working-time-only alarm has been rescheduled and the // rescheduled time is already due. Start processing the // event again. alarmToExecuteValid = false; restart = true; break; case -1: return true; // event has been deleted default: break; } updateCalAndDisplay = true; continue; } if (!alarmToExecuteValid) { qCDebug(KALARM_LOG) << "KAlarmApp::handleEvent: Alarm" << alarm.type() << ": execute"; alarmToExecute = alarm; // note the alarm to be displayed alarmToExecuteValid = true; // only trigger one alarm for the event } else qCDebug(KALARM_LOG) << "KAlarmApp::handleEvent: Alarm" << alarm.type() << ": skip"; } // If there is an alarm to execute, do this last after rescheduling/cancelling // any others. This ensures that the updated event is only saved once to the calendar. if (alarmToExecute.isValid()) execAlarm(*event, alarmToExecute, true, !alarmToExecute.repeatAtLogin()); else { if (function == EVENT_TRIGGER) { // The alarm is to be executed regardless of whether it's due. // Only trigger one alarm from the event - we don't want multiple // identical messages, for example. const KAAlarm alarm = event->firstAlarm(); if (alarm.isValid()) execAlarm(*event, alarm, false); } if (updateCalAndDisplay) KAlarm::updateEvent(*event); // update the window lists and calendar file else if (function != EVENT_TRIGGER) { qCDebug(KALARM_LOG) << "KAlarmApp::handleEvent: No action"; } } break; } } return true; } /****************************************************************************** * Called when an alarm action has completed, to perform any post-alarm actions. */ void KAlarmApp::alarmCompleted(const KAEvent& event) { if (!event.postAction().isEmpty()) { // doShellCommand() will error if the user is not authorised to run // shell commands. const QString command = event.postAction(); qCDebug(KALARM_LOG) << "KAlarmApp::alarmCompleted:" << event.id() << ":" << command; doShellCommand(command, event, nullptr, ProcData::POST_ACTION); } } /****************************************************************************** * Reschedule the alarm for its next recurrence after now. If none remain, * delete it. If the alarm is deleted and it is the last alarm for its event, * the event is removed from the calendar file and from every main window * instance. * If 'nextDt' is valid, the event is rescheduled for the next non-working * time occurrence after that. * Reply = 1 if 'nextDt' is valid and the rescheduled event is already due * = -1 if the event has been deleted * = 0 otherwise. */ int KAlarmApp::rescheduleAlarm(KAEvent& event, const KAAlarm& alarm, bool updateCalAndDisplay, const KADateTime& nextDt) { qCDebug(KALARM_LOG) << "KAlarmApp::rescheduleAlarm: Alarm type:" << alarm.type(); int reply = 0; bool update = false; event.startChanges(); if (alarm.repeatAtLogin()) { // Leave an alarm which repeats at every login until its main alarm triggers if (!event.reminderActive() && event.reminderMinutes() < 0) { // Executing an at-login alarm: first schedule the reminder // which occurs AFTER the main alarm. event.activateReminderAfter(KADateTime::currentUtcDateTime()); } // Repeat-at-login alarms are usually unchanged after triggering. // Ensure that the archive flag (which was set in execAlarm()) is saved. update = true; } else if (alarm.isReminder() || alarm.deferred()) { // It's a reminder alarm or an extra deferred alarm, so delete it event.removeExpiredAlarm(alarm.type()); update = true; } else { // Reschedule the alarm for its next occurrence. bool cancelled = false; DateTime last = event.mainDateTime(false); // note this trigger time if (last != event.mainDateTime(true)) last = DateTime(); // but ignore sub-repetition triggers bool next = nextDt.isValid(); KADateTime next_dt = nextDt; const KADateTime now = KADateTime::currentUtcDateTime(); do { const KAEvent::OccurType type = event.setNextOccurrence(next ? next_dt : now); switch (type) { case KAEvent::NO_OCCURRENCE: // All repetitions are finished, so cancel the event qCDebug(KALARM_LOG) << "KAlarmApp::rescheduleAlarm: No occurrence"; if (event.reminderMinutes() < 0 && last.isValid() && alarm.type() != KAAlarm::AT_LOGIN_ALARM && !event.mainExpired()) { // Set the reminder which is now due after the last main alarm trigger. // Note that at-login reminders are scheduled in execAlarm(). event.activateReminderAfter(last); updateCalAndDisplay = true; } if (cancelAlarm(event, alarm.type(), updateCalAndDisplay)) return -1; break; default: if (!(type & KAEvent::OCCURRENCE_REPEAT)) break; // Next occurrence is a repeat, so fall through to recurrence handling Q_FALLTHROUGH(); case KAEvent::RECURRENCE_DATE: case KAEvent::RECURRENCE_DATE_TIME: case KAEvent::LAST_RECURRENCE: // The event is due by now and repetitions still remain, so rewrite the event if (updateCalAndDisplay) update = true; break; case KAEvent::FIRST_OR_ONLY_OCCURRENCE: // The first occurrence is still due?!?, so don't do anything break; } if (cancelled) break; if (event.deferred()) { // Just in case there's also a deferred alarm, ensure it's removed event.removeExpiredAlarm(KAAlarm::DEFERRED_ALARM); update = true; } if (next) { // The alarm is restricted to working hours and/or non-holidays. // Check if the calculated next time is valid. next_dt = event.mainDateTime(true).effectiveKDateTime(); if (event.mainDateTime(false).isDateOnly()) { KADateTime dt(next_dt); dt.setDateOnly(true); next = !event.isWorkingTime(dt); } else next = !event.isWorkingTime(next_dt); } } while (next && next_dt <= now); reply = (!cancelled && next_dt.isValid() && (next_dt <= now)) ? 1 : 0; if (event.reminderMinutes() < 0 && last.isValid() && alarm.type() != KAAlarm::AT_LOGIN_ALARM) { // Set the reminder which is now due after the last main alarm trigger. // Note that at-login reminders are scheduled in execAlarm(). event.activateReminderAfter(last); } } event.endChanges(); if (update) KAlarm::updateEvent(event); // update the window lists and calendar file return reply; } /****************************************************************************** * Delete the alarm. If it is the last alarm for its event, the event is removed * from the calendar file and from every main window instance. * Reply = true if event has been deleted. */ bool KAlarmApp::cancelAlarm(KAEvent& event, KAAlarm::Type alarmType, bool updateCalAndDisplay) { qCDebug(KALARM_LOG) << "KAlarmApp::cancelAlarm"; if (alarmType == KAAlarm::MAIN_ALARM && !event.displaying() && event.toBeArchived()) { // The event is being deleted. Save it in the archived resources first. KAEvent ev(event); KAlarm::addArchivedEvent(ev); } event.removeExpiredAlarm(alarmType); if (!event.alarmCount()) { // If it's a command alarm being executed, mark it as deleted ProcData* pd = findCommandProcess(event.id()); if (pd) pd->eventDeleted = true; // Delete it KAlarm::deleteEvent(event, false); return true; } if (updateCalAndDisplay) KAlarm::updateEvent(event); // update the window lists and calendar file return false; } /****************************************************************************** * Cancel any reminder or deferred alarms in an repeat-at-login event. * This should be called when the event is first loaded. * If there are no more alarms left in the event, the event is removed from the * calendar file and from every main window instance. * Reply = true if event has been deleted. */ bool KAlarmApp::cancelReminderAndDeferral(KAEvent& event) { return cancelAlarm(event, KAAlarm::REMINDER_ALARM, false) || cancelAlarm(event, KAAlarm::DEFERRED_REMINDER_ALARM, false) || cancelAlarm(event, KAAlarm::DEFERRED_ALARM, true); } /****************************************************************************** * Execute an alarm by displaying its message or file, or executing its command. * Reply = ShellProcess instance if a command alarm * = MessageWin if an audio alarm * != 0 if successful * = -1 if execution has not completed * = 0 if the alarm is disabled, or if an error message was output. */ void* KAlarmApp::execAlarm(KAEvent& event, const KAAlarm& alarm, bool reschedule, bool allowDefer, bool noPreAction) { if (!mAlarmsEnabled || !event.enabled()) { // The event (or all events) is disabled qCDebug(KALARM_LOG) << "KAlarmApp::execAlarm:" << event.id() << ": disabled"; if (reschedule) rescheduleAlarm(event, alarm, true); return nullptr; } void* result = (void*)1; event.setArchive(); switch (alarm.action()) { case KAAlarm::COMMAND: if (!event.commandDisplay()) { // execCommandAlarm() will error if the user is not authorised // to run shell commands. result = execCommandAlarm(event, alarm); if (reschedule) rescheduleAlarm(event, alarm, true); break; } Q_FALLTHROUGH(); // fall through to MESSAGE case KAAlarm::MESSAGE: case KAAlarm::FILE: { // Display a message, file or command output, provided that the same event // isn't already being displayed MessageWin* win = MessageWin::findEvent(EventId(event)); // Find if we're changing a reminder message to the real message const bool reminder = (alarm.type() & KAAlarm::REMINDER_ALARM); const bool replaceReminder = !reminder && win && (win->alarmType() & KAAlarm::REMINDER_ALARM); if (!reminder && (!event.deferred() || (event.extraActionOptions() & KAEvent::ExecPreActOnDeferral)) && (replaceReminder || !win) && !noPreAction && !event.preAction().isEmpty()) { // It's not a reminder alarm, and it's not a deferred alarm unless the // pre-alarm action applies to deferred alarms, and there is no message // window (other than a reminder window) currently displayed for this // alarm, and we need to execute a command before displaying the new window. // // NOTE: The pre-action is not executed for a recurring alarm if an // alarm message window for a previous occurrence is still visible. // Check whether the command is already being executed for this alarm. for (const ProcData* pd : qAsConst(mCommandProcesses)) { if (pd->event->id() == event.id() && (pd->flags & ProcData::PRE_ACTION)) { qCDebug(KALARM_LOG) << "KAlarmApp::execAlarm: Already executing pre-DISPLAY command"; return pd->process; // already executing - don't duplicate the action } } // doShellCommand() will error if the user is not authorised to run // shell commands. const QString command = event.preAction(); qCDebug(KALARM_LOG) << "KAlarmApp::execAlarm: Pre-DISPLAY command:" << command; const int flags = (reschedule ? ProcData::RESCHEDULE : 0) | (allowDefer ? ProcData::ALLOW_DEFER : 0); if (doShellCommand(command, event, &alarm, (flags | ProcData::PRE_ACTION))) { AlarmCalendar::resources()->setAlarmPending(&event); return result; // display the message after the command completes } // Error executing command if (event.extraActionOptions() & KAEvent::CancelOnPreActError) { // Cancel the rest of the alarm execution qCDebug(KALARM_LOG) << "KAlarmApp::execAlarm:" << event.id() << ": pre-action failed: cancelled"; if (reschedule) rescheduleAlarm(event, alarm, true); return nullptr; } // Display the message even though it failed } if (!win) { // There isn't already a message for this event const int flags = (reschedule ? 0 : MessageWin::NO_RESCHEDULE) | (allowDefer ? 0 : MessageWin::NO_DEFER); (new MessageWin(&event, alarm, flags))->show(); } else if (replaceReminder) { // The caption needs to be changed from "Reminder" to "Message" win->cancelReminder(event, alarm); } else if (!win->hasDefer() && !alarm.repeatAtLogin()) { // It's a repeat-at-login message with no Defer button, // which has now reached its final trigger time and needs // to be replaced with a new message. win->showDefer(); win->showDateTime(event, alarm); } else { // Use the existing message window } if (win) { // Raise the existing message window and replay any sound win->repeat(alarm); // N.B. this reschedules the alarm } break; } case KAAlarm::EMAIL: { qCDebug(KALARM_LOG) << "KAlarmApp::execAlarm: EMAIL to:" << event.emailAddresses(QStringLiteral(",")); QStringList errmsgs; KAMail::JobData data(event, alarm, reschedule, (reschedule || allowDefer)); data.queued = true; int ans = KAMail::send(data, errmsgs); if (ans) { // The email has either been sent or failed - not queued if (ans < 0) result = nullptr; // failure data.queued = false; emailSent(data, errmsgs, (ans > 0)); } else { result = (void*)-1; // email has been queued } if (reschedule) rescheduleAlarm(event, alarm, true); break; } case KAAlarm::AUDIO: { // Play the sound, provided that the same event // isn't already playing MessageWin* win = MessageWin::findEvent(EventId(event)); if (!win) { // There isn't already a message for this event. const int flags = (reschedule ? 0 : MessageWin::NO_RESCHEDULE) | MessageWin::ALWAYS_HIDE; win = new MessageWin(&event, alarm, flags); } else { // There's an existing message window: replay the sound win->repeat(alarm); // N.B. this reschedules the alarm } return win; } default: return nullptr; } return result; } /****************************************************************************** * Called when sending an email has completed. */ void KAlarmApp::emailSent(KAMail::JobData& data, const QStringList& errmsgs, bool copyerr) { if (!errmsgs.isEmpty()) { // Some error occurred, although the email may have been sent successfully if (errmsgs.count() > 1) qCDebug(KALARM_LOG) << "KAlarmApp::emailSent:" << (copyerr ? "Copy error:" : "Failed:") << errmsgs[1]; MessageWin::showError(data.event, data.alarm.dateTime(), errmsgs); } else if (data.queued) Q_EMIT execAlarmSuccess(); } /****************************************************************************** * Execute the command specified in a command alarm. * To connect to the output ready signals of the process, specify a slot to be * called by supplying 'receiver' and 'slot' parameters. */ ShellProcess* KAlarmApp::execCommandAlarm(const KAEvent& event, const KAAlarm& alarm, const QObject* receiver, const char* slot) { // doShellCommand() will error if the user is not authorised to run // shell commands. const int flags = (event.commandXterm() ? ProcData::EXEC_IN_XTERM : 0) | (event.commandDisplay() ? ProcData::DISP_OUTPUT : 0); const QString command = event.cleanText(); if (event.commandScript()) { // Store the command script in a temporary file for execution qCDebug(KALARM_LOG) << "KAlarmApp::execCommandAlarm: Script"; const QString tmpfile = createTempScriptFile(command, false, event, alarm); if (tmpfile.isEmpty()) { setEventCommandError(event, KAEvent::CMD_ERROR); return nullptr; } return doShellCommand(tmpfile, event, &alarm, (flags | ProcData::TEMP_FILE), receiver, slot); } else { qCDebug(KALARM_LOG) << "KAlarmApp::execCommandAlarm:" << command; return doShellCommand(command, event, &alarm, flags, receiver, slot); } } /****************************************************************************** * Execute a shell command line specified by an alarm. * If the PRE_ACTION bit of 'flags' is set, the alarm will be executed via * execAlarm() once the command completes, the execAlarm() parameters being * derived from the remaining bits in 'flags'. * 'flags' must contain the bit PRE_ACTION or POST_ACTION if and only if it is * a pre- or post-alarm action respectively. * To connect to the output ready signals of the process, specify a slot to be * called by supplying 'receiver' and 'slot' parameters. * * Note that if shell access is not authorised, the attempt to run the command * will be errored. */ ShellProcess* KAlarmApp::doShellCommand(const QString& command, const KAEvent& event, const KAAlarm* alarm, int flags, const QObject* receiver, const char* slot) { qCDebug(KALARM_LOG) << "KAlarmApp::doShellCommand:" << command << "," << event.id(); QIODevice::OpenMode mode = QIODevice::WriteOnly; QString cmd; QString tmpXtermFile; if (flags & ProcData::EXEC_IN_XTERM) { // Execute the command in a terminal window. cmd = composeXTermCommand(command, event, alarm, flags, tmpXtermFile); if (cmd.isEmpty()) { qCWarning(KALARM_LOG) << "KAlarmApp::doShellCommand: Command failed (no terminal selected)"; const QStringList errors{i18nc("@info", "Failed to execute command\n(no terminal selected for command alarms)")}; commandErrorMsg(nullptr, event, alarm, flags, errors); return nullptr; } } else { cmd = command; mode = QIODevice::ReadWrite; } ProcData* pd = nullptr; ShellProcess* proc = nullptr; if (!cmd.isEmpty()) { // Use ShellProcess, which automatically checks whether the user is // authorised to run shell commands. proc = new ShellProcess(cmd); proc->setEnv(QStringLiteral("KALARM_UID"), event.id(), true); proc->setOutputChannelMode(KProcess::MergedChannels); // combine stdout & stderr connect(proc, &ShellProcess::shellExited, this, &KAlarmApp::slotCommandExited); if ((flags & ProcData::DISP_OUTPUT) && receiver && slot) { connect(proc, SIGNAL(receivedStdout(ShellProcess*)), receiver, slot); connect(proc, SIGNAL(receivedStderr(ShellProcess*)), receiver, slot); } if (mode == QIODevice::ReadWrite && !event.logFile().isEmpty()) { // Output is to be appended to a log file. // Set up a logging process to write the command's output to. QString heading; if (alarm && alarm->dateTime().isValid()) { const QString dateTime = alarm->dateTime().formatLocale(); heading = QStringLiteral("\n******* KAlarm %1 *******\n").arg(dateTime); } else heading = QStringLiteral("\n******* KAlarm *******\n"); QFile logfile(event.logFile()); if (logfile.open(QIODevice::Append | QIODevice::Text)) { QTextStream out(&logfile); out << heading; logfile.close(); } proc->setStandardOutputFile(event.logFile(), QIODevice::Append); } pd = new ProcData(proc, new KAEvent(event), (alarm ? new KAAlarm(*alarm) : nullptr), flags); if (flags & ProcData::TEMP_FILE) pd->tempFiles += command; if (!tmpXtermFile.isEmpty()) pd->tempFiles += tmpXtermFile; mCommandProcesses.append(pd); if (proc->start(mode)) return proc; } // Error executing command - report it qCWarning(KALARM_LOG) << "KAlarmApp::doShellCommand: Command failed to start"; commandErrorMsg(proc, event, alarm, flags); if (pd) { mCommandProcesses.removeAt(mCommandProcesses.indexOf(pd)); delete pd; } return nullptr; } /****************************************************************************** * Compose a command line to execute the given command in a terminal window. * 'tempScriptFile' receives the name of a temporary script file which is * invoked by the command line, if applicable. * Reply = command line, or empty string if error. */ QString KAlarmApp::composeXTermCommand(const QString& command, const KAEvent& event, const KAAlarm* alarm, int flags, QString& tempScriptFile) const { qCDebug(KALARM_LOG) << "KAlarmApp::composeXTermCommand:" << command << "," << event.id(); tempScriptFile.clear(); QString cmd = Preferences::cmdXTermCommand(); if (cmd.isEmpty()) return QString(); // no terminal application is configured cmd.replace(QLatin1String("%t"), KAboutData::applicationData().displayName()); // set the terminal window title if (cmd.indexOf(QLatin1String("%C")) >= 0) { // Execute the command from a temporary script file if (flags & ProcData::TEMP_FILE) cmd.replace(QLatin1String("%C"), command); // the command is already calling a temporary file else { tempScriptFile = createTempScriptFile(command, true, event, *alarm); if (tempScriptFile.isEmpty()) return QString(); cmd.replace(QLatin1String("%C"), tempScriptFile); // %C indicates where to insert the command } } else if (cmd.indexOf(QLatin1String("%W")) >= 0) { // Execute the command from a temporary script file, // with a sleep after the command is executed tempScriptFile = createTempScriptFile(command + QLatin1String("\nsleep 86400\n"), true, event, *alarm); if (tempScriptFile.isEmpty()) return QString(); cmd.replace(QLatin1String("%W"), tempScriptFile); // %w indicates where to insert the command } else if (cmd.indexOf(QLatin1String("%w")) >= 0) { // Append a sleep to the command. // Quote the command in case it contains characters such as [>|;]. const QString exec = KShell::quoteArg(command + QLatin1String("; sleep 86400")); cmd.replace(QLatin1String("%w"), exec); // %w indicates where to insert the command string } else { // Set the command to execute. // Put it in quotes in case it contains characters such as [>|;]. const QString exec = KShell::quoteArg(command); if (cmd.indexOf(QLatin1String("%c")) >= 0) cmd.replace(QLatin1String("%c"), exec); // %c indicates where to insert the command string else cmd.append(exec); // otherwise, simply append the command string } return cmd; } /****************************************************************************** * Create a temporary script file containing the specified command string. * Reply = path of temporary file, or null string if error. */ QString KAlarmApp::createTempScriptFile(const QString& command, bool insertShell, const KAEvent& event, const KAAlarm& alarm) const { QTemporaryFile tmpFile; tmpFile.setAutoRemove(false); // don't delete file when it is destructed if (!tmpFile.open()) qCCritical(KALARM_LOG) << "Unable to create a temporary script file"; else { tmpFile.setPermissions(QFile::ReadUser | QFile::WriteUser | QFile::ExeUser); QTextStream stream(&tmpFile); if (insertShell) stream << "#!" << ShellProcess::shellPath() << "\n"; stream << command; stream.flush(); if (tmpFile.error() != QFile::NoError) qCCritical(KALARM_LOG) << "Error" << tmpFile.errorString() << " writing to temporary script file"; else return tmpFile.fileName(); } const QStringList errmsgs(i18nc("@info", "Error creating temporary script file")); MessageWin::showError(event, alarm.dateTime(), errmsgs, QStringLiteral("Script")); return QString(); } /****************************************************************************** * Called when a command alarm's execution completes. */ void KAlarmApp::slotCommandExited(ShellProcess* proc) { qCDebug(KALARM_LOG) << "KAlarmApp::slotCommandExited"; // Find this command in the command list for (int i = 0, end = mCommandProcesses.count(); i < end; ++i) { ProcData* pd = mCommandProcesses.at(i); if (pd->process == proc) { // Found the command. Check its exit status. bool executeAlarm = pd->preAction(); const ShellProcess::Status status = proc->status(); if (status == ShellProcess::SUCCESS && !proc->exitCode()) { qCDebug(KALARM_LOG) << "KAlarmApp::slotCommandExited:" << pd->event->id() << ": SUCCESS"; clearEventCommandError(*pd->event, pd->preAction() ? KAEvent::CMD_ERROR_PRE : pd->postAction() ? KAEvent::CMD_ERROR_POST : KAEvent::CMD_ERROR); } else { QString errmsg = proc->errorMessage(); if (status == ShellProcess::SUCCESS || status == ShellProcess::NOT_FOUND) qCWarning(KALARM_LOG) << "KAlarmApp::slotCommandExited:" << pd->event->id() << ":" << errmsg << "exit status =" << status << ", code =" << proc->exitCode(); else qCWarning(KALARM_LOG) << "KAlarmApp::slotCommandExited:" << pd->event->id() << ":" << errmsg << "exit status =" << status; if (pd->messageBoxParent) { // Close the existing informational KMessageBox for this process const QList dialogs = pd->messageBoxParent->findChildren(); if (!dialogs.isEmpty()) delete dialogs[0]; setEventCommandError(*pd->event, pd->preAction() ? KAEvent::CMD_ERROR_PRE : pd->postAction() ? KAEvent::CMD_ERROR_POST : KAEvent::CMD_ERROR); if (!pd->tempFile()) { errmsg += QLatin1Char('\n'); errmsg += proc->command(); } KAMessageBox::error(pd->messageBoxParent, errmsg); } else commandErrorMsg(proc, *pd->event, pd->alarm, pd->flags); if (executeAlarm && (pd->event->extraActionOptions() & KAEvent::CancelOnPreActError)) { qCDebug(KALARM_LOG) << "KAlarmApp::slotCommandExited:" << pd->event->id() << ": pre-action failed: cancelled"; if (pd->reschedule()) rescheduleAlarm(*pd->event, *pd->alarm, true); executeAlarm = false; } } if (pd->preAction()) AlarmCalendar::resources()->setAlarmPending(pd->event, false); if (executeAlarm) execAlarm(*pd->event, *pd->alarm, pd->reschedule(), pd->allowDefer(), true); mCommandProcesses.removeAt(i); delete pd; break; } } // If there are now no executing shell commands, quit if a quit was queued if (mPendingQuit && mCommandProcesses.isEmpty()) quitIf(mPendingQuitCode); } /****************************************************************************** * Output an error message for a shell command, and record the alarm's error status. */ void KAlarmApp::commandErrorMsg(const ShellProcess* proc, const KAEvent& event, const KAAlarm* alarm, int flags, const QStringList& errors) { KAEvent::CmdErrType cmderr; QString dontShowAgain; QStringList errmsgs = errors; if (flags & ProcData::PRE_ACTION) { if (event.extraActionOptions() & KAEvent::DontShowPreActError) return; // don't notify user of any errors for the alarm errmsgs += i18nc("@info", "Pre-alarm action:"); dontShowAgain = QStringLiteral("Pre"); cmderr = KAEvent::CMD_ERROR_PRE; } else if (flags & ProcData::POST_ACTION) { errmsgs += i18nc("@info", "Post-alarm action:"); dontShowAgain = QStringLiteral("Post"); cmderr = (event.commandError() == KAEvent::CMD_ERROR_PRE) ? KAEvent::CMD_ERROR_PRE_POST : KAEvent::CMD_ERROR_POST; } else { dontShowAgain = QStringLiteral("Exec"); cmderr = KAEvent::CMD_ERROR; } // Record the alarm's error status setEventCommandError(event, cmderr); // Display an error message if (proc) { errmsgs += proc->errorMessage(); if (!(flags & ProcData::TEMP_FILE)) errmsgs += proc->command(); dontShowAgain += QString::number(proc->status()); } MessageWin::showError(event, (alarm ? alarm->dateTime() : DateTime()), errmsgs, dontShowAgain); } /****************************************************************************** * Notes that an informational KMessageBox is displayed for this process. */ void KAlarmApp::commandMessage(ShellProcess* proc, QWidget* parent) { // Find this command in the command list for (ProcData* pd : qAsConst(mCommandProcesses)) { if (pd->process == proc) { pd->messageBoxParent = parent; break; } } } /****************************************************************************** * If this is the first time through, open the calendar file, and start * processing the execution queue. */ -bool KAlarmApp::initCheck(bool calendarOnly, bool waitForCollection, ResourceId resourceId) +bool KAlarmApp::initCheck(bool calendarOnly, bool waitForResource, ResourceId resourceId) { static bool firstTime = true; if (firstTime) qCDebug(KALARM_LOG) << "KAlarmApp::initCheck: first time"; if (initialise() || firstTime) { /* Need to open the display calendar now, since otherwise if display * alarms are immediately due, they will often be processed while * MessageWin::redisplayAlarms() is executing open() (but before open() * completes), which causes problems!! */ AlarmCalendar::displayCalendar()->open(); if (!AlarmCalendar::resources()->open()) return false; } if (firstTime) { setArchivePurgeDays(); // Warn the user if there are no writable active alarm calendars checkWritableCalendar(); firstTime = false; } if (!calendarOnly) startProcessQueue(); // start processing the execution queue - if (waitForCollection) + if (waitForResource) { // Wait for one or all calendar resources to be populated if (!waitUntilPopulated(resourceId, AKONADI_TIMEOUT)) return false; } return true; } /****************************************************************************** * Wait for one or all enabled resources to be populated. * Reply = true if successful. */ bool KAlarmApp::waitUntilPopulated(ResourceId id, int timeout) { qCDebug(KALARM_LOG) << "KAlarmApp::waitUntilPopulated" << id; const Resource res = Resources::resource(id); if ((id < 0 && !Resources::allPopulated()) || (id >= 0 && !res.isPopulated())) { // Use AutoQPointer to guard against crash on application exit while // the event loop is still running. It prevents double deletion (both // on deletion of parent, and on return from this function). - AutoQPointer loop = new QEventLoop(AlarmListModel::all()); + AutoQPointer loop = new QEventLoop(DataModel::allAlarmListModel()); //TODO: The choice of parent object for QEventLoop can prevent EntityTreeModel signals // from activating connected slots in AkonadiDataModel, which prevents resources // from being informed that collections have loaded. Need to find a better parent // object - Qt item models seem to work, but what else? // These don't work: Resources::instance(), qApp(), theApp(), MainWindow::mainMainWindow(), AlarmCalendar::resources(), QStandardItemModel. // These do work: CollectionControlModel::instance(), AlarmListModel::all(). if (id < 0) connect(Resources::instance(), &Resources::resourcesPopulated, loop, &QEventLoop::quit); else connect(Resources::instance(), &Resources::resourcePopulated, [loop, &id](Resource& r) { if (r.id() == id) loop->quit(); }); if (timeout > 0) QTimer::singleShot(timeout * 1000, loop, &QEventLoop::quit); loop->exec(); } return (id < 0) ? Resources::allPopulated() : res.isPopulated(); } /****************************************************************************** * Called when an audio thread starts or stops. */ void KAlarmApp::notifyAudioPlaying(bool playing) { Q_EMIT audioPlaying(playing); } /****************************************************************************** * Stop audio play. */ void KAlarmApp::stopAudio() { MessageWin::stopAudio(); } /****************************************************************************** * Set the command error for the specified alarm. */ void KAlarmApp::setEventCommandError(const KAEvent& event, KAEvent::CmdErrType err) const { ProcData* pd = findCommandProcess(event.id()); if (pd && pd->eventDeleted) return; // the alarm has been deleted, so can't set error status if (err == KAEvent::CMD_ERROR_POST && event.commandError() == KAEvent::CMD_ERROR_PRE) err = KAEvent::CMD_ERROR_PRE_POST; event.setCommandError(err); KAEvent* ev = AlarmCalendar::resources()->event(EventId(event)); if (ev && ev->commandError() != err) ev->setCommandError(err); Resource resource = Resources::resourceForEvent(event.id()); resource.handleCommandErrorChange(event); } /****************************************************************************** * Clear the command error for the specified alarm. */ void KAlarmApp::clearEventCommandError(const KAEvent& event, KAEvent::CmdErrType err) const { ProcData* pd = findCommandProcess(event.id()); if (pd && pd->eventDeleted) return; // the alarm has been deleted, so can't set error status KAEvent::CmdErrType newerr = static_cast(event.commandError() & ~err); event.setCommandError(newerr); KAEvent* ev = AlarmCalendar::resources()->event(EventId(event)); if (ev) { newerr = static_cast(ev->commandError() & ~err); ev->setCommandError(newerr); } Resource resource = Resources::resourceForEvent(event.id()); resource.handleCommandErrorChange(event); } /****************************************************************************** * Find the currently executing command process for an event ID, if any. */ KAlarmApp::ProcData* KAlarmApp::findCommandProcess(const QString& eventId) const { for (ProcData* pd : qAsConst(mCommandProcesses)) { if (pd->event->id() == eventId) return pd; } return nullptr; } KAlarmApp::ProcData::ProcData(ShellProcess* p, KAEvent* e, KAAlarm* a, int f) : process(p) , event(e) , alarm(a) , messageBoxParent(nullptr) , flags(f) , eventDeleted(false) { } KAlarmApp::ProcData::~ProcData() { while (!tempFiles.isEmpty()) { // Delete the temporary file called by the XTerm command QFile f(tempFiles.first()); f.remove(); tempFiles.removeFirst(); } delete process; delete event; delete alarm; } // vim: et sw=4: diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 1cd3395b..a95dfa53 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -1,1623 +1,1623 @@ /* * mainwindow.cpp - main application window * Program: kalarm * Copyright © 2001-2019 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 "mainwindow.h" #include "alarmcalendar.h" #include "alarmlistdelegate.h" #include "alarmlistview.h" #include "birthdaydlg.h" #include "functions.h" #include "kalarmapp.h" #include "kamail.h" #include "newalarmaction.h" #include "prefdlg.h" #include "preferences.h" #include "resourceselector.h" #include "templatedlg.h" #include "templatemenuaction.h" #include "templatepickdlg.h" #include "traywindow.h" #include "wakedlg.h" -#include "resources/akonadidatamodel.h" +#include "resources/datamodel.h" #include "resources/resources.h" #include "resources/eventmodel.h" #include "lib/autoqpointer.h" #include "lib/messagebox.h" #include "lib/synchtimer.h" #include "kalarm_debug.h" #include #include #include #include #include #include #include #include #include #include using namespace KCalendarCore; using namespace KCalUtils; #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace KAlarmCal; namespace { const QString UI_FILE(QStringLiteral("kalarmui.rc")); const char* WINDOW_NAME = "MainWindow"; const char* VIEW_GROUP = "View"; const char* SHOW_COLUMNS = "ShowColumns"; const char* SHOW_ARCHIVED_KEY = "ShowArchivedAlarms"; const char* SHOW_RESOURCES_KEY = "ShowResources"; QString undoText; QString undoTextStripped; QList undoShortcut; QString redoText; QString redoTextStripped; QList redoShortcut; } /*============================================================================= = Class: MainWindow =============================================================================*/ MainWindow::WindowList MainWindow::mWindowList; TemplateDlg* MainWindow::mTemplateDlg = nullptr; /****************************************************************************** * Construct an instance. * To avoid resize() events occurring while still opening the calendar (and * resultant crashes), the calendar is opened before constructing the instance. */ MainWindow* MainWindow::create(bool restored) { theApp()->checkCalendar(); // ensure calendar is open return new MainWindow(restored); } MainWindow::MainWindow(bool restored) : MainWindowBase(nullptr, Qt::WindowContextHelpButtonHint) { qCDebug(KALARM_LOG) << "MainWindow:"; setAttribute(Qt::WA_DeleteOnClose); setWindowModality(Qt::WindowModal); setObjectName(QStringLiteral("MainWin")); // used by LikeBack setPlainCaption(KAboutData::applicationData().displayName()); KConfigGroup config(KSharedConfig::openConfig(), VIEW_GROUP); mShowResources = config.readEntry(SHOW_RESOURCES_KEY, false); mShowArchived = config.readEntry(SHOW_ARCHIVED_KEY, false); const QList showColumns = config.readEntry(SHOW_COLUMNS, QList()); if (!restored) { KConfigGroup wconfig(KSharedConfig::openConfig(), WINDOW_NAME); mResourcesWidth = wconfig.readEntry(QStringLiteral("Splitter %1").arg(QApplication::desktop()->width()), (int)0); } setAcceptDrops(true); // allow drag-and-drop onto this window mSplitter = new QSplitter(Qt::Horizontal, this); mSplitter->setChildrenCollapsible(false); mSplitter->installEventFilter(this); setCentralWidget(mSplitter); // Create the calendar resource selector widget Akonadi::ControlGui::widgetNeedsAkonadi(this); mResourceSelector = new ResourceSelector(mSplitter); mSplitter->setStretchFactor(0, 0); // don't resize resource selector when window is resized mSplitter->setStretchFactor(1, 1); // Create the alarm list widget - mListFilterModel = AlarmListModel::create(this); + mListFilterModel = DataModel::createAlarmListModel(this); mListFilterModel->setEventTypeFilter(mShowArchived ? CalEvent::ACTIVE | CalEvent::ARCHIVED : CalEvent::ACTIVE); mListView = new AlarmListView(WINDOW_NAME, mSplitter); mListView->setModel(mListFilterModel); mListView->setColumnsVisible(showColumns); mListView->setItemDelegate(new AlarmListDelegate(mListView)); connect(mListView->selectionModel(), &QItemSelectionModel::selectionChanged, this, &MainWindow::slotSelection); connect(mListView, &AlarmListView::contextMenuRequested, this, &MainWindow::slotContextMenuRequested); connect(mListView, &AlarmListView::columnsVisibleChanged, this, &MainWindow::slotAlarmListColumnsChanged); connect(Resources::instance(), &Resources::settingsChanged, this, &MainWindow::slotCalendarStatusChanged); connect(mResourceSelector, &ResourceSelector::resized, this, &MainWindow::resourcesResized); mListView->installEventFilter(this); initActions(); setAutoSaveSettings(QLatin1String(WINDOW_NAME), true); // save toolbars, window sizes etc. mWindowList.append(this); if (mWindowList.count() == 1) { // It's the first main window if (theApp()->wantShowInSystemTray()) theApp()->displayTrayIcon(true, this); // create system tray icon for run-in-system-tray mode else if (theApp()->trayWindow()) theApp()->trayWindow()->setAssocMainWindow(this); // associate this window with the system tray icon } slotCalendarStatusChanged(); // initialise action states now that window is registered } MainWindow::~MainWindow() { qCDebug(KALARM_LOG) << "~MainWindow"; bool trayParent = isTrayParent(); // must call before removing from window list mWindowList.removeAt(mWindowList.indexOf(this)); // Prevent view updates during window destruction delete mResourceSelector; mResourceSelector = nullptr; delete mListView; mListView = nullptr; if (theApp()->trayWindow()) { if (trayParent) delete theApp()->trayWindow(); else theApp()->trayWindow()->removeWindow(this); } KSharedConfig::openConfig()->sync(); // save any new window size to disc theApp()->quitIf(); } /****************************************************************************** * Called when the QApplication::saveStateRequest() signal has been emitted. * Save settings to the session managed config file, for restoration * when the program is restored. */ void MainWindow::saveProperties(KConfigGroup& config) { config.writeEntry("HiddenTrayParent", isTrayParent() && isHidden()); config.writeEntry("ShowArchived", mShowArchived); config.writeEntry("ShowColumns", mListView->columnsVisible()); config.writeEntry("ResourcesWidth", mResourceSelector->isHidden() ? 0 : mResourceSelector->width()); } /****************************************************************************** * Read settings from the session managed config file. * This function is automatically called whenever the app is being * restored. Read in whatever was saved in saveProperties(). */ void MainWindow::readProperties(const KConfigGroup& config) { mHiddenTrayParent = config.readEntry("HiddenTrayParent", true); mShowArchived = config.readEntry("ShowArchived", false); mResourcesWidth = config.readEntry("ResourcesWidth", (int)0); mShowResources = (mResourcesWidth > 0); mListView->setColumnsVisible(config.readEntry("ShowColumns", QList())); } /****************************************************************************** * Get the main main window, i.e. the parent of the system tray icon, or if * none, the first main window to be created. Visible windows take precedence * over hidden ones. */ MainWindow* MainWindow::mainMainWindow() { MainWindow* tray = theApp()->trayWindow() ? theApp()->trayWindow()->assocMainWindow() : nullptr; if (tray && tray->isVisible()) return tray; for (int i = 0, end = mWindowList.count(); i < end; ++i) if (mWindowList[i]->isVisible()) return mWindowList[i]; if (tray) return tray; if (mWindowList.isEmpty()) return nullptr; return mWindowList[0]; } /****************************************************************************** * Check whether this main window is effectively the parent of the system tray icon. */ bool MainWindow::isTrayParent() const { TrayWindow* tray = theApp()->trayWindow(); if (!tray || !QSystemTrayIcon::isSystemTrayAvailable()) return false; if (tray->assocMainWindow() == this) return true; return mWindowList.count() == 1; } /****************************************************************************** * Close all main windows. */ void MainWindow::closeAll() { while (!mWindowList.isEmpty()) delete mWindowList[0]; // N.B. the destructor removes the window from the list } /****************************************************************************** * Intercept events for the splitter widget. */ bool MainWindow::eventFilter(QObject* obj, QEvent* e) { if (obj == mSplitter) { switch (e->type()) { case QEvent::Resize: // Don't change resources size while WINDOW is being resized. // Resize event always occurs before Paint. mResizing = true; break; case QEvent::Paint: // Allow resources to be resized again mResizing = false; break; default: break; } } else if (obj == mListView) { switch (e->type()) { case QEvent::KeyPress: { QKeyEvent* ke = static_cast(e); if (ke->key() == Qt::Key_Delete && ke->modifiers() == Qt::ShiftModifier) { // Prevent Shift-Delete being processed by EventListDelegate mActionDeleteForce->trigger(); return true; } break; } default: break; } } return false; } /****************************************************************************** * Called when the window's size has changed (before it is painted). * Sets the last column in the list view to extend at least to the right hand * edge of the list view. * Records the new size in the config file. */ void MainWindow::resizeEvent(QResizeEvent* re) { // Save the window's new size only if it's the first main window MainWindowBase::resizeEvent(re); if (mResourcesWidth > 0) { QList widths; widths.append(mResourcesWidth); widths.append(width() - mResourcesWidth - mSplitter->handleWidth()); mSplitter->setSizes(widths); } } /****************************************************************************** * Called when the resources panel has been resized. * Records the new size in the config file. */ void MainWindow::resourcesResized() { if (!mShown || mResizing) return; QList widths = mSplitter->sizes(); if (widths.count() > 1) { mResourcesWidth = widths[0]; // Width is reported as non-zero when resource selector is // actually invisible, so note a zero width in these circumstances. if (mResourcesWidth <= 5) mResourcesWidth = 0; else if (mainMainWindow() == this) { KConfigGroup config(KSharedConfig::openConfig(), WINDOW_NAME); config.writeEntry(QStringLiteral("Splitter %1").arg(QApplication::desktop()->width()), mResourcesWidth); config.sync(); } } } /****************************************************************************** * Called when the window is first displayed. * Sets the last column in the list view to extend at least to the right hand * edge of the list view. */ void MainWindow::showEvent(QShowEvent* se) { if (mResourcesWidth > 0) { QList widths; widths.append(mResourcesWidth); widths.append(width() - mResourcesWidth - mSplitter->handleWidth()); mSplitter->setSizes(widths); } MainWindowBase::showEvent(se); mShown = true; } /****************************************************************************** * Display the window. */ void MainWindow::show() { MainWindowBase::show(); if (mMenuError) { // Show error message now that the main window has been displayed. // Waiting until now lets the user easily associate the message with // the main window which is faulty. KAMessageBox::error(this, xi18nc("@info", "Failure to create menus (perhaps %1 missing or corrupted)", UI_FILE)); mMenuError = false; } } /****************************************************************************** * Called after the window is hidden. */ void MainWindow::hideEvent(QHideEvent* he) { MainWindowBase::hideEvent(he); } /****************************************************************************** * Initialise the menu, toolbar and main window actions. */ void MainWindow::initActions() { KActionCollection* actions = actionCollection(); mActionTemplates = new QAction(i18nc("@action", "&Templates..."), this); actions->addAction(QStringLiteral("templates"), mActionTemplates); connect(mActionTemplates, &QAction::triggered, this, &MainWindow::slotTemplates); mActionNew = new NewAlarmAction(false, i18nc("@action", "&New"), this, actions); actions->addAction(QStringLiteral("new"), mActionNew); QAction* action = mActionNew->displayAlarmAction(QStringLiteral("newDisplay")); connect(action, &QAction::triggered, this, &MainWindow::slotNewDisplay); action = mActionNew->commandAlarmAction(QStringLiteral("newCommand")); connect(action, &QAction::triggered, this, &MainWindow::slotNewCommand); action = mActionNew->emailAlarmAction(QStringLiteral("newEmail")); connect(action, &QAction::triggered, this, &MainWindow::slotNewEmail); action = mActionNew->audioAlarmAction(QStringLiteral("newAudio")); connect(action, &QAction::triggered, this, &MainWindow::slotNewAudio); TemplateMenuAction* templateMenuAction = mActionNew->fromTemplateAlarmAction(QStringLiteral("newFromTemplate")); connect(templateMenuAction, &TemplateMenuAction::selected, this, &MainWindow::slotNewFromTemplate); mActionCreateTemplate = new QAction(i18nc("@action", "Create Tem&plate..."), this); actions->addAction(QStringLiteral("createTemplate"), mActionCreateTemplate); connect(mActionCreateTemplate, &QAction::triggered, this, &MainWindow::slotNewTemplate); mActionCopy = new QAction(QIcon::fromTheme(QStringLiteral("edit-copy")), i18nc("@action", "&Copy..."), this); actions->addAction(QStringLiteral("copy"), mActionCopy); actions->setDefaultShortcut(mActionCopy, QKeySequence(Qt::SHIFT + Qt::Key_Insert)); connect(mActionCopy, &QAction::triggered, this, &MainWindow::slotCopy); mActionModify = new QAction(QIcon::fromTheme(QStringLiteral("document-properties")), i18nc("@action", "&Edit..."), this); actions->addAction(QStringLiteral("modify"), mActionModify); actions->setDefaultShortcut(mActionModify, QKeySequence(Qt::CTRL + Qt::Key_E)); connect(mActionModify, &QAction::triggered, this, &MainWindow::slotModify); mActionDelete = new QAction(QIcon::fromTheme(QStringLiteral("edit-delete")), i18nc("@action", "&Delete"), this); actions->addAction(QStringLiteral("delete"), mActionDelete); actions->setDefaultShortcut(mActionDelete, QKeySequence::Delete); connect(mActionDelete, &QAction::triggered, this, &MainWindow::slotDeleteIf); // Set up Shift-Delete as a shortcut to delete without confirmation mActionDeleteForce = new QAction(i18nc("@action", "Delete Without Confirmation"), this); actions->addAction(QStringLiteral("delete-force"), mActionDeleteForce); actions->setDefaultShortcut(mActionDeleteForce, QKeySequence::Delete + Qt::SHIFT); connect(mActionDeleteForce, &QAction::triggered, this, &MainWindow::slotDeleteForce); mActionReactivate = new QAction(i18nc("@action", "Reac&tivate"), this); actions->addAction(QStringLiteral("undelete"), mActionReactivate); actions->setDefaultShortcut(mActionReactivate, QKeySequence(Qt::CTRL + Qt::Key_R)); connect(mActionReactivate, &QAction::triggered, this, &MainWindow::slotReactivate); mActionEnable = new QAction(this); actions->addAction(QStringLiteral("disable"), mActionEnable); actions->setDefaultShortcut(mActionEnable, QKeySequence(Qt::CTRL + Qt::Key_B)); connect(mActionEnable, &QAction::triggered, this, &MainWindow::slotEnable); action = new QAction(i18nc("@action", "Wake From Suspend..."), this); actions->addAction(QStringLiteral("wakeSuspend"), action); connect(action, &QAction::triggered, this, &MainWindow::slotWakeFromSuspend); action = KAlarm::createStopPlayAction(this); actions->addAction(QStringLiteral("stopAudio"), action); KGlobalAccel::setGlobalShortcut(action, QList()); // allow user to set a global shortcut mActionShowArchived = new KToggleAction(i18nc("@action", "Show Archi&ved Alarms"), this); actions->addAction(QStringLiteral("showArchivedAlarms"), mActionShowArchived); actions->setDefaultShortcut(mActionShowArchived, QKeySequence(Qt::CTRL + Qt::Key_P)); connect(mActionShowArchived, &KToggleAction::triggered, this, &MainWindow::slotShowArchived); mActionToggleTrayIcon = new KToggleAction(i18nc("@action", "Show in System &Tray"), this); actions->addAction(QStringLiteral("showInSystemTray"), mActionToggleTrayIcon); connect(mActionToggleTrayIcon, &KToggleAction::triggered, this, &MainWindow::slotToggleTrayIcon); mActionToggleResourceSel = new KToggleAction(QIcon::fromTheme(QStringLiteral("view-choose")), i18nc("@action", "Show &Calendars"), this); actions->addAction(QStringLiteral("showResources"), mActionToggleResourceSel); connect(mActionToggleResourceSel, &KToggleAction::triggered, this, &MainWindow::slotToggleResourceSelector); mActionSpreadWindows = KAlarm::createSpreadWindowsAction(this); actions->addAction(QStringLiteral("spread"), mActionSpreadWindows); KGlobalAccel::setGlobalShortcut(mActionSpreadWindows, QList()); // allow user to set a global shortcut mActionImportAlarms = new QAction(i18nc("@action", "Import &Alarms..."), this); actions->addAction(QStringLiteral("importAlarms"), mActionImportAlarms); connect(mActionImportAlarms, &QAction::triggered, this, &MainWindow::slotImportAlarms); mActionImportBirthdays = new QAction(i18nc("@action", "Import &Birthdays..."), this); actions->addAction(QStringLiteral("importBirthdays"), mActionImportBirthdays); connect(mActionImportBirthdays, &QAction::triggered, this, &MainWindow::slotBirthdays); mActionExportAlarms = new QAction(i18nc("@action", "E&xport Selected Alarms..."), this); actions->addAction(QStringLiteral("exportAlarms"), mActionExportAlarms); connect(mActionExportAlarms, &QAction::triggered, this, &MainWindow::slotExportAlarms); mActionExport = new QAction(i18nc("@action", "E&xport..."), this); actions->addAction(QStringLiteral("export"), mActionExport); connect(mActionExport, &QAction::triggered, this, &MainWindow::slotExportAlarms); action = new QAction(QIcon::fromTheme(QStringLiteral("view-refresh")), i18nc("@action", "&Refresh Alarms"), this); actions->addAction(QStringLiteral("refreshAlarms"), action); connect(action, &QAction::triggered, this, &MainWindow::slotRefreshAlarms); KToggleAction* toggleAction = KAlarm::createAlarmEnableAction(this); actions->addAction(QStringLiteral("alarmsEnable"), toggleAction); if (undoText.isNull()) { // Get standard texts, etc., for Undo and Redo actions QAction* act = KStandardAction::undo(this, nullptr, actions); undoShortcut = act->shortcuts(); undoText = act->text(); undoTextStripped = KLocalizedString::removeAcceleratorMarker(undoText); delete act; act = KStandardAction::redo(this, nullptr, actions); redoShortcut = act->shortcuts(); redoText = act->text(); redoTextStripped = KLocalizedString::removeAcceleratorMarker(redoText); delete act; } mActionUndo = new KToolBarPopupAction(QIcon::fromTheme(QStringLiteral("edit-undo")), undoText, this); actions->addAction(QStringLiteral("edit_undo"), mActionUndo); actions->setDefaultShortcuts(mActionUndo, undoShortcut); connect(mActionUndo, &KToolBarPopupAction::triggered, this, &MainWindow::slotUndo); mActionRedo = new KToolBarPopupAction(QIcon::fromTheme(QStringLiteral("edit-redo")), redoText, this); actions->addAction(QStringLiteral("edit_redo"), mActionRedo); actions->setDefaultShortcuts(mActionRedo, redoShortcut); connect(mActionRedo, &KToolBarPopupAction::triggered, this, &MainWindow::slotRedo); KStandardAction::find(mListView, SLOT(slotFind()), actions); mActionFindNext = KStandardAction::findNext(mListView, SLOT(slotFindNext()), actions); mActionFindPrev = KStandardAction::findPrev(mListView, SLOT(slotFindPrev()), actions); KStandardAction::selectAll(mListView, SLOT(selectAll()), actions); KStandardAction::deselect(mListView, SLOT(clearSelection()), actions); // Quit only once the event loop is called; otherwise, the parent window will // be deleted while still processing the action, resulting in a crash. QAction* act = KStandardAction::quit(nullptr, nullptr, actions); connect(act, &QAction::triggered, this, &MainWindow::slotQuit, Qt::QueuedConnection); KStandardAction::keyBindings(this, SLOT(slotConfigureKeys()), actions); KStandardAction::configureToolbars(this, SLOT(slotConfigureToolbar()), actions); KStandardAction::preferences(this, SLOT(slotPreferences()), actions); mResourceSelector->initActions(actions); setStandardToolBarMenuEnabled(true); createGUI(UI_FILE); // Load menu and toolbar settings applyMainWindowSettings(KSharedConfig::openConfig()->group(WINDOW_NAME)); mContextMenu = static_cast(factory()->container(QStringLiteral("listContext"), this)); mActionsMenu = static_cast(factory()->container(QStringLiteral("actions"), this)); QMenu* resourceMenu = static_cast(factory()->container(QStringLiteral("resourceContext"), this)); mResourceSelector->setContextMenu(resourceMenu); mMenuError = (!mContextMenu || !mActionsMenu || !resourceMenu); connect(mActionUndo->menu(), &QMenu::aboutToShow, this, &MainWindow::slotInitUndoMenu); connect(mActionUndo->menu(), &QMenu::triggered, this, &MainWindow::slotUndoItem); connect(mActionRedo->menu(), &QMenu::aboutToShow, this, &MainWindow::slotInitRedoMenu); connect(mActionRedo->menu(), &QMenu::triggered, this, &MainWindow::slotRedoItem); connect(Undo::instance(), &Undo::changed, this, &MainWindow::slotUndoStatus); connect(mListView, &AlarmListView::findActive, this, &MainWindow::slotFindActive); Preferences::connect(SIGNAL(archivedKeepDaysChanged(int)), this, SLOT(updateKeepArchived(int))); Preferences::connect(SIGNAL(showInSystemTrayChanged(bool)), this, SLOT(updateTrayIconAction())); connect(theApp(), &KAlarmApp::trayIconToggled, this, &MainWindow::updateTrayIconAction); // Set menu item states setEnableText(true); mActionShowArchived->setChecked(mShowArchived); if (!Preferences::archivedKeepDays()) mActionShowArchived->setEnabled(false); mActionToggleResourceSel->setChecked(mShowResources); slotToggleResourceSelector(); updateTrayIconAction(); // set the correct text for this action mActionUndo->setEnabled(Undo::haveUndo()); mActionRedo->setEnabled(Undo::haveRedo()); mActionFindNext->setEnabled(false); mActionFindPrev->setEnabled(false); mActionCopy->setEnabled(false); mActionModify->setEnabled(false); mActionDelete->setEnabled(false); mActionReactivate->setEnabled(false); mActionEnable->setEnabled(false); mActionCreateTemplate->setEnabled(false); mActionExport->setEnabled(false); Undo::emitChanged(); // set the Undo/Redo menu texts // Daemon::monitoringAlarms(); } /****************************************************************************** * Enable or disable the Templates menu item in every main window instance. */ void MainWindow::enableTemplateMenuItem(bool enable) { for (int i = 0, end = mWindowList.count(); i < end; ++i) mWindowList[i]->mActionTemplates->setEnabled(enable); } /****************************************************************************** * Refresh the alarm list in every main window instance. */ void MainWindow::refresh() { qCDebug(KALARM_LOG) << "MainWindow::refresh"; - AkonadiDataModel::instance()->reload(); + DataModel::reload(); } /****************************************************************************** * Called when the keep archived alarm setting changes in the user preferences. * Enable/disable Show archived alarms option. */ void MainWindow::updateKeepArchived(int days) { qCDebug(KALARM_LOG) << "MainWindow::updateKeepArchived:" << (bool)days; if (mShowArchived && !days) slotShowArchived(); // toggle Show Archived option setting mActionShowArchived->setEnabled(days); } /****************************************************************************** * Select an alarm in the displayed list. */ void MainWindow::selectEvent(const QString& eventId) { mListView->clearSelection(); const QModelIndex index = mListFilterModel->eventIndex(eventId); if (index.isValid()) { mListView->select(index); mListView->scrollTo(index); } } /****************************************************************************** * Return the single selected alarm in the displayed list. */ KAEvent MainWindow::selectedEvent() const { return mListView->selectedEvent(); } /****************************************************************************** * Deselect all alarms in the displayed list. */ void MainWindow::clearSelection() { mListView->clearSelection(); } /****************************************************************************** * Called when the New button is clicked to edit a new alarm to add to the list. */ void MainWindow::slotNew(EditAlarmDlg::Type type) { KAlarm::editNewAlarm(type, mListView); } /****************************************************************************** * Called when a template is selected from the New From Template popup menu. * Executes a New Alarm dialog, preset from the selected template. */ void MainWindow::slotNewFromTemplate(const KAEvent* tmplate) { KAlarm::editNewAlarm(tmplate, mListView); } /****************************************************************************** * Called when the New Template button is clicked to create a new template * based on the currently selected alarm. */ void MainWindow::slotNewTemplate() { KAEvent event = mListView->selectedEvent(); if (event.isValid()) KAlarm::editNewTemplate(&event, this); } /****************************************************************************** * Called when the Copy button is clicked to edit a copy of an existing alarm, * to add to the list. */ void MainWindow::slotCopy() { KAEvent event = mListView->selectedEvent(); if (event.isValid()) KAlarm::editNewAlarm(&event, this); } /****************************************************************************** * Called when the Modify button is clicked to edit the currently highlighted * alarm in the list. */ void MainWindow::slotModify() { KAEvent event = mListView->selectedEvent(); if (event.isValid()) KAlarm::editAlarm(&event, this); // edit alarm (view-only mode if archived or read-only) } /****************************************************************************** * Called when the Delete button is clicked to delete the currently highlighted * alarms in the list. */ void MainWindow::slotDelete(bool force) { QVector events = mListView->selectedEvents(); if (!force && Preferences::confirmAlarmDeletion()) { int n = events.count(); if (KAMessageBox::warningContinueCancel(this, i18ncp("@info", "Do you really want to delete the selected alarm?", "Do you really want to delete the %1 selected alarms?", n), i18ncp("@title:window", "Delete Alarm", "Delete Alarms", n), KGuiItem(i18nc("@action:button", "&Delete"), QStringLiteral("edit-delete")), KStandardGuiItem::cancel(), Preferences::CONFIRM_ALARM_DELETION) != KMessageBox::Continue) return; } // Remove any events which have just triggered, from the list to delete. Undo::EventList undos; for (int i = 0; i < events.count(); ) { Resource res = Resources::resourceForEvent(events[i].id()); if (!res.isValid()) events.remove(i); else undos.append(events[i++], res); } if (events.isEmpty()) qCDebug(KALARM_LOG) << "MainWindow::slotDelete: No alarms left to delete"; else { // Delete the events from the calendar and displays KAlarm::deleteEvents(events, true, this); Undo::saveDeletes(undos); } } /****************************************************************************** * Called when the Reactivate button is clicked to reinstate the currently * highlighted archived alarms in the list. */ void MainWindow::slotReactivate() { QVector events = mListView->selectedEvents(); mListView->clearSelection(); // Add the alarms to the displayed lists and to the calendar file Undo::EventList undos; QVector ineligibleIDs; KAlarm::reactivateEvents(events, ineligibleIDs, nullptr, this); // Create the undo list, excluding ineligible events for (int i = 0, end = events.count(); i < end; ++i) { if (!ineligibleIDs.contains(EventId(events[i]))) undos.append(events[i], Resources::resourceForEvent(events[i].id())); } Undo::saveReactivates(undos); } /****************************************************************************** * Called when the Enable/Disable button is clicked to enable or disable the * currently highlighted alarms in the list. */ void MainWindow::slotEnable() { bool enable = mActionEnableEnable; // save since changed in response to KAlarm::enableEvent() QVector events = mListView->selectedEvents(); QVector eventCopies; for (int i = 0, end = events.count(); i < end; ++i) eventCopies += events[i]; KAlarm::enableEvents(eventCopies, enable, this); slotSelection(); // update Enable/Disable action text } /****************************************************************************** * Called when the columns visible in the alarm list view have changed. */ void MainWindow::slotAlarmListColumnsChanged() { KConfigGroup config(KSharedConfig::openConfig(), VIEW_GROUP); config.writeEntry(SHOW_COLUMNS, mListView->columnsVisible()); config.sync(); } /****************************************************************************** * Called when the Show Archived Alarms menu item is selected or deselected. */ void MainWindow::slotShowArchived() { mShowArchived = !mShowArchived; mActionShowArchived->setChecked(mShowArchived); mActionShowArchived->setToolTip(mShowArchived ? i18nc("@info:tooltip", "Hide Archived Alarms") : i18nc("@info:tooltip", "Show Archived Alarms")); mListFilterModel->setEventTypeFilter(mShowArchived ? CalEvent::ACTIVE | CalEvent::ARCHIVED : CalEvent::ACTIVE); mListView->reset(); KConfigGroup config(KSharedConfig::openConfig(), VIEW_GROUP); config.writeEntry(SHOW_ARCHIVED_KEY, mShowArchived); config.sync(); } /****************************************************************************** * Called when the Spread Windows global shortcut is selected, to spread alarm * windows so that they are all visible. */ void MainWindow::slotSpreadWindowsShortcut() { mActionSpreadWindows->trigger(); } /****************************************************************************** * Called when the Wake From Suspend menu option is selected. */ void MainWindow::slotWakeFromSuspend() { (WakeFromSuspendDlg::create(this))->show(); } /****************************************************************************** * Called when the Import Alarms menu item is selected, to merge alarms from an * external calendar into the current calendars. */ void MainWindow::slotImportAlarms() { AlarmCalendar::resources()->importAlarms(this); } /****************************************************************************** * Called when the Export Alarms menu item is selected, to export the selected * alarms to an external calendar. */ void MainWindow::slotExportAlarms() { QVector events = mListView->selectedEvents(); if (!events.isEmpty()) { KAEvent::List evts = KAEvent::ptrList(events); AlarmCalendar::exportAlarms(evts, this); } } /****************************************************************************** * Called when the Import Birthdays menu item is selected, to display birthdays * from the address book for selection as alarms. */ void MainWindow::slotBirthdays() { // Use AutoQPointer to guard against crash on application exit while // the dialogue is still open. It prevents double deletion (both on // deletion of MainWindow, and on return from this function). AutoQPointer dlg = new BirthdayDlg(this); if (dlg->exec() == QDialog::Accepted) { QVector events = dlg->events(); if (!events.isEmpty()) { mListView->clearSelection(); // Add alarm to the displayed lists and to the calendar file KAlarm::UpdateResult status = KAlarm::addEvents(events, dlg, true, true); Undo::EventList undos; for (int i = 0, end = events.count(); i < end; ++i) undos.append(events[i], Resources::resourceForEvent(events[i].id())); Undo::saveAdds(undos, i18nc("@info", "Import birthdays")); if (status != KAlarm::UPDATE_FAILED) KAlarm::outputAlarmWarnings(dlg); } } } /****************************************************************************** * Called when the Templates menu item is selected, to display the alarm * template editing dialog. */ void MainWindow::slotTemplates() { if (!mTemplateDlg) { mTemplateDlg = TemplateDlg::create(this); enableTemplateMenuItem(false); // disable menu item in all windows connect(mTemplateDlg, &QDialog::finished, this, &MainWindow::slotTemplatesEnd); mTemplateDlg->show(); } } /****************************************************************************** * Called when the alarm template editing dialog has exited. */ void MainWindow::slotTemplatesEnd() { if (mTemplateDlg) { mTemplateDlg->deleteLater(); // this deletes the dialog once it is safe to do so mTemplateDlg = nullptr; enableTemplateMenuItem(true); // re-enable menu item in all windows } } /****************************************************************************** * Called when the Display System Tray Icon menu item is selected. */ void MainWindow::slotToggleTrayIcon() { theApp()->displayTrayIcon(!theApp()->trayIconDisplayed(), this); } /****************************************************************************** * Called when the Show Resource Selector menu item is selected. */ void MainWindow::slotToggleResourceSelector() { mShowResources = mActionToggleResourceSel->isChecked(); if (mShowResources) { if (mResourcesWidth <= 0) { mResourcesWidth = mResourceSelector->sizeHint().width(); mResourceSelector->resize(mResourcesWidth, mResourceSelector->height()); QList widths = mSplitter->sizes(); if (widths.count() == 1) { int listwidth = widths[0] - mSplitter->handleWidth() - mResourcesWidth; mListView->resize(listwidth, mListView->height()); widths.append(listwidth); widths[0] = mResourcesWidth; } mSplitter->setSizes(widths); } mResourceSelector->show(); } else mResourceSelector->hide(); KConfigGroup config(KSharedConfig::openConfig(), VIEW_GROUP); config.writeEntry(SHOW_RESOURCES_KEY, mShowResources); config.sync(); } /****************************************************************************** * Called when an error occurs in the resource calendar, to display a message. */ void MainWindow::showErrorMessage(const QString& msg) { KAMessageBox::error(this, msg); } /****************************************************************************** * Called when the system tray icon is created or destroyed. * Set the system tray icon menu text according to whether or not the system * tray icon is currently visible. */ void MainWindow::updateTrayIconAction() { mActionToggleTrayIcon->setEnabled(QSystemTrayIcon::isSystemTrayAvailable()); mActionToggleTrayIcon->setChecked(theApp()->trayIconDisplayed()); } /****************************************************************************** * Called when the active status of Find changes. */ void MainWindow::slotFindActive(bool active) { mActionFindNext->setEnabled(active); mActionFindPrev->setEnabled(active); } /****************************************************************************** * Called when the Undo action is selected. */ void MainWindow::slotUndo() { Undo::undo(this, KLocalizedString::removeAcceleratorMarker(mActionUndo->text())); } /****************************************************************************** * Called when the Redo action is selected. */ void MainWindow::slotRedo() { Undo::redo(this, KLocalizedString::removeAcceleratorMarker(mActionRedo->text())); } /****************************************************************************** * Called when an Undo item is selected. */ void MainWindow::slotUndoItem(QAction* action) { int id = mUndoMenuIds[action]; Undo::undo(id, this, Undo::actionText(Undo::UNDO, id)); } /****************************************************************************** * Called when a Redo item is selected. */ void MainWindow::slotRedoItem(QAction* action) { int id = mUndoMenuIds[action]; Undo::redo(id, this, Undo::actionText(Undo::REDO, id)); } /****************************************************************************** * Called when the Undo menu is about to show. * Populates the menu. */ void MainWindow::slotInitUndoMenu() { initUndoMenu(mActionUndo->menu(), Undo::UNDO); } /****************************************************************************** * Called when the Redo menu is about to show. * Populates the menu. */ void MainWindow::slotInitRedoMenu() { initUndoMenu(mActionRedo->menu(), Undo::REDO); } /****************************************************************************** * Populate the undo or redo menu. */ void MainWindow::initUndoMenu(QMenu* menu, Undo::Type type) { menu->clear(); mUndoMenuIds.clear(); const QString& action = (type == Undo::UNDO) ? undoTextStripped : redoTextStripped; QList ids = Undo::ids(type); for (int i = 0, end = ids.count(); i < end; ++i) { int id = ids[i]; QString actText = Undo::actionText(type, id); QString descrip = Undo::description(type, id); QString text = descrip.isEmpty() ? i18nc("@action Undo/Redo [action]", "%1 %2", action, actText) : i18nc("@action Undo [action]: message", "%1 %2: %3", action, actText, descrip); QAction* act = menu->addAction(text); mUndoMenuIds[act] = id; } } /****************************************************************************** * Called when the status of the Undo or Redo list changes. * Change the Undo or Redo text to include the action which would be undone/redone. */ void MainWindow::slotUndoStatus(const QString& undo, const QString& redo) { if (undo.isNull()) { mActionUndo->setEnabled(false); mActionUndo->setText(undoText); } else { mActionUndo->setEnabled(true); mActionUndo->setText(QStringLiteral("%1 %2").arg(undoText, undo)); } if (redo.isNull()) { mActionRedo->setEnabled(false); mActionRedo->setText(redoText); } else { mActionRedo->setEnabled(true); mActionRedo->setText(QStringLiteral("%1 %2").arg(redoText, redo)); } } /****************************************************************************** * Called when the Refresh Alarms menu item is selected. */ void MainWindow::slotRefreshAlarms() { KAlarm::refreshAlarms(); } /****************************************************************************** * Called when the "Configure KAlarm" menu item is selected. */ void MainWindow::slotPreferences() { KAlarmPrefDlg::display(); } /****************************************************************************** * Called when the Configure Keys menu item is selected. */ void MainWindow::slotConfigureKeys() { KShortcutsDialog::configure(actionCollection(), KShortcutsEditor::LetterShortcutsAllowed, this); } /****************************************************************************** * Called when the Configure Toolbars menu item is selected. */ void MainWindow::slotConfigureToolbar() { KConfigGroup grp(KSharedConfig::openConfig()->group(WINDOW_NAME)); saveMainWindowSettings(grp); KEditToolBar dlg(factory()); connect(&dlg, &KEditToolBar::newToolBarConfig, this, &MainWindow::slotNewToolbarConfig); dlg.exec(); } /****************************************************************************** * Called when OK or Apply is clicked in the Configure Toolbars dialog, to save * the new configuration. */ void MainWindow::slotNewToolbarConfig() { createGUI(UI_FILE); applyMainWindowSettings(KSharedConfig::openConfig()->group(WINDOW_NAME)); } /****************************************************************************** * Called when the Quit menu item is selected. * Note that this must be called by the event loop, not directly from the menu * item, since otherwise the window will be deleted while still processing the * menu, resulting in a crash. */ void MainWindow::slotQuit() { theApp()->doQuit(this); } /****************************************************************************** * Called when the user or the session manager attempts to close the window. */ void MainWindow::closeEvent(QCloseEvent* ce) { if (!qApp->isSavingSession()) { // The user (not the session manager) wants to close the window. if (isTrayParent()) { // It's the parent window of the system tray icon, so just hide // it to prevent the system tray icon closing. hide(); theApp()->quitIf(); ce->ignore(); return; } } ce->accept(); } /****************************************************************************** * Called when the drag cursor enters a main or system tray window, to accept * or reject the dragged object. */ void MainWindow::executeDragEnterEvent(QDragEnterEvent* e) { const QMimeData* data = e->mimeData(); bool accept = ICalDrag::canDecode(data) ? !e->source() // don't accept "text/calendar" objects from this application : data->hasText() || data->hasUrls() || KPIM::MailList::canDecode(data); if (accept) e->acceptProposedAction(); } /****************************************************************************** * Called when an object is dropped on the window. * If the object is recognised, the edit alarm dialog is opened appropriately. */ void MainWindow::dropEvent(QDropEvent* e) { executeDropEvent(this, e); } static QString getMailHeader(const char* header, KMime::Content& content) { KMime::Headers::Base* hd = content.headerByType(header); return hd ? hd->asUnicodeString() : QString(); } /****************************************************************************** * Called when an object is dropped on a main or system tray window, to * evaluate the action required and extract the text. */ void MainWindow::executeDropEvent(MainWindow* win, QDropEvent* e) { qCDebug(KALARM_LOG) << "MainWindow::executeDropEvent: Formats:" << e->mimeData()->formats(); const QMimeData* data = e->mimeData(); KAEvent::SubAction action = KAEvent::MESSAGE; QByteArray bytes; AlarmText alarmText; KPIM::MailList mailList; QList urls; MemoryCalendar::Ptr calendar(new MemoryCalendar(Preferences::timeSpecAsZone())); #ifndef NDEBUG QString fmts = data->formats().join(QLatin1String(", ")); qCDebug(KALARM_LOG) << "MainWindow::executeDropEvent:" << fmts; #endif /* The order of the tests below matters, since some dropped objects * provide more than one mime type. * Don't change them without careful thought !! */ if (!(bytes = data->data(QStringLiteral("message/rfc822"))).isEmpty()) { // Email message(s). Ignore all but the first. qCDebug(KALARM_LOG) << "MainWindow::executeDropEvent: email"; KMime::Content content; content.setContent(bytes); content.parse(); QString body; if (content.textContent()) body = content.textContent()->decodedText(true, true); // strip trailing newlines & spaces unsigned long sernum = 0; if (KPIM::MailList::canDecode(data)) { // Get its KMail serial number to allow the KMail message // to be called up from the alarm message window. mailList = KPIM::MailList::fromMimeData(data); if (!mailList.isEmpty()) sernum = mailList.at(0).serialNumber(); } alarmText.setEmail(getMailHeader("To", content), getMailHeader("From", content), getMailHeader("Cc", content), getMailHeader("Date", content), getMailHeader("Subject", content), body, sernum); } else if (KPIM::MailList::canDecode(data)) { mailList = KPIM::MailList::fromMimeData(data); // KMail message(s). Ignore all but the first. qCDebug(KALARM_LOG) << "MainWindow::executeDropEvent: KMail_list"; if (mailList.isEmpty()) return; const KPIM::MailSummary& summary = mailList.at(0); QDateTime dt; dt.setSecsSinceEpoch(summary.date()); const QString body = KAMail::getMailBody(summary.serialNumber()); alarmText.setEmail(summary.to(), summary.from(), QString(), QLocale().toString(dt), summary.subject(), body, summary.serialNumber()); } else if (ICalDrag::fromMimeData(data, calendar)) { // iCalendar - If events are included, use the first event qCDebug(KALARM_LOG) << "MainWindow::executeDropEvent: iCalendar"; const Event::List events = calendar->rawEvents(); if (!events.isEmpty()) { Event::Ptr event = events[0]; if (event->alarms().isEmpty()) { Alarm::Ptr alarm = event->newAlarm(); alarm->setEnabled(true); alarm->setTime(event->dtStart()); alarm->setDisplayAlarm(event->summary().isEmpty() ? event->description() : event->summary()); event->addAlarm(alarm); } KAEvent ev(event); KAlarm::editNewAlarm(&ev, win); return; } // If todos are included, use the first todo const Todo::List todos = calendar->rawTodos(); if (!todos.isEmpty()) { Todo::Ptr todo = todos[0]; alarmText.setTodo(todo); KADateTime start(todo->dtStart(true)); KADateTime due(todo->dtDue(true)); bool haveBothTimes = false; if (todo->hasDueDate()) { if (start.isValid()) haveBothTimes = true; else start = due; } if (todo->allDay()) start.setDateOnly(true); KAEvent::Flags flags = KAEvent::DEFAULT_FONT; if (start.isDateOnly()) flags |= KAEvent::ANY_TIME; KAEvent ev(start, alarmText.displayText(), Preferences::defaultBgColour(), Preferences::defaultFgColour(), QFont(), KAEvent::MESSAGE, 0, flags, true); ev.startChanges(); if (todo->recurs()) { ev.setRecurrence(*todo->recurrence()); ev.setNextOccurrence(KADateTime::currentUtcDateTime()); } const Alarm::List alarms = todo->alarms(); if (!alarms.isEmpty() && alarms[0]->type() == Alarm::Display) { // A display alarm represents a reminder int offset = 0; if (alarms[0]->hasStartOffset()) offset = alarms[0]->startOffset().asSeconds(); else if (alarms[0]->hasEndOffset()) { offset = alarms[0]->endOffset().asSeconds(); if (haveBothTimes) { // Get offset relative to start time instead of due time offset += start.secsTo(due); } } if (offset / 60) ev.setReminder(-offset / 60, false); } ev.endChanges(); KAlarm::editNewAlarm(&ev, win); } return; } else if (!(urls = data->urls()).isEmpty()) { const QUrl& url(urls.at(0)); const Akonadi::Item item = Akonadi::Item::fromUrl(url); if (item.isValid()) { // It's an Akonadi item qCDebug(KALARM_LOG) << "MainWindow::executeDropEvent: Akonadi item" << item.id(); if (QUrlQuery(url).queryItemValue(QStringLiteral("type")) == QLatin1String("message/rfc822")) { // It's an email held in Akonadi qCDebug(KALARM_LOG) << "MainWindow::executeDropEvent: Akonadi email"; Akonadi::ItemFetchJob* job = new Akonadi::ItemFetchJob(item); job->fetchScope().fetchFullPayload(); Akonadi::Item::List items; if (job->exec()) items = job->items(); if (items.isEmpty()) { qCWarning(KALARM_LOG) << "MainWindow::executeDropEvent: Akonadi item" << item.id() << "not found"; return; } const Akonadi::Item& it = items.at(0); if (!it.isValid() || !it.hasPayload()) { qCWarning(KALARM_LOG) << "MainWindow::executeDropEvent: invalid email"; return; } KMime::Message::Ptr message = it.payload(); QString body; if (message->textContent()) body = message->textContent()->decodedText(true, true); // strip trailing newlines & spaces alarmText.setEmail(getMailHeader("To", *message), getMailHeader("From", *message), getMailHeader("Cc", *message), getMailHeader("Date", *message), getMailHeader("Subject", *message), body, it.id()); } } else { qCDebug(KALARM_LOG) << "MainWindow::executeDropEvent: URL"; // Try to find the mime type of the file, without downloading a remote file QMimeDatabase mimeDb; const QString mimeTypeName = mimeDb.mimeTypeForUrl(url).name(); action = mimeTypeName.startsWith(QLatin1String("audio/")) ? KAEvent::AUDIO : KAEvent::FILE; alarmText.setText(url.toDisplayString()); } } if (alarmText.isEmpty()) { if (data->hasText()) { const QString text = data->text(); qCDebug(KALARM_LOG) << "MainWindow::executeDropEvent: text"; alarmText.setText(text); } else return; } if (!alarmText.isEmpty()) { if (action == KAEvent::MESSAGE && (alarmText.isEmail() || alarmText.isScript())) { // If the alarm text could be interpreted as an email or command script, // prompt for which type of alarm to create. QStringList types; types += i18nc("@item:inlistbox", "Display Alarm"); if (alarmText.isEmail()) types += i18nc("@item:inlistbox", "Email Alarm"); else if (alarmText.isScript()) types += i18nc("@item:inlistbox", "Command Alarm"); bool ok = false; QString type = QInputDialog::getItem(mainMainWindow(), i18nc("@title:window", "Alarm Type"), i18nc("@info", "Choose alarm type to create:"), types, 0, false, &ok); if (!ok) return; // user didn't press OK int i = types.indexOf(type); if (i == 1) action = alarmText.isEmail() ? KAEvent::EMAIL : KAEvent::COMMAND; } KAlarm::editNewAlarm(action, win, &alarmText); } } /****************************************************************************** * Called when the status of a calendar has changed. * Enable or disable actions appropriately. */ void MainWindow::slotCalendarStatusChanged() { // Find whether there are any writable calendars bool active = !Resources::enabledResources(CalEvent::ACTIVE, true).isEmpty(); bool templat = !Resources::enabledResources(CalEvent::TEMPLATE, true).isEmpty(); for (int i = 0, end = mWindowList.count(); i < end; ++i) { MainWindow* w = mWindowList[i]; w->mActionImportAlarms->setEnabled(active || templat); w->mActionImportBirthdays->setEnabled(active); w->mActionCreateTemplate->setEnabled(templat); // Note: w->mActionNew enabled status is set in the NewAlarmAction class. w->slotSelection(); } } /****************************************************************************** * Called when the selected items in the ListView change. * Enables the actions appropriately. */ void MainWindow::slotSelection() { // Find which events have been selected QVector events = mListView->selectedEvents(); int count = events.count(); if (!count) { selectionCleared(); // disable actions Q_EMIT selectionChanged(); return; } // Find whether there are any writable resources bool active = mActionNew->isEnabled(); bool readOnly = false; bool allArchived = true; bool enableReactivate = true; bool enableEnableDisable = true; bool enableEnable = false; bool enableDisable = false; AlarmCalendar* resources = AlarmCalendar::resources(); const KADateTime now = KADateTime::currentUtcDateTime(); for (int i = 0; i < count; ++i) { KAEvent* ev = resources->event(EventId(events.at(i))); // get up-to-date status KAEvent* event = ev ? ev : &events[i]; bool expired = event->expired(); if (!expired) allArchived = false; if (resources->eventReadOnly(event->id())) readOnly = true; if (enableReactivate && (!expired || !event->occursAfter(now, true))) enableReactivate = false; if (enableEnableDisable) { if (expired) enableEnableDisable = enableEnable = enableDisable = false; else { if (!enableEnable && !event->enabled()) enableEnable = true; if (!enableDisable && event->enabled()) enableDisable = true; } } } qCDebug(KALARM_LOG) << "MainWindow::slotSelection: true"; mActionCreateTemplate->setEnabled((count == 1) && !Resources::enabledResources(CalEvent::TEMPLATE, true).isEmpty()); mActionExportAlarms->setEnabled(true); mActionExport->setEnabled(true); mActionCopy->setEnabled(active && count == 1); mActionModify->setEnabled(count == 1); mActionDelete->setEnabled(!readOnly && (active || allArchived)); mActionReactivate->setEnabled(active && enableReactivate); mActionEnable->setEnabled(active && !readOnly && (enableEnable || enableDisable)); if (enableEnable || enableDisable) setEnableText(enableEnable); Q_EMIT selectionChanged(); } /****************************************************************************** * Called when a context menu is requested in the ListView. * Displays a context menu to modify or delete the selected item. */ void MainWindow::slotContextMenuRequested(const QPoint& globalPos) { qCDebug(KALARM_LOG) << "MainWindow::slotContextMenuRequested"; if (mContextMenu) mContextMenu->popup(globalPos); } /****************************************************************************** * Disables actions when no item is selected. */ void MainWindow::selectionCleared() { mActionCreateTemplate->setEnabled(false); mActionExportAlarms->setEnabled(false); mActionExport->setEnabled(false); mActionCopy->setEnabled(false); mActionModify->setEnabled(false); mActionDelete->setEnabled(false); mActionReactivate->setEnabled(false); mActionEnable->setEnabled(false); } /****************************************************************************** * Set the text of the Enable/Disable menu action. */ void MainWindow::setEnableText(bool enable) { mActionEnableEnable = enable; mActionEnable->setText(enable ? i18nc("@action", "Ena&ble") : i18nc("@action", "Disa&ble")); } /****************************************************************************** * Display or hide the specified main window. * This should only be called when the application doesn't run in the system tray. */ MainWindow* MainWindow::toggleWindow(MainWindow* win) { if (win && mWindowList.indexOf(win) != -1) { // A window is specified (and it exists) if (win->isVisible()) { // The window is visible, so close it win->close(); return nullptr; } else { // The window is hidden, so display it win->hide(); // in case it's on a different desktop win->setWindowState(win->windowState() & ~Qt::WindowMinimized); win->raise(); win->activateWindow(); return win; } } // No window is specified, or the window doesn't exist. Open a new one. win = create(); win->show(); return win; } /****************************************************************************** * Called when the Edit button is clicked in an alarm message window. * This controls the alarm edit dialog created by the alarm window, and allows * it to remain unaffected by the alarm window closing. * See MessageWin::slotEdit() for more information. */ void MainWindow::editAlarm(EditAlarmDlg* dlg, const KAEvent& event) { mEditAlarmMap[dlg] = event; connect(dlg, &KEditToolBar::accepted, this, &MainWindow::editAlarmOk); connect(dlg, &KEditToolBar::destroyed, this, &MainWindow::editAlarmDeleted); dlg->setAttribute(Qt::WA_DeleteOnClose, true); // ensure no memory leaks dlg->show(); } /****************************************************************************** * Called when OK is clicked in the alarm edit dialog shown by editAlarm(). * Updates the event which has been edited. */ void MainWindow::editAlarmOk() { EditAlarmDlg* dlg = qobject_cast(sender()); if (!dlg) return; QMap::Iterator it = mEditAlarmMap.find(dlg); if (it == mEditAlarmMap.end()) return; KAEvent event = it.value(); mEditAlarmMap.erase(it); if (!event.isValid()) return; if (dlg->result() != QDialog::Accepted) return; Resource res = Resources::resourceForEvent(event.id()); KAlarm::updateEditedAlarm(dlg, event, res); } /****************************************************************************** * Called when the alarm edit dialog shown by editAlarm() is deleted. * Removes the dialog from the pending list. */ void MainWindow::editAlarmDeleted(QObject* obj) { mEditAlarmMap.remove(static_cast(obj)); } // vim: et sw=4: diff --git a/src/newalarmaction.cpp b/src/newalarmaction.cpp index 257dcc23..6c5dc1d3 100644 --- a/src/newalarmaction.cpp +++ b/src/newalarmaction.cpp @@ -1,179 +1,179 @@ /* * newalarmaction.cpp - menu action to select a new alarm type * Program: kalarm * Copyright © 2007-2019 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 "newalarmaction.h" #include "functions.h" #include "templatemenuaction.h" -#include "resources/akonadidatamodel.h" +#include "resources/datamodel.h" #include "resources/resources.h" #include "resources/eventmodel.h" #include "lib/shellprocess.h" #include "kalarm_debug.h" #include #include #include #include #include #include using namespace KAlarmCal; #define DISP_ICON QStringLiteral("window-new") #define CMD_ICON QStringLiteral("new-command-alarm") #define MAIL_ICON QStringLiteral("mail-message-new") #define AUDIO_ICON QStringLiteral("new-audio-alarm") #define TEMPLATE_ICON QStringLiteral("document-new-from-template") #define DISP_KEY QKeySequence(Qt::CTRL + Qt::Key_D) #define CMD_KEY QKeySequence(Qt::CTRL + Qt::Key_C) #define MAIL_KEY QKeySequence(Qt::CTRL + Qt::Key_M) #define AUDIO_KEY QKeySequence(Qt::CTRL + Qt::Key_U) /****************************************************************************** * Create New Alarm actions as a menu containing each alarm type, and add to * the KActionCollection. */ NewAlarmAction::NewAlarmAction(bool templates, const QString& label, QObject* parent, KActionCollection* collection) : KActionMenu(QIcon::fromTheme(QStringLiteral("document-new")), label, parent) , mActionCollection(collection) { mDisplayAction = new QAction(QIcon::fromTheme(DISP_ICON), (templates ? i18nc("@item:inmenu", "&Display Alarm Template") : i18nc("@action", "New Display Alarm")), parent); menu()->addAction(mDisplayAction); mTypes[mDisplayAction] = EditAlarmDlg::DISPLAY; mCommandAction = new QAction(QIcon::fromTheme(CMD_ICON), (templates ? i18nc("@item:inmenu", "&Command Alarm Template") : i18nc("@action", "New Command Alarm")), parent); menu()->addAction(mCommandAction); mTypes[mCommandAction] = EditAlarmDlg::COMMAND; mEmailAction = new QAction(QIcon::fromTheme(MAIL_ICON), (templates ? i18nc("@item:inmenu", "&Email Alarm Template") : i18nc("@action", "New Email Alarm")), parent); menu()->addAction(mEmailAction); mTypes[mEmailAction] = EditAlarmDlg::EMAIL; mAudioAction = new QAction(QIcon::fromTheme(AUDIO_ICON), (templates ? i18nc("@item:inmenu", "&Audio Alarm Template") : i18nc("@action", "New Audio Alarm")), parent); menu()->addAction(mAudioAction); mTypes[mAudioAction] = EditAlarmDlg::AUDIO; if (!templates) { if (!mActionCollection) { mDisplayAction->setShortcut(DISP_KEY); mCommandAction->setShortcut(CMD_KEY); mEmailAction->setShortcut(MAIL_KEY); mAudioAction->setShortcut(AUDIO_KEY); } // Include New From Template only in non-template menu mTemplateAction = new TemplateMenuAction(QIcon::fromTheme(TEMPLATE_ICON), i18nc("@action", "New Alarm From &Template"), parent); menu()->addAction(mTemplateAction); connect(Resources::instance(), &Resources::settingsChanged, this, &NewAlarmAction::slotCalendarStatusChanged); - connect(TemplateListModel::all(), &EventListModel::haveEventsStatus, this, &NewAlarmAction::slotCalendarStatusChanged); + connect(DataModel::allTemplateListModel(), &EventListModel::haveEventsStatus, this, &NewAlarmAction::slotCalendarStatusChanged); slotCalendarStatusChanged(); // initialise action states } setDelayed(false); connect(menu(), &QMenu::aboutToShow, this, &NewAlarmAction::slotInitMenu); connect(menu(), &QMenu::triggered, this, &NewAlarmAction::slotSelected); } /****************************************************************************** */ QAction* NewAlarmAction::displayAlarmAction(const QString& name) { if (mActionCollection) { mActionCollection->addAction(name, mDisplayAction); mActionCollection->setDefaultShortcut(mDisplayAction, DISP_KEY); KGlobalAccel::setGlobalShortcut(mDisplayAction, QList()); // allow user to set a global shortcut } return mDisplayAction; } QAction* NewAlarmAction::commandAlarmAction(const QString& name) { if (mActionCollection) { mActionCollection->addAction(name, mCommandAction); mActionCollection->setDefaultShortcut(mCommandAction, CMD_KEY); KGlobalAccel::setGlobalShortcut(mCommandAction, QList()); // allow user to set a global shortcut } return mCommandAction; } QAction* NewAlarmAction::emailAlarmAction(const QString& name) { if (mActionCollection) { mActionCollection->addAction(name, mEmailAction); mActionCollection->setDefaultShortcut(mEmailAction, MAIL_KEY); KGlobalAccel::setGlobalShortcut(mEmailAction, QList()); // allow user to set a global shortcut } return mEmailAction; } QAction* NewAlarmAction::audioAlarmAction(const QString& name) { if (mActionCollection) { mActionCollection->addAction(name, mAudioAction); mActionCollection->setDefaultShortcut(mAudioAction, AUDIO_KEY); KGlobalAccel::setGlobalShortcut(mAudioAction, QList()); // allow user to set a global shortcut } return mAudioAction; } TemplateMenuAction* NewAlarmAction::fromTemplateAlarmAction(const QString& name) { if (mActionCollection) mActionCollection->addAction(name, mTemplateAction); return mTemplateAction; } /****************************************************************************** * Called when the action is clicked. */ void NewAlarmAction::slotInitMenu() { // Don't allow shell commands in kiosk mode mCommandAction->setEnabled(ShellProcess::authorised()); } /****************************************************************************** * Called when an alarm type is selected from the New popup menu. */ void NewAlarmAction::slotSelected(QAction* action) { QMap::ConstIterator it = mTypes.constFind(action); if (it != mTypes.constEnd()) Q_EMIT selected(it.value()); } /****************************************************************************** * Called when the status of a calendar has changed. * Enable or disable the New From Template action appropriately. */ void NewAlarmAction::slotCalendarStatusChanged() { // Find whether there are any writable active alarm calendars bool active = !Resources::enabledResources(CalEvent::ACTIVE, true).isEmpty(); - bool haveEvents = TemplateListModel::all()->haveEvents(); + bool haveEvents = DataModel::allTemplateListModel()->haveEvents(); mTemplateAction->setEnabled(active && haveEvents); setEnabled(active); } // vim: et sw=4: diff --git a/src/resources/akonadidatamodel.cpp b/src/resources/akonadidatamodel.cpp index d831975d..ea4e8356 100644 --- a/src/resources/akonadidatamodel.cpp +++ b/src/resources/akonadidatamodel.cpp @@ -1,958 +1,956 @@ /* * akonadidatamodel.cpp - KAlarm calendar file access using Akonadi * Program: kalarm * Copyright © 2007-2019 David Jarvie * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "akonadidatamodel.h" #include "preferences.h" #include "resources/akonadiresourcemigrator.h" #include "resources/resources.h" #include "lib/synchtimer.h" #include "kalarm_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace Akonadi; using namespace KAlarmCal; // Ensure ResourceDataModelBase::UserRole is valid. ResourceDataModelBase does // not include Akonadi headers, so here we check that it has been set to be // compatible with EntityTreeModel::UserRole. static_assert((int)ResourceDataModelBase::UserRole>=(int)Akonadi::EntityTreeModel::UserRole, "ResourceDataModelBase::UserRole wrong value"); /*============================================================================= = Class: AkonadiDataModel =============================================================================*/ AkonadiDataModel* AkonadiDataModel::mInstance = nullptr; int AkonadiDataModel::mTimeHourPos = -2; /****************************************************************************** * Construct and return the singleton. */ AkonadiDataModel* AkonadiDataModel::instance() { if (!mInstance) mInstance = new AkonadiDataModel(new ChangeRecorder(qApp), qApp); return mInstance; } /****************************************************************************** * Constructor. */ AkonadiDataModel::AkonadiDataModel(ChangeRecorder* monitor, QObject* parent) : EntityTreeModel(monitor, parent) , ResourceDataModelBase() , mMonitor(monitor) { // Populate all collections, selected/enabled or unselected/disabled. setItemPopulationStrategy(ImmediatePopulation); // Restrict monitoring to collections containing the KAlarm mime types monitor->setCollectionMonitored(Collection::root()); monitor->setResourceMonitored("akonadi_kalarm_resource"); monitor->setResourceMonitored("akonadi_kalarm_dir_resource"); monitor->setMimeTypeMonitored(KAlarmCal::MIME_ACTIVE); monitor->setMimeTypeMonitored(KAlarmCal::MIME_ARCHIVED); monitor->setMimeTypeMonitored(KAlarmCal::MIME_TEMPLATE); monitor->itemFetchScope().fetchFullPayload(); monitor->itemFetchScope().fetchAttribute(); AttributeFactory::registerAttribute(); AttributeFactory::registerAttribute(); AttributeFactory::registerAttribute(); connect(monitor, SIGNAL(collectionChanged(Akonadi::Collection,QSet)), SLOT(slotCollectionChanged(Akonadi::Collection,QSet))); connect(monitor, &Monitor::collectionRemoved, this, &AkonadiDataModel::slotCollectionRemoved); initResourceMigrator(); MinuteTimer::connect(this, SLOT(slotUpdateTimeTo())); Preferences::connect(SIGNAL(archivedColourChanged(QColor)), this, SLOT(slotUpdateArchivedColour(QColor))); Preferences::connect(SIGNAL(disabledColourChanged(QColor)), this, SLOT(slotUpdateDisabledColour(QColor))); Preferences::connect(SIGNAL(holidaysChanged(KHolidays::HolidayRegion)), this, SLOT(slotUpdateHolidays())); Preferences::connect(SIGNAL(workTimeChanged(QTime,QTime,QBitArray)), this, SLOT(slotUpdateWorkingHours())); connect(Resources::instance(), &Resources::resourceMessage, this, &AkonadiDataModel::slotResourceMessage, Qt::QueuedConnection); connect(this, &AkonadiDataModel::rowsInserted, this, &AkonadiDataModel::slotRowsInserted); connect(this, &AkonadiDataModel::rowsAboutToBeRemoved, this, &AkonadiDataModel::slotRowsAboutToBeRemoved); connect(this, &Akonadi::EntityTreeModel::collectionTreeFetched, this, &AkonadiDataModel::slotCollectionTreeFetched); connect(this, &Akonadi::EntityTreeModel::collectionPopulated, this, &AkonadiDataModel::slotCollectionPopulated); connect(monitor, &Monitor::itemChanged, this, &AkonadiDataModel::slotMonitoredItemChanged); connect(ServerManager::self(), &ServerManager::stateChanged, this, &AkonadiDataModel::checkResources); checkResources(ServerManager::state()); } /****************************************************************************** * Destructor. */ AkonadiDataModel::~AkonadiDataModel() { if (mInstance == this) mInstance = nullptr; } /****************************************************************************** * Called when the server manager changes state. * If it is now running, i.e. the agent manager knows about * all existing resources. * Once it is running, i.e. the agent manager knows about * all existing resources, if necessary migrate any KResources alarm calendars from * pre-Akonadi versions of KAlarm, or create default Akonadi calendar resources * if any are missing. */ void AkonadiDataModel::checkResources(ServerManager::State state) { switch (state) { case ServerManager::Running: if (!isMigrating() && !isMigrationComplete()) { qCDebug(KALARM_LOG) << "AkonadiDataModel::checkResources: Server running"; setMigrationInitiated(); AkonadiResourceMigrator::execute(); } break; case ServerManager::NotRunning: qCDebug(KALARM_LOG) << "AkonadiDataModel::checkResources: Server stopped"; setMigrationInitiated(false); initResourceMigrator(); Q_EMIT serverStopped(); break; default: break; } } /****************************************************************************** * Initialise the calendar migrator so that it can be run (either for the first * time, or again). */ void AkonadiDataModel::initResourceMigrator() { AkonadiResourceMigrator::reset(); connect(AkonadiResourceMigrator::instance(), &AkonadiResourceMigrator::creating, this, &AkonadiDataModel::slotCollectionBeingCreated); connect(AkonadiResourceMigrator::instance(), &QObject::destroyed, this, &AkonadiDataModel::slotMigrationCompleted); } ChangeRecorder* AkonadiDataModel::monitor() { return instance()->mMonitor; } /****************************************************************************** * Return the data for a given role, for a specified item. */ QVariant AkonadiDataModel::data(const QModelIndex& index, int role) const { if (role == ResourceIdRole) role = CollectionIdRole; if (roleHandled(role)) { const Collection collection = EntityTreeModel::data(index, CollectionRole).value(); if (collection.isValid()) { // This is a Collection row // Update the collection's resource with the current collection value. const Resource& res = updateResource(collection); bool handled; const QVariant value = resourceData(role, res, handled); if (handled) return value; } else { Item item = EntityTreeModel::data(index, ItemRole).value(); if (item.isValid()) { // This is an Item row const QString mime = item.mimeType(); if ((mime != KAlarmCal::MIME_ACTIVE && mime != KAlarmCal::MIME_ARCHIVED && mime != KAlarmCal::MIME_TEMPLATE) || !item.hasPayload()) return QVariant(); Resource res; const KAEvent ev(event(item, index, res)); // this sets item.parentCollection() if (role == ParentResourceIdRole) return item.parentCollection().id(); bool handled; const QVariant value = eventData(role, index.column(), ev, res, handled); if (handled) return value; } } } return EntityTreeModel::data(index, role); } /****************************************************************************** * Return the number of columns for either a collection or an item. */ int AkonadiDataModel::entityColumnCount(HeaderGroup group) const { switch (group) { case CollectionTreeHeaders: return 1; case ItemListHeaders: return ColumnCount; default: return EntityTreeModel::entityColumnCount(group); } } /****************************************************************************** * Return offset to add to headerData() role, for item models. */ int AkonadiDataModel::headerDataEventRoleOffset() const { return TerminalUserRole * ItemListHeaders; } /****************************************************************************** * Return data for a column heading. */ QVariant AkonadiDataModel::entityHeaderData(int section, Qt::Orientation orientation, int role, HeaderGroup group) const { bool eventHeaders = false; switch (group) { case ItemListHeaders: eventHeaders = true; Q_FALLTHROUGH(); // fall through to CollectionTreeHeaders case CollectionTreeHeaders: { bool handled; const QVariant value = ResourceDataModelBase::headerData(section, orientation, role, eventHeaders, handled); if (handled) return value; break; } default: break; } return EntityTreeModel::entityHeaderData(section, orientation, role, group); } /****************************************************************************** * Recursive function to Q_EMIT the dataChanged() signal for all items in a * specified column range. */ void AkonadiDataModel::signalDataChanged(bool (*checkFunc)(const Item&), int startColumn, int endColumn, const QModelIndex& parent) { int start = -1; int end = -1; for (int row = 0, count = rowCount(parent); row < count; ++row) { const QModelIndex ix = index(row, 0, parent); const Item item = ix.data(ItemRole).value(); const bool isItem = item.isValid(); if (isItem) { if ((*checkFunc)(item)) { // For efficiency, Q_EMIT a single signal for each group of // consecutive items, rather than a separate signal for each item. if (start < 0) start = row; end = row; continue; } } if (start >= 0) Q_EMIT dataChanged(index(start, startColumn, parent), index(end, endColumn, parent)); start = -1; if (!isItem) signalDataChanged(checkFunc, startColumn, endColumn, ix); } if (start >= 0) Q_EMIT dataChanged(index(start, startColumn, parent), index(end, endColumn, parent)); } /****************************************************************************** * Signal every minute that the time-to-alarm values have changed. */ static bool checkItem_isActive(const Item& item) { return item.mimeType() == KAlarmCal::MIME_ACTIVE; } void AkonadiDataModel::slotUpdateTimeTo() { signalDataChanged(&checkItem_isActive, TimeToColumn, TimeToColumn, QModelIndex()); } /****************************************************************************** * Called when the colour used to display archived alarms has changed. */ static bool checkItem_isArchived(const Item& item) { return item.mimeType() == KAlarmCal::MIME_ARCHIVED; } void AkonadiDataModel::slotUpdateArchivedColour(const QColor&) { qCDebug(KALARM_LOG) << "AkonadiDataModel::slotUpdateArchivedColour"; signalDataChanged(&checkItem_isArchived, 0, ColumnCount - 1, QModelIndex()); } /****************************************************************************** * Called when the colour used to display disabled alarms has changed. */ static bool checkItem_isDisabled(const Item& item) { if (item.hasPayload()) { const KAEvent event = item.payload(); if (event.isValid()) return !event.enabled(); } return false; } void AkonadiDataModel::slotUpdateDisabledColour(const QColor&) { qCDebug(KALARM_LOG) << "AkonadiDataModel::slotUpdateDisabledColour"; signalDataChanged(&checkItem_isDisabled, 0, ColumnCount - 1, QModelIndex()); } /****************************************************************************** * Called when the definition of holidays has changed. */ static bool checkItem_excludesHolidays(const Item& item) { if (item.hasPayload()) { const KAEvent event = item.payload(); if (event.isValid() && event.holidaysExcluded()) return true; } return false; } void AkonadiDataModel::slotUpdateHolidays() { qCDebug(KALARM_LOG) << "AkonadiDataModel::slotUpdateHolidays"; Q_ASSERT(TimeToColumn == TimeColumn + 1); // signal should be emitted only for TimeTo and Time columns signalDataChanged(&checkItem_excludesHolidays, TimeColumn, TimeToColumn, QModelIndex()); } /****************************************************************************** * Called when the definition of working hours has changed. */ static bool checkItem_workTimeOnly(const Item& item) { if (item.hasPayload()) { const KAEvent event = item.payload(); if (event.isValid() && event.workTimeOnly()) return true; } return false; } void AkonadiDataModel::slotUpdateWorkingHours() { qCDebug(KALARM_LOG) << "AkonadiDataModel::slotUpdateWorkingHours"; Q_ASSERT(TimeToColumn == TimeColumn + 1); // signal should be emitted only for TimeTo and Time columns signalDataChanged(&checkItem_workTimeOnly, TimeColumn, TimeToColumn, QModelIndex()); } /****************************************************************************** * Reload a collection from Akonadi storage. The backend data is not reloaded. */ -bool AkonadiDataModel::reloadResource(const Resource& resource) +bool AkonadiDataModel::reload(const Resource& resource) { if (!resource.isValid()) return false; - qCDebug(KALARM_LOG) << "AkonadiDataModel::reloadResource:" << resource.id(); + qCDebug(KALARM_LOG) << "AkonadiDataModel::reload:" << resource.id(); Collection collection(resource.id()); mMonitor->setCollectionMonitored(collection, false); mMonitor->setCollectionMonitored(collection, true); return true; } /****************************************************************************** * Reload all collections from Akonadi storage. The backend data is not reloaded. */ void AkonadiDataModel::reload() { qCDebug(KALARM_LOG) << "AkonadiDataModel::reload"; const Collection::List collections = mMonitor->collectionsMonitored(); for (const Collection& collection : collections) { mMonitor->setCollectionMonitored(collection, false); mMonitor->setCollectionMonitored(collection, true); } } /****************************************************************************** * Returns the index to a specified event. */ QModelIndex AkonadiDataModel::eventIndex(const KAEvent& event) const { return itemIndex(Item(mEventIds.value(event.id()).itemId)); } /****************************************************************************** * Returns the index to a specified event. */ QModelIndex AkonadiDataModel::eventIndex(const QString& eventId) const { return itemIndex(Item(mEventIds.value(eventId).itemId)); } /****************************************************************************** * Return all events belonging to a collection. */ QList AkonadiDataModel::events(ResourceId id) const { QList list; const QModelIndex ix = modelIndexForCollection(this, Collection(id)); if (ix.isValid()) getChildEvents(ix, list); for (KAEvent& ev : list) ev.setResourceId(id); return list; } /****************************************************************************** * Recursive function to append all child Events with a given mime type. */ void AkonadiDataModel::getChildEvents(const QModelIndex& parent, QList& events) const { for (int row = 0, count = rowCount(parent); row < count; ++row) { const QModelIndex ix = index(row, 0, parent); const Item item = ix.data(ItemRole).value(); if (item.isValid()) { if (item.hasPayload()) { KAEvent event = item.payload(); if (event.isValid()) events += event; } } else { const Collection c = ix.data(CollectionRole).value(); if (c.isValid()) getChildEvents(ix, events); } } } KAEvent AkonadiDataModel::event(const QString& eventId) const { return event(eventIndex(eventId)); } KAEvent AkonadiDataModel::event(const QModelIndex& ix) const { if (!ix.isValid()) return KAEvent(); Item item = ix.data(ItemRole).value(); Resource r; return event(item, ix, r); } /****************************************************************************** * Return the event for an Item at a specified model index. * The item's parent collection is set, as is the event's collection ID. */ KAEvent AkonadiDataModel::event(Akonadi::Item& item, const QModelIndex& ix, Resource& res) const { //TODO: Tune performance: This function is called very frequently with the same parameters if (ix.isValid()) { const Collection pc = ix.data(ParentCollectionRole).value(); item.setParentCollection(pc); res = resource(pc.id()); if (res.isValid()) { // Fetch the KAEvent defined by the Item, including commandError. return AkonadiResource::event(res, item); } } res = Resource::null(); return KAEvent(); } /****************************************************************************** * Return the up to date Item for a specified Akonadi ID. */ Item AkonadiDataModel::itemById(Item::Id id) const { Item item(id); if (!refresh(item)) return Item(); return item; } /****************************************************************************** * Return the Item for a given event. */ Item AkonadiDataModel::itemForEvent(const QString& eventId) const { const QModelIndex ix = eventIndex(eventId); if (!ix.isValid()) return Item(); return ix.data(ItemRole).value(); } #if 0 /****************************************************************************** * Add an event to a specified Collection. * If the event is scheduled to be added to the collection, it is updated with * its Akonadi item ID. * The event's 'updated' flag is cleared. * Reply = true if item creation has been scheduled. */ bool AkonadiDataModel::addEvent(KAEvent& event, Resource& resource) { qCDebug(KALARM_LOG) << "AkonadiDataModel::addEvent: ID:" << event.id(); if (!resource.addEvent(event)) return false; // Note that the item ID will be inserted in mEventIds after the Akonadi // Item has been created by ItemCreateJob, when slotRowsInserted() is called. mEventIds[event.id()] = EventIds(resource.id()); return true; } #endif /****************************************************************************** * Called when rows have been inserted into the model. */ void AkonadiDataModel::slotRowsInserted(const QModelIndex& parent, int start, int end) { qCDebug(KALARM_LOG) << "AkonadiDataModel::slotRowsInserted:" << start << "-" << end << "(parent =" << parent << ")"; QHash> events; for (int row = start; row <= end; ++row) { const QModelIndex ix = index(row, 0, parent); const Collection collection = ix.data(CollectionRole).value(); if (collection.isValid()) { // A collection has been inserted. Create a new resource to hold it. qCDebug(KALARM_LOG) << "AkonadiDataModel::slotRowsInserted: Collection" << collection.id() << collection.name(); Resource& resource = updateResource(collection); // Ignore it if it isn't owned by a valid Akonadi resource. if (resource.isValid()) { setCollectionChanged(resource, collection, true); Resources::notifyNewResourceInitialised(resource); if (!collection.hasAttribute()) { // If the compatibility attribute is missing at this point, // it doesn't always get notified later, so fetch the // collection to ensure that we see it. AgentInstance agent = AgentManager::self()->instance(collection.resource()); CollectionFetchJob* job = new CollectionFetchJob(Collection::root(), CollectionFetchJob::Recursive); job->fetchScope().setResource(agent.identifier()); connect(job, &CollectionFetchJob::result, instance(), &AkonadiDataModel::collectionFetchResult); } } } else { // An item has been inserted Item item = ix.data(ItemRole).value(); if (item.isValid()) { qCDebug(KALARM_LOG) << "item id=" << item.id() << ", revision=" << item.revision(); Resource res; const KAEvent evnt = event(item, ix, res); // this sets item.parentCollection() if (evnt.isValid()) { qCDebug(KALARM_LOG) << "AkonadiDataModel::slotRowsInserted: Event" << evnt.id(); // Only notify new events if the collection is already populated. // If not populated, all events will be notified when it is // eventually populated. if (res.isPopulated()) events[res] += evnt; mEventIds[evnt.id()] = EventIds(item.parentCollection().id(), item.id()); } // Notify the resource containing the item. AkonadiResource::notifyItemChanged(res, item, true); } } } for (auto it = events.constBegin(); it != events.constEnd(); ++it) { Resource res = it.key(); AkonadiResource::notifyEventsChanged(res, it.value()); } } /****************************************************************************** * Called when a CollectionFetchJob has completed. * Check for and process changes in attribute values. */ void AkonadiDataModel::collectionFetchResult(KJob* j) { CollectionFetchJob* job = qobject_cast(j); if (j->error()) qCWarning(KALARM_LOG) << "AkonadiDataModel::collectionFetchResult: CollectionFetchJob" << job->fetchScope().resource()<< "error: " << j->errorString(); else { const Collection::List collections = job->collections(); for (const Collection& c : collections) { qCDebug(KALARM_LOG) << "AkonadiDataModel::collectionFetchResult:" << c.id(); auto it = mResources.find(c.id()); if (it == mResources.end()) continue; Resource& resource = it.value(); setCollectionChanged(resource, c, false); } } } /****************************************************************************** * Called when rows are about to be removed from the model. */ void AkonadiDataModel::slotRowsAboutToBeRemoved(const QModelIndex& parent, int start, int end) { qCDebug(KALARM_LOG) << "AkonadiDataModel::slotRowsAboutToBeRemoved:" << start << "-" << end << "(parent =" << parent << ")"; QHash> events; for (int row = start; row <= end; ++row) { const QModelIndex ix = index(row, 0, parent); Item item = ix.data(ItemRole).value(); Resource res; const KAEvent evnt = event(item, ix, res); // this sets item.parentCollection() if (evnt.isValid()) { qCDebug(KALARM_LOG) << "AkonadiDataModel::slotRowsAboutToBeRemoved: Collection:" << item.parentCollection().id() << ", Event ID:" << evnt.id(); events[res] += evnt; mEventIds.remove(evnt.id()); } } for (auto it = events.constBegin(); it != events.constEnd(); ++it) { Resource res = it.key(); AkonadiResource::notifyEventsToBeDeleted(res, it.value()); } } /****************************************************************************** * Called when a monitored collection has changed. * Updates the collection held by the collection's resource, and notifies * changes of interest. */ void AkonadiDataModel::slotCollectionChanged(const Akonadi::Collection& c, const QSet& attributeNames) { qCDebug(KALARM_LOG) << "AkonadiDataModel::slotCollectionChanged:" << c.id() << attributeNames; auto it = mResources.find(c.id()); if (it != mResources.end()) { // The Monitor::collectionChanged() signal is not always emitted when // attributes are created! So check whether any attributes not // included in 'attributeNames' have been created. Resource& resource = it.value(); setCollectionChanged(resource, c, attributeNames.contains(CompatibilityAttribute::name())); } } /****************************************************************************** * Called when a monitored collection's properties or content have changed. * Optionally emits a signal if properties of interest have changed. */ void AkonadiDataModel::setCollectionChanged(Resource& resource, const Collection& collection, bool checkCompat) { AkonadiResource::notifyCollectionChanged(resource, collection, checkCompat); if (isMigrating()) { mCollectionIdsBeingCreated.removeAll(collection.id()); if (mCollectionsBeingCreated.isEmpty() && mCollectionIdsBeingCreated.isEmpty() && AkonadiResourceMigrator::completed()) { qCDebug(KALARM_LOG) << "AkonadiDataModel::setCollectionChanged: Migration completed"; setMigrationComplete(); - Q_EMIT migrationCompleted(); } } } /****************************************************************************** * Called when a monitored collection is removed. */ void AkonadiDataModel::slotCollectionRemoved(const Collection& collection) { const Collection::Id id = collection.id(); qCDebug(KALARM_LOG) << "AkonadiDataModel::slotCollectionRemoved:" << id; mResources.remove(collection.id()); // AkonadiResource will remove the resource from Resources. } /****************************************************************************** * Called when a collection creation is about to start, or has completed. */ void AkonadiDataModel::slotCollectionBeingCreated(const QString& path, Akonadi::Collection::Id id, bool finished) { if (finished) { mCollectionsBeingCreated.removeAll(path); mCollectionIdsBeingCreated << id; } else mCollectionsBeingCreated << path; } /****************************************************************************** * Called when the collection tree has been fetched for the first time. */ void AkonadiDataModel::slotCollectionTreeFetched() { Resources::notifyResourcesCreated(); } /****************************************************************************** * Called when a collection has been populated. */ void AkonadiDataModel::slotCollectionPopulated(Akonadi::Collection::Id id) { qCDebug(KALARM_LOG) << "AkonadiDataModel::slotCollectionPopulated:" << id; AkonadiResource::notifyCollectionLoaded(id, events(id)); } /****************************************************************************** * Called when calendar migration has completed. */ void AkonadiDataModel::slotMigrationCompleted() { if (mCollectionsBeingCreated.isEmpty() && mCollectionIdsBeingCreated.isEmpty()) { qCDebug(KALARM_LOG) << "AkonadiDataModel: Migration completed"; setMigrationComplete(); - Q_EMIT migrationCompleted(); } } /****************************************************************************** * Called when an item in the monitored collections has changed. */ void AkonadiDataModel::slotMonitoredItemChanged(const Akonadi::Item& item, const QSet&) { qCDebug(KALARM_LOG) << "AkonadiDataModel::slotMonitoredItemChanged: item id=" << item.id() << ", revision=" << item.revision(); const QModelIndex ix = itemIndex(item); if (ix.isValid()) { Resource res; Item itm = item; KAEvent evnt = event(itm, ix, res); // this sets item.parentCollection() if (evnt.isValid()) { // Notify the resource containing the item. if (res.isValid()) AkonadiResource::notifyItemChanged(res, itm, false); // Wait to ensure that the base EntityTreeModel has processed the // itemChanged() signal first, before we notify AkonadiResource // that the event has changed. mPendingEventChanges.enqueue(evnt); QTimer::singleShot(0, this, &AkonadiDataModel::slotEmitEventUpdated); } } } /****************************************************************************** * Called to Q_EMIT a signal when an event in the monitored collections has * changed. */ void AkonadiDataModel::slotEmitEventUpdated() { while (!mPendingEventChanges.isEmpty()) { const KAEvent event = mPendingEventChanges.dequeue(); Resource res = Resources::resource(event.resourceId()); AkonadiResource::notifyEventsChanged(res, {event}); } } /****************************************************************************** * Refresh the specified Collection with up to date data. * Return: true if successful, false if collection not found. */ bool AkonadiDataModel::refresh(Akonadi::Collection& collection) const { const QModelIndex ix = modelIndexForCollection(this, collection); if (!ix.isValid()) return false; collection = ix.data(CollectionRole).value(); // Also update our own copy of the collection. updateResource(collection); return true; } /****************************************************************************** * Refresh the specified Item with up to date data. * Return: true if successful, false if item not found. */ bool AkonadiDataModel::refresh(Akonadi::Item& item) const { const QModelIndex ix = itemIndex(item); if (!ix.isValid()) return false; item = ix.data(ItemRole).value(); return true; } /****************************************************************************** * Return the AkonadiResource object for a collection ID. */ Resource AkonadiDataModel::resource(Collection::Id id) const { return mResources.value(id, AkonadiResource::nullResource()); } /****************************************************************************** * Return the resource at a specified index, with up to date data. */ Resource AkonadiDataModel::resource(const QModelIndex& ix) const { return mResources.value(ix.data(CollectionIdRole).toLongLong(), AkonadiResource::nullResource()); } /****************************************************************************** * Find the QModelIndex of a resource. */ QModelIndex AkonadiDataModel::resourceIndex(const Resource& resource) const { const Collection& collection = AkonadiResource::collection(resource); const QModelIndex ix = modelIndexForCollection(this, collection); if (!ix.isValid()) return QModelIndex(); return ix; } /****************************************************************************** * Find the QModelIndex of a resource with a given ID. */ QModelIndex AkonadiDataModel::resourceIndex(Akonadi::Collection::Id id) const { const QModelIndex ix = modelIndexForCollection(this, Collection(id)); if (!ix.isValid()) return QModelIndex(); return ix; } /****************************************************************************** * Return a reference to the collection held in a Resource. This is the * definitive copy of the collection used by this model. * Return: the collection held by the model, or null if not found. */ Collection* AkonadiDataModel::collection(Collection::Id id) const { auto it = mResources.find(id); if (it != mResources.end()) { Collection& c = AkonadiResource::collection(it.value()); if (c.isValid()) return &c; } return nullptr; } /****************************************************************************** * Return a reference to the collection held in a Resource. This is the * definitive copy of the collection used by this model. * Return: the collection held by the model, or null if not found. */ Collection* AkonadiDataModel::collection(const Resource& resource) const { return collection(resource.id()); } /****************************************************************************** * Find the QModelIndex of an item. */ QModelIndex AkonadiDataModel::itemIndex(const Akonadi::Item& item) const { const QModelIndexList ixs = modelIndexesForItem(this, item); for (const QModelIndex& ix : ixs) { if (ix.isValid()) return ix; } return QModelIndex(); } /****************************************************************************** * Update the resource which holds a given Collection, by copying the Collection * value into it. If there is no resource, a new resource is created. * Param: collection - this should have been fetched from the model to ensure * that its value is up to date. */ Resource& AkonadiDataModel::updateResource(const Collection& collection) const { auto it = mResources.find(collection.id()); if (it != mResources.end()) { Collection& resourceCol = AkonadiResource::collection(it.value()); if (&collection != &resourceCol) resourceCol = collection; } else { // Create a new resource for the collection. it = mResources.insert(collection.id(), AkonadiResource::create(collection)); } return it.value(); } /****************************************************************************** * Display a message to the user. */ void AkonadiDataModel::slotResourceMessage(Resource&, ResourceType::MessageType type, const QString& message, const QString& details) { handleResourceMessage(type, message, details); } // vim: et sw=4: diff --git a/src/resources/akonadidatamodel.h b/src/resources/akonadidatamodel.h index 2c044ab1..bb210d84 100644 --- a/src/resources/akonadidatamodel.h +++ b/src/resources/akonadidatamodel.h @@ -1,192 +1,189 @@ /* * akonadidatamodel.h - KAlarm calendar file access using Akonadi * Program: kalarm * Copyright © 2010-2019 David Jarvie * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef AKONADIDATAMODEL_H #define AKONADIDATAMODEL_H #include "resources/resourcedatamodelbase.h" #include "resources/akonadiresource.h" #include #include #include #include #include #include namespace Akonadi { class ChangeRecorder; } class KJob; using namespace KAlarmCal; class AkonadiDataModel : public Akonadi::EntityTreeModel, public ResourceDataModelBase { Q_OBJECT public: enum Change { Enabled, ReadOnly, AlarmTypes }; static AkonadiDataModel* instance(); ~AkonadiDataModel() override; static Akonadi::ChangeRecorder* monitor(); /** Refresh the specified collection instance with up to date data. */ bool refresh(Akonadi::Collection&) const; /** Refresh the specified item instance with up to date data. */ bool refresh(Akonadi::Item&) const; Resource resource(Akonadi::Collection::Id) const; Resource resource(const QModelIndex&) const; QModelIndex resourceIndex(const Resource&) const; QModelIndex resourceIndex(Akonadi::Collection::Id) const; Akonadi::Collection* collection(Akonadi::Collection::Id id) const; Akonadi::Collection* collection(const Resource&) const; /** Reload a collection's data from Akonadi storage (not from the backend). */ - bool reloadResource(const Resource&); + bool reload(const Resource&); /** Reload all collections' data from Akonadi storage (not from the backend). */ void reload(); KAEvent event(const QString& eventId) const; KAEvent event(const QModelIndex&) const; using QObject::event; // prevent warning about hidden virtual method /** Return an event's model index, based on its ID. */ QModelIndex eventIndex(const KAEvent&) const; QModelIndex eventIndex(const QString& eventId) const; /** Return the up-to-date Item, given its ID. * If not found, an invalid Item is returned. */ Akonadi::Item itemById(Akonadi::Item::Id) const; /** Return the Item for a given event. */ Akonadi::Item itemForEvent(const QString& eventId) const; QVariant data(const QModelIndex&, int role = Qt::DisplayRole) const override; int headerDataEventRoleOffset() const override; private Q_SLOTS: /** Called when a resource notifies a message to display to the user. */ void slotResourceMessage(Resource&, ResourceType::MessageType, const QString& message, const QString& details); Q_SIGNALS: - /** Signal emitted when calendar migration/creation has completed. */ - void migrationCompleted(); - /** Signal emitted when the Akonadi server has stopped. */ void serverStopped(); protected: QVariant entityHeaderData(int section, Qt::Orientation, int role, HeaderGroup) const override; int entityColumnCount(HeaderGroup) const override; private Q_SLOTS: void checkResources(Akonadi::ServerManager::State); void slotMigrationCompleted(); void collectionFetchResult(KJob*); void slotCollectionChanged(const Akonadi::Collection& c, const QSet& attrNames); void slotCollectionRemoved(const Akonadi::Collection&); void slotCollectionBeingCreated(const QString& path, Akonadi::Collection::Id, bool finished); void slotCollectionTreeFetched(); void slotCollectionPopulated(Akonadi::Collection::Id); void slotUpdateTimeTo(); void slotUpdateArchivedColour(const QColor&); void slotUpdateDisabledColour(const QColor&); void slotUpdateHolidays(); void slotUpdateWorkingHours(); void slotRowsInserted(const QModelIndex& parent, int start, int end); void slotRowsAboutToBeRemoved(const QModelIndex& parent, int start, int end); void slotMonitoredItemChanged(const Akonadi::Item&, const QSet&); void slotEmitEventUpdated(); private: struct CalData // data per collection { CalData() : enabled(false) { } CalData(bool e, const QColor& c) : colour(c), enabled(e) { } QColor colour; // user selected color for the calendar bool enabled; // whether the collection is enabled }; struct CollJobData // collection data for jobs in progress { CollJobData() : id(-1) {} CollJobData(Akonadi::Collection::Id i, const QString& d) : id(i), displayName(d) {} Akonadi::Collection::Id id; QString displayName; }; struct CollTypeData // data for configuration dialog for collection creation job { CollTypeData() : parent(nullptr), alarmType(CalEvent::EMPTY) {} CollTypeData(CalEvent::Type t, QWidget* p) : parent(p), alarmType(t) {} QWidget* parent; CalEvent::Type alarmType; }; AkonadiDataModel(Akonadi::ChangeRecorder*, QObject* parent); void initResourceMigrator(); Resource& updateResource(const Akonadi::Collection&) const; /** Return the alarm for the specified Akonadi Item. * The item's parentCollection() is set. * @param res Set the resource for the item's parent collection. * @return the event, or invalid event if no such event exists. */ KAEvent event(Akonadi::Item&, const QModelIndex&, Resource& res) const; QModelIndex itemIndex(const Akonadi::Item&) const; void signalDataChanged(bool (*checkFunc)(const Akonadi::Item&), int startColumn, int endColumn, const QModelIndex& parent); void setCollectionChanged(Resource&, const Akonadi::Collection&, bool checkCompat); QList events(ResourceId) const; void getChildEvents(const QModelIndex& parent, QList&) const; static AkonadiDataModel* mInstance; static int mTimeHourPos; // position of hour within time string, or -1 if leading zeroes included Akonadi::ChangeRecorder* mMonitor; QHash mPendingCollectionJobs; // pending collection creation/deletion jobs, with collection ID & name QHash mPendingColCreateJobs; // default alarm type for pending collection creation jobs QList mCollectionsBeingCreated; // path names of new collections being created by migrator QList mCollectionIdsBeingCreated; // ids of new collections being created by migrator struct EventIds { Akonadi::Collection::Id collectionId{-1}; Akonadi::Item::Id itemId{-1}; explicit EventIds(Akonadi::Collection::Id c = -1, Akonadi::Item::Id i = -1) : collectionId(c), itemId(i) {} }; QHash mEventIds; // collection and item ID for each event ID mutable QHash mResources; QQueue mPendingEventChanges; // changed events with changedEvent() signal pending }; #endif // AKONADIDATAMODEL_H // vim: et sw=4: diff --git a/src/resources/akonadiresource.cpp b/src/resources/akonadiresource.cpp index eae89a83..a9600ee1 100644 --- a/src/resources/akonadiresource.cpp +++ b/src/resources/akonadiresource.cpp @@ -1,983 +1,973 @@ /* * akonadiresource.cpp - class for an Akonadi alarm calendar resource * Program: kalarm * Copyright © 2019 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 "akonadiresource.h" #include "resources.h" #include "akonadidatamodel.h" #include "akonadiresourcemigrator.h" #include "lib/autoqpointer.h" #include "kalarm_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include using namespace Akonadi; #include #include namespace { const QString KALARM_RESOURCE(QStringLiteral("akonadi_kalarm_resource")); const QString KALARM_DIR_RESOURCE(QStringLiteral("akonadi_kalarm_dir_resource")); const Collection::Rights WritableRights = Collection::CanChangeItem | Collection::CanCreateItem | Collection::CanDeleteItem; const QRegularExpression MatchMimeType(QStringLiteral("^application/x-vnd\\.kde\\.alarm.*"), QRegularExpression::DotMatchesEverythingOption); } // Class to provide an object for removeDuplicateResources() signals to be received. class DuplicateResourceObject : public QObject { Q_OBJECT public: DuplicateResourceObject(QObject* parent = nullptr) : QObject(parent) {} void reset() { mAgentPaths.clear(); } public Q_SLOTS: void collectionFetchResult(KJob*); private: struct ResourceCol { QString resourceId; // Akonadi resource identifier ResourceId collectionId; // Akonadi collection ID ResourceCol() {} ResourceCol(const QString& r, ResourceId c) : resourceId(r), collectionId(c) {} }; QHash mAgentPaths; // path, (resource identifier, collection ID) pairs }; DuplicateResourceObject* AkonadiResource::mDuplicateResourceObject{nullptr}; Resource AkonadiResource::create(const Akonadi::Collection& collection) { if (collection.id() < 0 || collection.remoteId().isEmpty()) return Resource::null(); // return invalid Resource Resource resource = Resources::resource(collection.id()); if (!resource.isValid()) { // A resource with this ID doesn't exist, so create a new resource. addResource(new AkonadiResource(collection), resource); } return resource; } AkonadiResource::AkonadiResource(const Collection& collection) : ResourceType(collection.id()) , mCollection(collection) , mValid(collection.id() >= 0 && !collection.remoteId().isEmpty()) { if (mValid) { // Fetch collection data, including remote ID, resource and mime types and // current attributes. fetchCollectionAttribute(false); // If the collection doesn't belong to a resource, it can't be used. mValid = AgentManager::self()->instance(mCollection.resource()).isValid(); connect(AkonadiDataModel::monitor(), &Monitor::collectionRemoved, this, &AkonadiResource::slotCollectionRemoved); } } AkonadiResource::~AkonadiResource() { } Resource AkonadiResource::nullResource() { static Resource nullRes(new AkonadiResource(Collection())); return nullRes; } bool AkonadiResource::isValid() const { // The collection ID must not have changed since construction. return mValid && id() >= 0 && mCollection.id() == id(); } Akonadi::Collection AkonadiResource::collection() const { return mCollection; } ResourceType::StorageType AkonadiResource::storageType() const { const QString id = AgentManager::self()->instance(mCollection.resource()).type().identifier(); if (id == KALARM_RESOURCE) return File; if (id == KALARM_DIR_RESOURCE) return Directory; return NoStorage; } QString AkonadiResource::storageTypeString(bool description) const { const AgentType agentType = AgentManager::self()->instance(mCollection.resource()).type(); if (!agentType.isValid()) return QString(); if (description) return agentType.name(); bool local = true; bool dir = false; if (agentType.identifier() == KALARM_DIR_RESOURCE) dir = true; else local = location().isLocalFile(); return storageTypeStr(false, !dir, local); } QUrl AkonadiResource::location() const { return QUrl::fromUserInput(mCollection.remoteId(), QString(), QUrl::AssumeLocalFile); } QString AkonadiResource::displayLocation() const { // Don't simply use remoteId() since that may contain "file://" prefix, and percent encoding. return location().toDisplayString(QUrl::PrettyDecoded | QUrl::PreferLocalFile); } QString AkonadiResource::displayName() const { return mCollection.displayName(); } QString AkonadiResource::configName() const { return mCollection.resource(); } CalEvent::Types AkonadiResource::alarmTypes() const { if (!mValid) return CalEvent::EMPTY; return CalEvent::types(mCollection.contentMimeTypes()); } CalEvent::Types AkonadiResource::enabledTypes() const { if (!mValid) return CalEvent::EMPTY; if (!mHaveCollectionAttribute) fetchCollectionAttribute(true); return mCollectionAttribute.enabled(); } void AkonadiResource::setEnabled(CalEvent::Type type, bool enabled) { const CalEvent::Types types = enabledTypes(); const CalEvent::Types newTypes = enabled ? types | type : types & ~type; if (newTypes != types) setEnabled(newTypes); } void AkonadiResource::setEnabled(CalEvent::Types types) { if (!mHaveCollectionAttribute) fetchCollectionAttribute(true); const bool newAttr = !mCollection.hasAttribute(); if (mHaveCollectionAttribute && mCollectionAttribute.enabled() == types) return; // no change qCDebug(KALARM_LOG) << "AkonadiResource:" << mCollection.id() << "Set enabled:" << types << " was=" << mCollectionAttribute.enabled(); mCollectionAttribute.setEnabled(types); mHaveCollectionAttribute = true; if (newAttr) { // Akonadi often doesn't notify changes to the enabled status // (surely a bug?), so ensure that the change is noticed. mNewEnabled = true; } modifyCollectionAttribute(); } bool AkonadiResource::readOnly() const { AkonadiDataModel::instance()->refresh(mCollection); // update with latest data return (mCollection.rights() & WritableRights) != WritableRights; } int AkonadiResource::writableStatus(CalEvent::Type type) const { if (!mValid) return -1; AkonadiDataModel::instance()->refresh(mCollection); // update with latest data if ((type == CalEvent::EMPTY && !enabledTypes()) || (type != CalEvent::EMPTY && !isEnabled(type))) return -1; if ((mCollection.rights() & WritableRights) != WritableRights) return -1; if (!mCollection.hasAttribute()) return -1; switch (mCollection.attribute()->compatibility()) { case KACalendar::Current: return 1; case KACalendar::Converted: case KACalendar::Convertible: return 0; default: return -1; } } bool AkonadiResource::keepFormat() const { if (!mValid) return false; if (!mHaveCollectionAttribute) fetchCollectionAttribute(true); return mCollectionAttribute.keepFormat(); } void AkonadiResource::setKeepFormat(bool keep) { if (!mHaveCollectionAttribute) fetchCollectionAttribute(true); if (mHaveCollectionAttribute && mCollectionAttribute.keepFormat() == keep) return; // no change mCollectionAttribute.setKeepFormat(keep); mHaveCollectionAttribute = true; modifyCollectionAttribute(); } QColor AkonadiResource::backgroundColour() const { if (!mValid) return QColor(); if (!mHaveCollectionAttribute) fetchCollectionAttribute(true); return mCollectionAttribute.backgroundColor(); } void AkonadiResource::setBackgroundColour(const QColor& colour) { if (!mHaveCollectionAttribute) fetchCollectionAttribute(true); if (mHaveCollectionAttribute && mCollectionAttribute.backgroundColor() == colour) return; // no change mCollectionAttribute.setBackgroundColor(colour); mHaveCollectionAttribute = true; modifyCollectionAttribute(); } bool AkonadiResource::configIsStandard(CalEvent::Type type) const { if (!mValid) return false; if (!mHaveCollectionAttribute) fetchCollectionAttribute(true); return mCollectionAttribute.isStandard(type); } CalEvent::Types AkonadiResource::configStandardTypes() const { if (!mValid) return CalEvent::EMPTY; if (!mHaveCollectionAttribute) fetchCollectionAttribute(true); return mCollectionAttribute.standard(); } void AkonadiResource::configSetStandard(CalEvent::Type type, bool standard) { if (!mHaveCollectionAttribute) fetchCollectionAttribute(true); if (mHaveCollectionAttribute && mCollectionAttribute.isStandard(type) == standard) return; // no change mCollectionAttribute.setStandard(type, standard); mHaveCollectionAttribute = true; modifyCollectionAttribute(); } void AkonadiResource::configSetStandard(CalEvent::Types types) { if (!mHaveCollectionAttribute) fetchCollectionAttribute(true); if (mHaveCollectionAttribute && mCollectionAttribute.standard() == types) return; // no change mCollectionAttribute.setStandard(types); mHaveCollectionAttribute = true; modifyCollectionAttribute(); } KACalendar::Compat AkonadiResource::compatibilityVersion(QString& versionString) const { versionString.clear(); if (!mValid) return KACalendar::Incompatible; AkonadiDataModel::instance()->refresh(mCollection); // update with latest data if (!mCollection.hasAttribute()) return KACalendar::Incompatible; const CompatibilityAttribute* attr = mCollection.attribute(); versionString = KAlarmCal::getVersionString(attr->version()); return attr->compatibility(); } /****************************************************************************** * Update the resource to the current KAlarm storage format. */ bool AkonadiResource::updateStorageFormat() { //TODO: implement updateStorageFormat(): see AkonadiResourceMigrator::updateStorageFormat() return false; } /****************************************************************************** * Edit the resource's configuration. */ void AkonadiResource::editResource(QWidget* dialogParent) { if (isValid()) { AgentInstance instance = AgentManager::self()->instance(configName()); if (instance.isValid()) { // Use AutoQPointer to guard against crash on application exit while // the event loop is still running. It prevents double deletion (both // on deletion of parent, and on return from this function). AutoQPointer dlg = new AgentConfigurationDialog(instance, dialogParent); dlg->exec(); } } } /****************************************************************************** * Remove the resource. The calendar file is not removed. * @return true if the resource has been removed or a removal job has been scheduled. * @note The instance will be invalid once it has been removed. */ bool AkonadiResource::removeResource() { if (!isValid()) return false; qCDebug(KALARM_LOG) << "AkonadiResource::removeResource:" << id(); notifyDeletion(); // Note: Don't use CollectionDeleteJob, since that also deletes the backend storage. AgentManager* agentManager = AgentManager::self(); const AgentInstance instance = agentManager->instance(configName()); if (instance.isValid()) agentManager->removeInstance(instance); // The instance will be removed from Resources by slotCollectionRemoved(). return true; } /****************************************************************************** * Called when a monitored collection is removed. * If it's this resource, invalidate the resource and remove it from Resources. */ void AkonadiResource::slotCollectionRemoved(const Collection& collection) { if (collection.id() == id()) { qCDebug(KALARM_LOG) << "AkonadiResource::slotCollectionRemoved:" << id(); disconnect(AkonadiDataModel::monitor(), nullptr, this, nullptr); ResourceType::removeResource(collection.id()); } } bool AkonadiResource::load(bool readThroughCache) { Q_UNUSED(readThroughCache); AgentManager::self()->instance(mCollection.resource()).synchronize(); return true; } bool AkonadiResource::isPopulated() const { if (!ResourceType::isPopulated()) { const QModelIndex ix = AkonadiDataModel::instance()->resourceIndex(mCollection.id()); if (!ix.data(AkonadiDataModel::IsPopulatedRole).toBool()) return false; setLoaded(true); } return true; } bool AkonadiResource::save(bool writeThroughCache) { Q_UNUSED(writeThroughCache); AgentManager::self()->instance(mCollection.resource()).synchronize(); return true; } #if 0 /** Reload the resource. Any cached data is first discarded. */ bool reload() override; #endif /****************************************************************************** * Add an event to the resource, and add it to Akonadi. */ bool AkonadiResource::addEvent(const KAEvent& event) { qCDebug(KALARM_LOG) << "AkonadiResource::addEvent: ID:" << event.id(); Item item; if (!KAlarmCal::setItemPayload(item, event, mCollection.contentMimeTypes())) { qCWarning(KALARM_LOG) << "AkonadiResource::addEvent: Invalid mime type for collection"; return false; } ItemCreateJob* job = new ItemCreateJob(item, mCollection); connect(job, &ItemCreateJob::result, this, &AkonadiResource::itemJobDone); mPendingItemJobs[job] = -1; // the Item doesn't have an ID yet job->start(); return true; } /****************************************************************************** * Update an event in the resource, and update it in Akonadi. * Its UID must be unchanged. */ bool AkonadiResource::updateEvent(const KAEvent& event) { qCDebug(KALARM_LOG) << "AkonadiResource::updateEvent:" << event.id(); Item item = AkonadiDataModel::instance()->itemForEvent(event.id()); if (!item.isValid()) return false; if (!KAlarmCal::setItemPayload(item, event, mCollection.contentMimeTypes())) { qCWarning(KALARM_LOG) << "AkonadiResource::updateEvent: Invalid mime type for collection"; return false; } queueItemModifyJob(item); return true; } /****************************************************************************** * Delete an event from the resource, and from Akonadi. */ bool AkonadiResource::deleteEvent(const KAEvent& event) { qCDebug(KALARM_LOG) << "AkonadiResource::deleteEvent:" << event.id(); if (isBeingDeleted()) { qCDebug(KALARM_LOG) << "AkonadiResource::deleteEvent: Collection being deleted"; return true; // the event's collection is being deleted } const Item item = AkonadiDataModel::instance()->itemForEvent(event.id()); if (!item.isValid()) return false; ItemDeleteJob* job = new ItemDeleteJob(item); connect(job, &ItemDeleteJob::result, this, &AkonadiResource::itemJobDone); mPendingItemJobs[job] = item.id(); job->start(); return true; } /****************************************************************************** * Save a command error change to Akonadi. */ void AkonadiResource::handleCommandErrorChange(const KAEvent& event) { Item item = AkonadiDataModel::instance()->itemForEvent(event.id()); if (item.isValid()) { const KAEvent::CmdErrType err = event.commandError(); switch (err) { case KAEvent::CMD_NO_ERROR: if (!item.hasAttribute()) return; // no change Q_FALLTHROUGH(); case KAEvent::CMD_ERROR: case KAEvent::CMD_ERROR_PRE: case KAEvent::CMD_ERROR_POST: case KAEvent::CMD_ERROR_PRE_POST: { EventAttribute* attr = item.attribute(Item::AddIfMissing); if (attr->commandError() == err) return; // no change attr->setCommandError(err); queueItemModifyJob(item); return; } default: break; } } } /****************************************************************************** * Return a reference to the Collection held by a resource. */ Collection& AkonadiResource::collection(Resource& res) { static Collection nullCollection; AkonadiResource* akres = resource(res); return akres ? akres->mCollection : nullCollection; } const Collection& AkonadiResource::collection(const Resource& res) { static const Collection nullCollection; const AkonadiResource* akres = resource(res); return akres ? akres->mCollection : nullCollection; } /****************************************************************************** * Return the event for an Akonadi Item. */ KAEvent AkonadiResource::event(Resource& resource, const Akonadi::Item& item) { if (!item.isValid() || !item.hasPayload()) return KAEvent(); KAEvent ev = item.payload(); if (ev.isValid()) { if (item.hasAttribute()) ev.setCommandError(item.attribute()->commandError()); // Set collection ID using a const method, to avoid unnecessary copying of KAEvent ev.setCollectionId_const(resource.id()); } return ev; } -/****************************************************************************** -* Get the collection to use for storing an alarm. -* Optionally, the standard collection for the alarm type is returned. If more -* than one collection is a candidate, the user is prompted. -*/ -Resource AkonadiResource::destination(CalEvent::Type type, QWidget* promptParent, bool noPrompt, bool* cancelled) -{ - return Resources::destination(type, promptParent, noPrompt, cancelled); -} - /****************************************************************************** * Check for, and remove, any Akonadi resources which duplicate use of calendar * files/directories. */ void AkonadiResource::removeDuplicateResources() { if (!mDuplicateResourceObject) mDuplicateResourceObject = new DuplicateResourceObject(Resources::instance()); mDuplicateResourceObject->reset(); const AgentInstance::List agents = AgentManager::self()->instances(); for (const AgentInstance& agent : agents) { if (agent.type().mimeTypes().indexOf(MatchMimeType) >= 0) { CollectionFetchJob* job = new CollectionFetchJob(Collection::root(), CollectionFetchJob::Recursive); job->fetchScope().setResource(agent.identifier()); connect(job, &CollectionFetchJob::result, mDuplicateResourceObject, &DuplicateResourceObject::collectionFetchResult); } } } /****************************************************************************** * Called when a removeDuplicateResources() CollectionFetchJob has completed. */ void DuplicateResourceObject::collectionFetchResult(KJob* j) { CollectionFetchJob* job = qobject_cast(j); if (j->error()) qCCritical(KALARM_LOG) << "AkonadiResource::collectionFetchResult: CollectionFetchJob" << job->fetchScope().resource()<< "error: " << j->errorString(); else { AgentManager* agentManager = AgentManager::self(); const Collection::List collections = job->collections(); for (const Collection& c : collections) { if (c.contentMimeTypes().indexOf(MatchMimeType) >= 0) { ResourceCol thisRes(job->fetchScope().resource(), c.id()); auto it = mAgentPaths.constFind(c.remoteId()); if (it != mAgentPaths.constEnd()) { // Remove the resource containing the higher numbered Collection // ID, which is likely to be the more recently created. const ResourceCol prevRes = it.value(); if (thisRes.collectionId > prevRes.collectionId) { qCWarning(KALARM_LOG) << "AkonadiResource::collectionFetchResult: Removing duplicate resource" << thisRes.resourceId; agentManager->removeInstance(agentManager->instance(thisRes.resourceId)); continue; } qCWarning(KALARM_LOG) << "AkonadiResource::collectionFetchResult: Removing duplicate resource" << prevRes.resourceId; agentManager->removeInstance(agentManager->instance(prevRes.resourceId)); } mAgentPaths[c.remoteId()] = thisRes; } } } } /****************************************************************************** * Called when a collection has been populated. * Stores all its events, even if their alarm types are currently disabled. * Emits a signal if all collections have been populated. */ void AkonadiResource::notifyCollectionLoaded(ResourceId id, const QList& events) { if (id >= 0) { Resource res = Resources::resource(id); AkonadiResource* akres = resource(res); if (akres) { const CalEvent::Types types = akres->alarmTypes(); QHash eventHash; for (const KAEvent& event : events) if (event.category() & types) eventHash[event.id()] = event; akres->setLoadedEvents(eventHash); } } } /****************************************************************************** * Called when the collection's properties or content have changed. * Updates this resource's copy of the collection, and emits a signal if * properties of interest have changed. */ void AkonadiResource::notifyCollectionChanged(Resource& res, const Collection& collection, bool checkCompatibility) { if (collection.id() != res.id()) return; AkonadiResource* akres = resource(res); if (!akres) return; Changes change = NoChange; // Check for a read/write permission change const Collection::Rights oldRights = akres->mCollection.rights() & WritableRights; const Collection::Rights newRights = collection.rights() & WritableRights; if (newRights != oldRights) { qCDebug(KALARM_LOG) << "AkonadiResource::setCollectionChanged:" << collection.id() << ": rights ->" << newRights; change |= ReadOnly; } // Check for a change in content mime types // (e.g. when a collection is first created at startup). if (collection.contentMimeTypes() != akres->mCollection.contentMimeTypes()) { qCDebug(KALARM_LOG) << "AkonadiResource::setCollectionChanged:" << collection.id() << ": alarm types ->" << collection.contentMimeTypes(); change |= AlarmTypes; } // Check for the collection being enabled/disabled. // Enabled/disabled can only be set by KAlarm (not the resource), so if the // attribute doesn't exist, it is ignored. const CalEvent::Types oldEnabled = akres->mLastEnabled; const CalEvent::Types newEnabled = collection.hasAttribute() ? collection.attribute()->enabled() : CalEvent::EMPTY; if (!akres->mCollectionAttrChecked || newEnabled != oldEnabled) { qCDebug(KALARM_LOG) << "AkonadiResource::setCollectionChanged:" << collection.id() << ": enabled ->" << newEnabled; akres->mCollectionAttrChecked = true; change |= Enabled; } akres->mLastEnabled = newEnabled; akres->mCollection = collection; if (change != NoChange) Resources::notifySettingsChanged(akres, change, oldEnabled); if (!resource(res)) return; // this resource has been deleted // Check for the backend calendar format changing. bool hadCompat = akres->mHaveCompatibilityAttribute; akres->mHaveCompatibilityAttribute = collection.hasAttribute(); if (akres->mHaveCompatibilityAttribute) { // The attribute must exist in order to know the calendar format. if (checkCompatibility || !hadCompat || *collection.attribute() != *akres->mCollection.attribute()) { //TODO: check if a temporary AkonadiResource object is actually needed, as in the comment // Update to current KAlarm format if necessary, and if the user agrees. // Create a new temporary 'Resource' object, because the one passed // to this method can get overwritten with an old version of its // CompatibilityAttribute before AkonadiResourceMigration finishes, // due to AkonadiDataModel still containing an out of date value. qCDebug(KALARM_LOG) << "AkonadiResource::setCollectionChanged:" << collection.id() << ": compatibility ->" << collection.attribute()->compatibility(); // Note that the AkonadiResource will be deleted once no more // QSharedPointers reference it. AkonadiResourceMigrator::updateToCurrentFormat(res, false, akres); } } } /****************************************************************************** * Called to notify that an event has been added or updated in Akonadi. */ void AkonadiResource::notifyEventsChanged(Resource& res, const QList& events) { AkonadiResource* akres = resource(res); if (akres) akres->setUpdatedEvents(events); } /****************************************************************************** * Called when an Item has been changed or created in Akonadi. */ void AkonadiResource::notifyItemChanged(Resource& res, const Akonadi::Item& item, bool created) { AkonadiResource* akres = resource(res); if (akres) { int i = akres->mItemsBeingCreated.removeAll(item.id()); // the new item has now been initialised if (!created || i) akres->checkQueuedItemModifyJob(item); // execute the next job queued for the item } } /****************************************************************************** * Called to notify that an event is about to be deleted from Akonadi. */ void AkonadiResource::notifyEventsToBeDeleted(Resource& res, const QList& events) { AkonadiResource* akres = resource(res); if (akres) akres->setDeletedEvents(events); } /****************************************************************************** * Queue an ItemModifyJob for execution. Ensure that only one job is * simultaneously active for any one Item. * * This is necessary because we can't call two ItemModifyJobs for the same Item * at the same time; otherwise Akonadi will detect a conflict and require manual * intervention to resolve it. */ void AkonadiResource::queueItemModifyJob(const Item& item) { qCDebug(KALARM_LOG) << "AkonadiResource::queueItemModifyJob:" << item.id(); QHash::Iterator it = mItemModifyJobQueue.find(item.id()); if (it != mItemModifyJobQueue.end()) { // A job is already queued for this item. Replace the queued item value with the new one. qCDebug(KALARM_LOG) << "AkonadiResource::queueItemModifyJob: Replacing previously queued job"; it.value() = item; } else { // There is no job already queued for this item if (mItemsBeingCreated.contains(item.id())) { qCDebug(KALARM_LOG) << "AkonadiResource::queueItemModifyJob: Waiting for item initialisation"; mItemModifyJobQueue[item.id()] = item; // wait for item initialisation to complete } else { Item newItem = item; Item current = item; if (AkonadiDataModel::instance()->refresh(current)) // fetch the up-to-date item newItem.setRevision(current.revision()); mItemModifyJobQueue[item.id()] = Item(); // mark the queued item as now executing ItemModifyJob* job = new ItemModifyJob(newItem); job->disableRevisionCheck(); connect(job, &ItemModifyJob::result, this, &AkonadiResource::itemJobDone); mPendingItemJobs[job] = item.id(); qCDebug(KALARM_LOG) << "AkonadiResource::queueItemModifyJob: Executing Modify job for item" << item.id() << ", revision=" << newItem.revision(); } } } /****************************************************************************** * Called when an item job has completed. * Checks for any error. * Note that for an ItemModifyJob, the item revision number may not be updated * to the post-modification value. The next queued ItemModifyJob is therefore * not kicked off from here, but instead from the slot attached to the * itemChanged() signal, which has the revision updated. */ void AkonadiResource::itemJobDone(KJob* j) { const QHash::iterator it = mPendingItemJobs.find(j); Item::Id itemId = -1; if (it != mPendingItemJobs.end()) { itemId = it.value(); mPendingItemJobs.erase(it); } const QByteArray jobClass = j->metaObject()->className(); qCDebug(KALARM_LOG) << "AkonadiResource::itemJobDone:" << jobClass; if (j->error()) { QString errMsg; if (jobClass == "Akonadi::ItemCreateJob") errMsg = i18nc("@info", "Failed to create alarm."); else if (jobClass == "Akonadi::ItemModifyJob") errMsg = i18nc("@info", "Failed to update alarm."); else if (jobClass == "Akonadi::ItemDeleteJob") errMsg = i18nc("@info", "Failed to delete alarm."); else Q_ASSERT(0); qCCritical(KALARM_LOG) << "AkonadiResource::itemJobDone:" << errMsg << itemId << ":" << j->errorString(); if (itemId >= 0 && jobClass == "Akonadi::ItemModifyJob") { // Execute the next queued job for this item const Item current = AkonadiDataModel::instance()->itemById(itemId); // fetch the up-to-date item checkQueuedItemModifyJob(current); } Resources::notifyResourceMessage(this, MessageType::Error, errMsg, j->errorString()); } else { if (jobClass == "Akonadi::ItemCreateJob") { // Prevent modification of the item until it is fully initialised. // Either slotMonitoredItemChanged() or slotRowsInserted(), or both, // will be called when the item is done. itemId = static_cast(j)->item().id(); qCDebug(KALARM_LOG) << "AkonadiResource::itemJobDone(ItemCreateJob): item id=" << itemId; mItemsBeingCreated << itemId; } } /* if (itemId >= 0 && jobClass == "Akonadi::ItemModifyJob") { const QHash::iterator it = mItemModifyJobQueue.find(itemId); if (it != mItemModifyJobQueue.end()) { if (!it.value().isValid()) mItemModifyJobQueue.erase(it); // there are no more jobs queued for the item } }*/ } /****************************************************************************** * Check whether there are any ItemModifyJobs waiting for a specified item, and * if so execute the first one provided its creation has completed. This * prevents clashes in Akonadi conflicts between simultaneous ItemModifyJobs for * the same item. * * Note that when an item is newly created (e.g. via addEvent()), the KAlarm * resource itemAdded() function creates an ItemModifyJob to give it a remote * ID. Until that job is complete, any other ItemModifyJob for the item will * cause a conflict. */ void AkonadiResource::checkQueuedItemModifyJob(const Item& item) { if (mItemsBeingCreated.contains(item.id())) return; // the item hasn't been fully initialised yet const QHash::iterator it = mItemModifyJobQueue.find(item.id()); if (it == mItemModifyJobQueue.end()) return; // there are no jobs queued for the item Item qitem = it.value(); if (!qitem.isValid()) { // There is no further job queued for the item, so remove the item from the list mItemModifyJobQueue.erase(it); } else { // Queue the next job for the Item, after updating the Item's // revision number to match that set by the job just completed. qitem.setRevision(item.revision()); mItemModifyJobQueue[item.id()] = Item(); // mark the queued item as now executing ItemModifyJob* job = new ItemModifyJob(qitem); job->disableRevisionCheck(); connect(job, &ItemModifyJob::result, this, &AkonadiResource::itemJobDone); mPendingItemJobs[job] = qitem.id(); qCDebug(KALARM_LOG) << "Executing queued Modify job for item" << qitem.id() << ", revision=" << qitem.revision(); } } /****************************************************************************** * Update the stored CollectionAttribute value from the Akonadi database. */ void AkonadiResource::fetchCollectionAttribute(bool refresh) const { if (refresh) AkonadiDataModel::instance()->refresh(mCollection); // update with latest data if (!mCollection.hasAttribute()) { mCollectionAttribute = CollectionAttribute(); mHaveCollectionAttribute = false; } else { mCollectionAttribute = *mCollection.attribute(); mHaveCollectionAttribute = true; } } /****************************************************************************** * Update the CollectionAttribute value in the Akonadi database. */ void AkonadiResource::modifyCollectionAttribute() { // Note that we can't supply 'mCollection' to CollectionModifyJob since that // also contains the CompatibilityAttribute value, which is read-only for // applications. So create a new Collection instance and only set a value // for CollectionAttribute. Collection c(mCollection.id()); CollectionAttribute* att = c.attribute(Collection::AddIfMissing); *att = mCollectionAttribute; CollectionModifyJob* job = new CollectionModifyJob(c, this); connect(job, &CollectionModifyJob::result, this, &AkonadiResource::modifyCollectionAttrJobDone); } /****************************************************************************** * Called when a CollectionAttribute modification job has completed. * Checks for any error. */ void AkonadiResource::modifyCollectionAttrJobDone(KJob* j) { Collection collection = static_cast(j)->collection(); const Collection::Id id = collection.id(); const bool newEnabled = mNewEnabled; mNewEnabled = false; if (j->error()) { // If the collection is being/has been deleted, ignore the error. if (!isBeingDeleted() && AkonadiDataModel::instance()->resource(id).isValid() && id == mCollection.id()) { qCCritical(KALARM_LOG) << "AkonadiResource::modifyCollectionAttrJobDone:" << collection.id() << "Failed to update calendar" << displayName() << ":" << j->errorString(); Resources::notifyResourceMessage(this, MessageType::Error, i18nc("@info", "Failed to update calendar \"%1\".", displayName()), j->errorString()); } } else { AkonadiDataModel::instance()->refresh(mCollection); // pick up the modified attribute if (newEnabled) { const CalEvent::Types oldEnabled = mLastEnabled; mLastEnabled = collection.hasAttribute() ? collection.attribute()->enabled() : CalEvent::EMPTY; Resources::notifySettingsChanged(this, Enabled, oldEnabled); } } } #include "akonadiresource.moc" // vim: et sw=4: diff --git a/src/resources/akonadiresource.h b/src/resources/akonadiresource.h index 1c593e34..0f43218a 100644 --- a/src/resources/akonadiresource.h +++ b/src/resources/akonadiresource.h @@ -1,347 +1,336 @@ /* * akonadiresource.h - class for an Akonadi alarm calendar resource * Program: kalarm * Copyright © 2019 David Jarvie * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef AKONADIRESOURCE_H #define AKONADIRESOURCE_H #include "resource.h" #include #include #include #include class KJob; class DuplicateResourceObject; using namespace KAlarmCal; /** Class for an alarm calendar resource accessed through Akonadi. * Public access to this class is normally via the Resource class. */ class AkonadiResource : public ResourceType { Q_OBJECT public: /** Construct a new AkonadiResource. * The supplied collection must be up to date. */ static Resource create(const Akonadi::Collection&); protected: /** Constructor. * The supplied collection must be up to date. */ explicit AkonadiResource(const Akonadi::Collection&); public: ~AkonadiResource() override; static Resource nullResource(); /** Return whether the resource has a valid configuration. */ bool isValid() const override; /** Return the resource's collection. */ Akonadi::Collection collection() const; /** Return the type of storage used by the resource. */ StorageType storageType() const override; /** Return the type of the resource (file, remote file, etc.) * for display purposes. * @param description true for description (e.g. "Remote file"), * false for brief label (e.g. "URL"). */ QString storageTypeString(bool description) const override; /** Return the location(s) of the resource (URL, file path, etc.) */ QUrl location() const override; /** Return the location of the resource (URL, file path, etc.) * for display purposes. */ QString displayLocation() const override; /** Return the resource's display name. */ QString displayName() const override; /** Return the resource's configuration identifier, which for this class is * the Akonadi resource identifier. This is not the name normally * displayed to the user. */ QString configName() const override; /** Return which types of alarms the resource can contain. */ CalEvent::Types alarmTypes() const override; /** Return which alarm types (active, archived or template) the * resource is enabled for. */ CalEvent::Types enabledTypes() const override; /** Set the enabled/disabled state of the resource and its alarms, * for a specified alarm type (active, archived or template). The * enabled/disabled state for other alarm types is not affected. * The alarms of that type in a disabled resource are ignored, and * not displayed in the alarm list. The standard status for that type * for a disabled resource is automatically cleared. * @param type alarm type * @param enabled true to set enabled, false to set disabled. */ void setEnabled(CalEvent::Type type, bool enabled) override; /** Set which alarm types (active, archived or template) the resource * is enabled for. * @param types alarm types */ void setEnabled(CalEvent::Types types) override; /** Return whether the resource is configured as read-only or is * read-only on disc. */ bool readOnly() const override; /** Return whether the resource is both enabled and fully writable for a * given alarm type, i.e. not read-only, and compatible with the current * KAlarm calendar format. * * @param type alarm type to check for, or EMPTY to check for any type. * @return 1 = fully enabled and writable, * 0 = enabled and writable except that backend calendar is in an * old KAlarm format, * -1 = read-only, disabled or incompatible format. */ int writableStatus(CalEvent::Type type = CalEvent::EMPTY) const override; using ResourceType::isWritable; /** Return whether the user has chosen not to update the resource's * calendar storage format. */ bool keepFormat() const override; /** Set or clear whether the user has chosen not to update the resource's * calendar storage format. */ void setKeepFormat(bool keep) override; /** Return the background colour used to display alarms belonging to * this resource. * @return display colour, or invalid if none specified */ QColor backgroundColour() const override; /** Set the background colour used to display alarms belonging to this * resource. * @param colour display colour, or invalid to use the default colour */ void setBackgroundColour(const QColor& colour) override; /** Return whether the resource is set in its Akonadi attribute to be the * standard resource for a specified alarm type (active, archived or * template). There is no check for whether the resource is enabled, is * writable, or is the only resource set as standard. * * @note To determine whether the resource is actually the standard * resource, call Resources::isStandard(). * * @param type alarm type */ bool configIsStandard(CalEvent::Type type) const override; /** Return which alarm types (active, archived or template) the resource * is standard for, as set in its Akonadi attribute. This is restricted to * the alarm types which the resource can contain (@see alarmTypes()). * There is no check for whether the resource is enabled, is writable, or * is the only resource set as standard. * * @note To determine what alarm types the resource is actually the standard * resource for, call Resources::standardTypes(). * * @return alarm types. */ CalEvent::Types configStandardTypes() const override; /** Set or clear the resource as the standard resource for a specified alarm * type (active, archived or template), storing the setting in its Akonadi * attribute. There is no check for whether the resource is eligible to be * set as standard, or to ensure that it is the only standard resource for * the type. * * @note To set the resource's standard status and ensure that it is * eligible and the only standard resource for the type, call * Resources::setStandard(). * * @param type alarm type * @param standard true to set as standard, false to clear standard status. */ void configSetStandard(CalEvent::Type type, bool standard) override; /** Set which alarm types (active, archived or template) the resource is * the standard resource for, storing the setting in its Akonadi attribute. * There is no check for whether the resource is eligible to be set as * standard, or to ensure that it is the only standard resource for the * types. * * @note To set the resource's standard status and ensure that it is * eligible and the only standard resource for the types, call * Resources::setStandard(). * * @param types alarm types.to set as standard */ void configSetStandard(CalEvent::Types types) override; /** Return whether the resource is in a different format from the * current KAlarm format, in which case it cannot be written to. * Note that isWritable() takes account of incompatible format * as well as read-only and enabled statuses. * @param versionString Receives calendar's KAlarm version as a string. */ KACalendar::Compat compatibilityVersion(QString& versionString) const override; /** Update the resource to the current KAlarm storage format. */ bool updateStorageFormat() override; /** Edit the resource's configuration. */ void editResource(QWidget* dialogParent) override; /** Remove the resource. The calendar file is not removed. * @note The instance will be invalid once it has been removed. * @return true if the resource has been removed or a removal job has been scheduled. */ bool removeResource() override; /** Load the resource from storage, and fetch all events. * Not applicable to AkonadiResource, since the Akonadi resource handles * loading automatically. * @return true. */ bool load(bool readThroughCache = true) override; #if 0 /** Reload the resource. Any cached data is first discarded. */ bool reload() override; #endif /** Return whether the resource has fully loaded. */ bool isPopulated() const override; /** Save the resource. * Not applicable to AkonadiResource, since AkonadiDataModel handles saving * automatically. * @return true. */ bool save(bool writeThroughCache = true) override; /** Add an event to the resource, and add it to Akonadi. */ bool addEvent(const KAEvent&) override; /** Update an event in the resource, and update it in Akonadi. * Its UID must be unchanged. */ bool updateEvent(const KAEvent&) override; /** Delete an event from the resource, and from Akonadi. */ bool deleteEvent(const KAEvent&) override; /** Called to notify the resource that an event's command error has changed. */ void handleCommandErrorChange(const KAEvent&) override; /*----------------------------------------------------------------------------- * The methods below are all particular to the AkonadiResource class, and in * order to be accessible to clients are defined as 'static'. *----------------------------------------------------------------------------*/ /****************************************************************************** * Return a reference to the Collection held by an Akonadi resource. * @reply Reference to the Collection, which belongs to AkonadiDataModel and * whose ID must not be changed. */ static Akonadi::Collection& collection(Resource&); static const Akonadi::Collection& collection(const Resource&); // Akonadi::Collection& collection() { return mCollection; } /** Return the event for an Akonadi Item belonging to a resource. */ static KAEvent event(Resource&, const Akonadi::Item&); using QObject::event; // prevent warning about hidden virtual method - /** Find the collection to be used to store an event of a given type. - * This will be the standard collection for the type, but if this is not valid, - * the user will be prompted to select a collection. - * @param type The event type - * @param promptParent The parent widget for the prompt - * @param noPrompt Don't prompt the user even if the standard collection is not valid - * @param cancelled If non-null: set to true if the user cancelled the - * prompt dialogue; set to false if any other error - */ - static Resource destination(CalEvent::Type type, QWidget* promptParent = nullptr, bool noPrompt = false, bool* cancelled = nullptr); - /** Check for, and remove, Akonadi resources which duplicate use of * calendar files/directories. */ static void removeDuplicateResources(); /** Called to notify that a resource's Collection has been populated. * @param events The full list of events in the Collection. */ static void notifyCollectionLoaded(ResourceId, const QList& events); /** Called to notify that a resource's Collection has changed. */ static void notifyCollectionChanged(Resource&, const Akonadi::Collection&, bool checkCompatibility); /** Called to notify that events have been added or updated in Akonadi. */ static void notifyEventsChanged(Resource&, const QList&); /** Called to notify that events are to be deleted from Akonadi. */ static void notifyEventsToBeDeleted(Resource&, const QList&); /** Called to notify that an Akonadi Item belonging to a resource has * changed or been created. * @note notifyEventChanged() should also be called to signal the * new or changed event to interested parties. * @param created true if the item has been created; false if changed. */ static void notifyItemChanged(Resource&, const Akonadi::Item&, bool created); private Q_SLOTS: void slotCollectionRemoved(const Akonadi::Collection&); void itemJobDone(KJob*); void modifyCollectionAttrJobDone(KJob*); private: void queueItemModifyJob(const Akonadi::Item&); void checkQueuedItemModifyJob(const Akonadi::Item&); void fetchCollectionAttribute(bool refresh) const; void modifyCollectionAttribute(); static DuplicateResourceObject* mDuplicateResourceObject; // QObject used by removeDuplicateResources() mutable Akonadi::Collection mCollection; // the Akonadi Collection represented by this resource mutable CollectionAttribute mCollectionAttribute; // current set value of CollectionAttribute QHash mPendingItemJobs; // pending item creation/deletion jobs, with event ID QHash mItemModifyJobQueue; // pending item modification jobs, invalid item = queue empty but job active QList mItemsBeingCreated; // new items not fully initialised yet bool mValid; // whether the collection is valid and belongs to an Akonadi resource mutable bool mHaveCollectionAttribute{false}; // whether the collection has a CollectionAttribute bool mHaveCompatibilityAttribute{false}; // whether the collection has a CompatibilityAttribute CalEvent::Types mLastEnabled{CalEvent::EMPTY}; // last known enabled status mutable bool mNewEnabled{false}; bool mCollectionAttrChecked{false}; // CollectionAttribute has been processed first time }; #endif // AKONADIRESOURCE_H // vim: et sw=4: diff --git a/src/resources/datamodel.cpp b/src/resources/datamodel.cpp new file mode 100644 index 00000000..d60640a9 --- /dev/null +++ b/src/resources/datamodel.cpp @@ -0,0 +1,88 @@ +/* + * datamodel.cpp - calendar data model dependent functions + * Program: kalarm + * Copyright © 2019 David Jarvie + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "datamodel.h" + +#include "akonadidatamodel.h" +#include "akonadiresource.h" +#include "eventmodel.h" +#include "resourcemodel.h" + +namespace DataModel +{ + +void initialise() +{ + AkonadiDataModel::instance(); +} + +void reload() +{ + AkonadiDataModel::instance()->reload(); +} + +bool reload(Resource& resource) +{ + return AkonadiDataModel::instance()->reload(resource); +} + +bool isMigrationComplete() +{ + return AkonadiDataModel::instance()->isMigrationComplete(); +} + +void removeDuplicateResources() +{ + AkonadiResource::removeDuplicateResources(); +} + +ResourceListModel* createResourceListModel(QObject* parent) +{ + return ResourceListModel::create(parent); +} + +ResourceFilterCheckListModel* createResourceFilterCheckListModel(QObject* parent) +{ + return ResourceFilterCheckListModel::create(parent); +} + +AlarmListModel* createAlarmListModel(QObject* parent) +{ + return AlarmListModel::create(parent); +} + +AlarmListModel* allAlarmListModel() +{ + return AlarmListModel::all(); +} + +TemplateListModel* createTemplateListModel(QObject* parent) +{ + return TemplateListModel::create(parent); +} + +TemplateListModel* allTemplateListModel() +{ + return TemplateListModel::all(); +} + +} // namespace DataModel + +// vim: et sw=4: diff --git a/src/resources/datamodel.h b/src/resources/datamodel.h new file mode 100644 index 00000000..27b983b8 --- /dev/null +++ b/src/resources/datamodel.h @@ -0,0 +1,65 @@ +/* + * datamodel.h - calendar data model dependent functions + * Program: kalarm + * Copyright © 2019 David Jarvie + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef DATAMODEL_H +#define DATAMODEL_H + +class Resource; +class ResourceListModel; +class ResourceFilterCheckListModel; +class AlarmListModel; +class TemplateListModel; +class QObject; + +/** Class to create objects dependent on data model. */ +namespace DataModel +{ + +void initialise(); + +/** Reload all resources' data from storage. + * @note In the case of Akonadi, this does not reload from the backend storage. + */ +void reload(); + +/** Reload a resource's data from storage. + * @note In the case of Akonadi, this does not reload from the backend storage. + */ +bool reload(Resource&); + +bool isMigrationComplete(); + +/** Check for, and remove, any duplicate Akonadi resources, i.e. those which + * use the same calendar file/directory. + */ +void removeDuplicateResources(); + +ResourceListModel* createResourceListModel(QObject* parent); +ResourceFilterCheckListModel* createResourceFilterCheckListModel(QObject* parent); +AlarmListModel* createAlarmListModel(QObject* parent); +AlarmListModel* allAlarmListModel(); +TemplateListModel* createTemplateListModel(QObject* parent); +TemplateListModel* allTemplateListModel(); + +} // namespace DataModel + +#endif // DATAMODEL_H + +// vim: et sw=4: diff --git a/src/resources/resourcedatamodelbase.cpp b/src/resources/resourcedatamodelbase.cpp index 1c537c10..1fb9901f 100644 --- a/src/resources/resourcedatamodelbase.cpp +++ b/src/resources/resourcedatamodelbase.cpp @@ -1,626 +1,627 @@ /* * resourcedatamodelbase.cpp - base for models containing calendars and events * Program: kalarm * Copyright © 2007-2019 David Jarvie * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "resourcedatamodelbase.h" -#include "resource.h" +#include "resources.h" #include "alarmtime.h" #include "mainwindow.h" #include "preferences.h" #include "lib/messagebox.h" #include "kalarm_debug.h" #include #include #include #include #include #include #include #include /*============================================================================= = Class: ResourceDataModelBase =============================================================================*/ QPixmap* ResourceDataModelBase::mTextIcon = nullptr; QPixmap* ResourceDataModelBase::mFileIcon = nullptr; QPixmap* ResourceDataModelBase::mCommandIcon = nullptr; QPixmap* ResourceDataModelBase::mEmailIcon = nullptr; QPixmap* ResourceDataModelBase::mAudioIcon = nullptr; QSize ResourceDataModelBase::mIconSize; /****************************************************************************** * Constructor. */ ResourceDataModelBase::ResourceDataModelBase() { if (!mTextIcon) { mTextIcon = new QPixmap(QIcon::fromTheme(QStringLiteral("dialog-information")).pixmap(16, 16)); mFileIcon = new QPixmap(QIcon::fromTheme(QStringLiteral("document-open")).pixmap(16, 16)); mCommandIcon = new QPixmap(QIcon::fromTheme(QStringLiteral("system-run")).pixmap(16, 16)); mEmailIcon = new QPixmap(QIcon::fromTheme(QStringLiteral("mail-unread")).pixmap(16, 16)); mAudioIcon = new QPixmap(QIcon::fromTheme(QStringLiteral("audio-x-generic")).pixmap(16, 16)); mIconSize = mTextIcon->size().expandedTo(mFileIcon->size()).expandedTo(mCommandIcon->size()).expandedTo(mEmailIcon->size()).expandedTo(mAudioIcon->size()); } } ResourceDataModelBase::~ResourceDataModelBase() { } /****************************************************************************** * Create a bulleted list of alarm types for insertion into .... */ QString ResourceDataModelBase::typeListForDisplay(CalEvent::Types alarmTypes) { QString list; if (alarmTypes & CalEvent::ACTIVE) list += QLatin1String("") + i18nc("@info", "Active Alarms") + QLatin1String(""); if (alarmTypes & CalEvent::ARCHIVED) list += QLatin1String("") + i18nc("@info", "Archived Alarms") + QLatin1String(""); if (alarmTypes & CalEvent::TEMPLATE) list += QLatin1String("") + i18nc("@info", "Alarm Templates") + QLatin1String(""); if (!list.isEmpty()) list = QLatin1String("") + list + QLatin1String(""); return list; } /****************************************************************************** * Return the read-only status tooltip for a collection, determined by the * read-write permissions and the KAlarm calendar format compatibility. * A null string is returned if the collection is read-write and compatible. */ QString ResourceDataModelBase::readOnlyTooltip(const Resource& resource) { switch (resource.compatibility()) { case KACalendar::Current: return resource.readOnly() ? i18nc("@info", "Read-only") : QString(); case KACalendar::Converted: case KACalendar::Convertible: return i18nc("@info", "Read-only (old format)"); case KACalendar::Incompatible: default: return i18nc("@info", "Read-only (other format)"); } } /****************************************************************************** * Return data for a column heading. */ QVariant ResourceDataModelBase::headerData(int section, Qt::Orientation orientation, int role, bool eventHeaders, bool& handled) { if (orientation == Qt::Horizontal) { handled = true; if (eventHeaders) { // Event column headers if (section < 0 || section >= ColumnCount) return QVariant(); if (role == Qt::DisplayRole || role == ColumnTitleRole) { switch (section) { case TimeColumn: return i18nc("@title:column", "Time"); case TimeToColumn: return i18nc("@title:column", "Time To"); case RepeatColumn: return i18nc("@title:column", "Repeat"); case ColourColumn: return (role == Qt::DisplayRole) ? QString() : i18nc("@title:column", "Color"); case TypeColumn: return (role == Qt::DisplayRole) ? QString() : i18nc("@title:column", "Type"); case TextColumn: return i18nc("@title:column", "Message, File or Command"); case TemplateNameColumn: return i18nc("@title:column Template name", "Name"); } } else if (role == Qt::WhatsThisRole) return whatsThisText(section); } else { // Calendar column headers if (section != 0) return QVariant(); if (role == Qt::DisplayRole) return i18nc("@title:column", "Calendars"); } } handled = false; return QVariant(); } bool ResourceDataModelBase::roleHandled(int role) const { switch (role) { case Qt::WhatsThisRole: case Qt::ForegroundRole: case Qt::BackgroundRole: case Qt::DisplayRole: case Qt::TextAlignmentRole: case Qt::DecorationRole: case Qt::SizeHintRole: case Qt::AccessibleTextRole: case Qt::ToolTipRole: case BaseColourRole: case TimeDisplayRole: case SortRole: case StatusRole: case ValueRole: case EventIdRole: case ParentResourceIdRole: case EnabledRole: case AlarmActionsRole: case AlarmSubActionRole: case CommandErrorRole: return true; default: return false; } } /****************************************************************************** * Return the data for a given role, for a specified resource. */ QVariant ResourceDataModelBase::resourceData(int& role, const Resource& resource, bool& handled) const { if (roleHandled(role)) // Ensure that resourceDataHandles() is coded correctly { handled = true; switch (role) { case Qt::DisplayRole: return resource.displayName(); case BaseColourRole: role = Qt::BackgroundRole; // use base model background colour break; case Qt::BackgroundRole: { const QColor colour = resource.backgroundColour(); if (colour.isValid()) return colour; break; } case Qt::ForegroundRole: return resource.foregroundColour(); case Qt::ToolTipRole: return tooltip(resource, CalEvent::ACTIVE | CalEvent::ARCHIVED | CalEvent::TEMPLATE); default: break; } } handled = false; return QVariant(); } /****************************************************************************** * Return the data for a given role, for a specified event. */ QVariant ResourceDataModelBase::eventData(int role, int column, const KAEvent& event, const Resource& resource, bool& handled) const { if (roleHandled(role)) // Ensure that eventDataHandles() is coded correctly { handled = true; bool calendarColour = false; if (role == Qt::WhatsThisRole) return whatsThisText(column); if (!event.isValid()) return QVariant(); switch (role) { case EventIdRole: return event.id(); case StatusRole: return event.category(); case AlarmActionsRole: return event.actionTypes(); case AlarmSubActionRole: return event.actionSubType(); case CommandErrorRole: return event.commandError(); default: break; } switch (column) { case TimeColumn: switch (role) { case Qt::BackgroundRole: calendarColour = true; break; case Qt::DisplayRole: if (event.expired()) return AlarmTime::alarmTimeText(event.startDateTime(), '0'); return AlarmTime::alarmTimeText(event.nextTrigger(KAEvent::DISPLAY_TRIGGER), '0'); case TimeDisplayRole: if (event.expired()) return AlarmTime::alarmTimeText(event.startDateTime(), '~'); return AlarmTime::alarmTimeText(event.nextTrigger(KAEvent::DISPLAY_TRIGGER), '~'); case Qt::TextAlignmentRole: return Qt::AlignRight; case SortRole: { DateTime due; if (event.expired()) due = event.startDateTime(); else due = event.nextTrigger(KAEvent::DISPLAY_TRIGGER); return due.isValid() ? due.effectiveKDateTime().toUtc().qDateTime() : QDateTime(QDate(9999,12,31), QTime(0,0,0)); } default: break; } break; case TimeToColumn: switch (role) { case Qt::BackgroundRole: calendarColour = true; break; case Qt::DisplayRole: if (event.expired()) return QString(); return AlarmTime::timeToAlarmText(event.nextTrigger(KAEvent::DISPLAY_TRIGGER)); case Qt::TextAlignmentRole: return Qt::AlignRight; case SortRole: { if (event.expired()) return -1; const DateTime due = event.nextTrigger(KAEvent::DISPLAY_TRIGGER); const KADateTime now = KADateTime::currentUtcDateTime(); if (due.isDateOnly()) return now.date().daysTo(due.date()) * 1440; return (now.secsTo(due.effectiveKDateTime()) + 59) / 60; } } break; case RepeatColumn: switch (role) { case Qt::BackgroundRole: calendarColour = true; break; case Qt::DisplayRole: return repeatText(event); case Qt::TextAlignmentRole: return Qt::AlignHCenter; case SortRole: return repeatOrder(event); } break; case ColourColumn: switch (role) { case Qt::BackgroundRole: { const KAEvent::Actions type = event.actionTypes(); if (type & KAEvent::ACT_DISPLAY) return event.bgColour(); if (type == KAEvent::ACT_COMMAND) { if (event.commandError() != KAEvent::CMD_NO_ERROR) return QColor(Qt::red); } break; } case Qt::ForegroundRole: if (event.commandError() != KAEvent::CMD_NO_ERROR) { if (event.actionTypes() == KAEvent::ACT_COMMAND) return QColor(Qt::white); QColor colour = Qt::red; int r, g, b; event.bgColour().getRgb(&r, &g, &b); if (r > 128 && g <= 128 && b <= 128) colour = QColor(Qt::white); return colour; } break; case Qt::DisplayRole: if (event.commandError() != KAEvent::CMD_NO_ERROR) return QLatin1String("!"); break; case SortRole: { const unsigned i = (event.actionTypes() == KAEvent::ACT_DISPLAY) ? event.bgColour().rgb() : 0; return QStringLiteral("%1").arg(i, 6, 10, QLatin1Char('0')); } default: break; } break; case TypeColumn: switch (role) { case Qt::BackgroundRole: calendarColour = true; break; case Qt::DecorationRole: { QVariant v; v.setValue(*eventIcon(event)); return v; } case Qt::TextAlignmentRole: return Qt::AlignHCenter; case Qt::SizeHintRole: return mIconSize; case Qt::AccessibleTextRole: //TODO: Implement accessibility return QString(); case ValueRole: return static_cast(event.actionSubType()); case SortRole: return QStringLiteral("%1").arg(event.actionSubType(), 2, 10, QLatin1Char('0')); } break; case TextColumn: switch (role) { case Qt::BackgroundRole: calendarColour = true; break; case Qt::DisplayRole: case SortRole: return AlarmText::summary(event, 1); case Qt::ToolTipRole: return AlarmText::summary(event, 10); default: break; } break; case TemplateNameColumn: switch (role) { case Qt::BackgroundRole: calendarColour = true; break; case Qt::DisplayRole: return event.templateName(); case SortRole: return event.templateName().toUpper(); } break; default: break; } if (calendarColour) { const QColor colour = resource.backgroundColour(); if (colour.isValid()) return colour; } switch (role) { case Qt::ForegroundRole: if (!event.enabled()) return Preferences::disabledColour(); if (event.expired()) return Preferences::archivedColour(); break; // use the default for normal active alarms case Qt::ToolTipRole: // Show the last command execution error message switch (event.commandError()) { case KAEvent::CMD_ERROR: return i18nc("@info:tooltip", "Command execution failed"); case KAEvent::CMD_ERROR_PRE: return i18nc("@info:tooltip", "Pre-alarm action execution failed"); case KAEvent::CMD_ERROR_POST: return i18nc("@info:tooltip", "Post-alarm action execution failed"); case KAEvent::CMD_ERROR_PRE_POST: return i18nc("@info:tooltip", "Pre- and post-alarm action execution failed"); default: case KAEvent::CMD_NO_ERROR: break; } break; case EnabledRole: return event.enabled(); default: break; } } handled = false; return QVariant(); } /****************************************************************************** * Return a resource's tooltip text. The resource's enabled status is * evaluated for specified alarm types. */ QString ResourceDataModelBase::tooltip(const Resource& resource, CalEvent::Types types) const { const QString name = QLatin1Char('@') + resource.displayName(); // insert markers for stripping out name const QString type = QLatin1Char('@') + resource.storageTypeString(false); // file/directory/URL etc. const QString locn = resource.displayLocation(); const bool inactive = !(resource.enabledTypes() & types); const QString readonly = readOnlyTooltip(resource); const bool writable = readonly.isEmpty(); //TODO: should the above line be = resource.isWritable() ? const QString disabled = i18nc("@info", "Disabled"); if (inactive && !writable) return xi18nc("@info:tooltip", "%1" "%2: %3" "%4, %5", name, type, locn, disabled, readonly); if (inactive || !writable) return xi18nc("@info:tooltip", "%1" "%2: %3" "%4", name, type, locn, (inactive ? disabled : readonly)); return xi18nc("@info:tooltip", "%1" "%2: %3", name, type, locn); } /****************************************************************************** * Return the repetition text. */ QString ResourceDataModelBase::repeatText(const KAEvent& event) { const QString repText = event.recurrenceText(true); return repText.isEmpty() ? event.repetitionText(true) : repText; } /****************************************************************************** * Return a string for sorting the repetition column. */ QString ResourceDataModelBase::repeatOrder(const KAEvent& event) { int repOrder = 0; int repInterval = 0; if (event.repeatAtLogin()) repOrder = 1; else { repInterval = event.recurInterval(); switch (event.recurType()) { case KARecurrence::MINUTELY: repOrder = 2; break; case KARecurrence::DAILY: repOrder = 3; break; case KARecurrence::WEEKLY: repOrder = 4; break; case KARecurrence::MONTHLY_DAY: case KARecurrence::MONTHLY_POS: repOrder = 5; break; case KARecurrence::ANNUAL_DATE: case KARecurrence::ANNUAL_POS: repOrder = 6; break; case KARecurrence::NO_RECUR: default: break; } } return QStringLiteral("%1%2").arg(static_cast('0' + repOrder)).arg(repInterval, 8, 10, QLatin1Char('0')); } /****************************************************************************** * Returns the QWhatsThis text for a specified column. */ QString ResourceDataModelBase::whatsThisText(int column) { switch (column) { case TimeColumn: return i18nc("@info:whatsthis", "Next scheduled date and time of the alarm"); case TimeToColumn: return i18nc("@info:whatsthis", "How long until the next scheduled trigger of the alarm"); case RepeatColumn: return i18nc("@info:whatsthis", "How often the alarm recurs"); case ColourColumn: return i18nc("@info:whatsthis", "Background color of alarm message"); case TypeColumn: return i18nc("@info:whatsthis", "Alarm type (message, file, command or email)"); case TextColumn: return i18nc("@info:whatsthis", "Alarm message text, URL of text file to display, command to execute, or email subject line"); case TemplateNameColumn: return i18nc("@info:whatsthis", "Name of the alarm template"); default: return QString(); } } /****************************************************************************** * Return the icon associated with an event's action. */ QPixmap* ResourceDataModelBase::eventIcon(const KAEvent& event) { switch (event.actionTypes()) { case KAEvent::ACT_EMAIL: return mEmailIcon; case KAEvent::ACT_AUDIO: return mAudioIcon; case KAEvent::ACT_COMMAND: return mCommandIcon; case KAEvent::ACT_DISPLAY: if (event.actionSubType() == KAEvent::FILE) return mFileIcon; Q_FALLTHROUGH(); // fall through to ACT_DISPLAY_COMMAND case KAEvent::ACT_DISPLAY_COMMAND: default: return mTextIcon; } } /****************************************************************************** * Display a message to the user. */ void ResourceDataModelBase::handleResourceMessage(ResourceType::MessageType type, const QString& message, const QString& details) { if (type == ResourceType::MessageType::Error) KAMessageBox::detailedError(MainWindow::mainMainWindow(), message, details); else if (type == ResourceType::MessageType::Info) KAMessageBox::informationList(MainWindow::mainMainWindow(), message, {details}); } bool ResourceDataModelBase::isMigrationComplete() const { return mMigrationStatus == 1; } bool ResourceDataModelBase::isMigrating() const { return mMigrationStatus == 0; } void ResourceDataModelBase::setMigrationInitiated(bool started) { mMigrationStatus = (started ? 0 : -1); } void ResourceDataModelBase::setMigrationComplete() { mMigrationStatus = 1; + Resources::notifyResourcesMigrated(); } // vim: et sw=4: diff --git a/src/resources/resources.cpp b/src/resources/resources.cpp index 42bc1748..195b2cde 100644 --- a/src/resources/resources.cpp +++ b/src/resources/resources.cpp @@ -1,599 +1,608 @@ /* * resource.cpp - generic class containing an alarm calendar resource * Program: kalarm * Copyright © 2019 David Jarvie * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "resources.h" #include "resource.h" #include "resourcedatamodelbase.h" #include "resourcemodel.h" #include "resourceselectdialog.h" #include "preferences.h" #include "lib/autoqpointer.h" #include "kalarm_debug.h" #include Resources* Resources::mInstance{nullptr}; // Copy of all ResourceType instances with valid ID, wrapped in the Resource // container which manages the instance. QHash Resources::mResources; bool Resources::mCreated{false}; bool Resources::mPopulated{false}; Resources* Resources::instance() { if (!mInstance) mInstance = new Resources; return mInstance; } Resources::Resources() { } Resource Resources::resource(ResourceId id) { return mResources.value(id, Resource::null()); } /****************************************************************************** * Return the resources which are enabled for a specified alarm type. * If 'writable' is true, only writable resources are included. */ QVector Resources::enabledResources(CalEvent::Type type, bool writable) { const CalEvent::Types types = (type == CalEvent::EMPTY) ? CalEvent::ACTIVE | CalEvent::ARCHIVED | CalEvent::TEMPLATE : type; QVector result; for (auto it = mResources.constBegin(); it != mResources.constEnd(); ++it) { const Resource& res = it.value(); if (writable && !res.isWritable()) continue; if (res.enabledTypes() & types) result += res; } return result; } /****************************************************************************** * Return the standard resource for an alarm type. */ Resource Resources::getStandard(CalEvent::Type type) { Resources* manager = instance(); bool wantDefaultArchived = (type == CalEvent::ARCHIVED); Resource defaultArchived; for (auto it = manager->mResources.constBegin(); it != manager->mResources.constEnd(); ++it) { const Resource& res = it.value(); if (res.isWritable(type)) { if (res.configIsStandard(type)) return res; if (wantDefaultArchived) { if (defaultArchived.isValid()) wantDefaultArchived = false; // found two archived alarm resources else defaultArchived = res; // this is the first archived alarm resource } } } if (wantDefaultArchived && defaultArchived.isValid()) { // There is no resource specified as the standard archived alarm // resource, but there is exactly one writable archived alarm // resource. Set that resource to be the standard. defaultArchived.configSetStandard(CalEvent::ARCHIVED, true); return defaultArchived; } return Resource(); } /****************************************************************************** * Return whether a collection is the standard collection for a specified * mime type. */ bool Resources::isStandard(const Resource& resource, CalEvent::Type type) { // If it's for archived alarms, get and also set the standard resource if // necessary. if (type == CalEvent::ARCHIVED) return getStandard(type) == resource; return resource.configIsStandard(type) && resource.isWritable(type); } /****************************************************************************** * Return the alarm types for which a resource is the standard resource. */ CalEvent::Types Resources::standardTypes(const Resource& resource, bool useDefault) { if (!resource.isWritable()) return CalEvent::EMPTY; Resources* manager = instance(); auto it = manager->mResources.constFind(resource.id()); if (it == manager->mResources.constEnd()) return CalEvent::EMPTY; CalEvent::Types stdTypes = resource.configStandardTypes() & resource.enabledTypes(); if (useDefault) { // Also return alarm types for which this is the only resource. // Check if it is the only writable resource for these type(s). if (!(stdTypes & CalEvent::ARCHIVED) && resource.isEnabled(CalEvent::ARCHIVED)) { // If it's the only enabled archived alarm resource, set it as standard. getStandard(CalEvent::ARCHIVED); stdTypes = resource.configStandardTypes() & resource.enabledTypes(); } CalEvent::Types enabledNotStd = resource.enabledTypes() & ~stdTypes; if (enabledNotStd) { // The resource is enabled for type(s) for which it is not the standard. for (auto itr = manager->mResources.constBegin(); itr != manager->mResources.constEnd() && enabledNotStd; ++itr) { const Resource& res = itr.value(); if (res != resource && res.isWritable()) { const CalEvent::Types en = res.enabledTypes() & enabledNotStd; if (en) enabledNotStd &= ~en; // this resource handles the same alarm type } } } stdTypes |= enabledNotStd; } return stdTypes; } /****************************************************************************** * Set or clear the standard status for a resource. */ void Resources::setStandard(Resource& resource, CalEvent::Type type, bool standard) { if (!(type & resource.enabledTypes())) return; Resources* manager = instance(); auto it = manager->mResources.find(resource.id()); if (it == manager->mResources.end()) return; resource = it.value(); // just in case it's a different object! if (standard == resource.configIsStandard(type)) return; if (!standard) resource.configSetStandard(type, false); else if (resource.isWritable(type)) { // Clear the standard status for any other resources. for (auto itr = manager->mResources.begin(); itr != manager->mResources.end(); ++itr) { Resource& res = itr.value(); if (res != resource) res.configSetStandard(type, false); } resource.configSetStandard(type, true); } } /****************************************************************************** * Set the alarm types for which a resource the standard resource. */ void Resources::setStandard(Resource& resource, CalEvent::Types types) { types &= resource.enabledTypes(); Resources* manager = instance(); auto it = manager->mResources.find(resource.id()); if (it == manager->mResources.end()) return; resource = it.value(); // just in case it's a different object! if (types != resource.configStandardTypes() && (!types || resource.isWritable())) { if (types) { // Clear the standard status for any other resources. for (auto itr = manager->mResources.begin(); itr != manager->mResources.end(); ++itr) { Resource& res = itr.value(); if (res != resource) { const CalEvent::Types rtypes = res.configStandardTypes(); if (rtypes & types) res.configSetStandard(rtypes & ~types); } } } resource.configSetStandard(types); } } /****************************************************************************** -* Get the collection to use for storing an alarm. -* Optionally, the standard collection for the alarm type is returned. If more -* than one collection is a candidate, the user is prompted. +* Find the resource to be used to store an event of a given type. +* This will be the standard resource for the type, but if this is not valid, +* the user will be prompted to select a resource. */ -Resource Resources::destination(ResourceListModel* model, CalEvent::Type type, QWidget* promptParent, bool noPrompt, bool* cancelled) +Resource Resources::destination(CalEvent::Type type, QWidget* promptParent, bool noPrompt, bool* cancelled) { if (cancelled) *cancelled = false; Resource standard; if (type == CalEvent::EMPTY) return standard; standard = getStandard(type); // Archived alarms are always saved in the default resource, // else only prompt if necessary. if (type == CalEvent::ARCHIVED || noPrompt || (!Preferences::askResource() && standard.isValid())) return standard; // Prompt for which collection to use + ResourceListModel* model = DataModel::createResourceListModel(promptParent); model->setFilterWritable(true); model->setFilterEnabled(true); model->setEventTypeFilter(type); model->useResourceColour(false); Resource res; switch (model->rowCount()) { case 0: break; case 1: res = model->resource(0); break; default: { // Use AutoQPointer to guard against crash on application exit while // the dialogue is still open. It prevents double deletion (both on // deletion of 'promptParent', and on return from this function). AutoQPointer dlg = new ResourceSelectDialog(model, promptParent); dlg->setWindowTitle(i18nc("@title:window", "Choose Calendar")); dlg->setDefaultResource(standard); if (dlg->exec()) res = dlg->selectedResource(); if (!res.isValid() && cancelled) *cancelled = true; } } return res; } /****************************************************************************** * Return whether all configured resources have been created. */ bool Resources::allCreated() { return instance()->mCreated; } /****************************************************************************** * Return whether all configured resources have been loaded at least once. */ bool Resources::allPopulated() { return instance()->mPopulated; } /****************************************************************************** * Return the resource which an event belongs to, provided its alarm type is * enabled. */ Resource Resources::resourceForEvent(const QString& eventId) { for (auto it = mResources.constBegin(); it != mResources.constEnd(); ++it) { const Resource& res = it.value(); if (res.containsEvent(eventId)) return res; } return Resource::null(); } /****************************************************************************** * Return the resource which an event belongs to, and the event, provided its * alarm type is enabled. */ Resource Resources::resourceForEvent(const QString& eventId, KAEvent& event) { for (auto it = mResources.constBegin(); it != mResources.constEnd(); ++it) { const Resource& res = it.value(); event = res.event(eventId); if (event.isValid()) return res; } if (mResources.isEmpty()) // otherwise, 'event' was set invalid in the loop event = KAEvent(); return Resource::null(); } /****************************************************************************** * Return the resource which has a given configuration identifier. */ Resource Resources::resourceForConfigName(const QString& configName) { for (auto it = mResources.constBegin(); it != mResources.constEnd(); ++it) { const Resource& res = it.value(); if (res.configName() == configName) return res; } return Resource::null(); } /****************************************************************************** * Called after a new resource has been created, when it has completed its * initialisation. */ void Resources::notifyNewResourceInitialised(Resource& res) { if (res.isValid()) Q_EMIT instance()->resourceAdded(res); } /****************************************************************************** * Called when all configured resources have been created for the first time. */ void Resources::notifyResourcesCreated() { mCreated = true; Q_EMIT instance()->resourcesCreated(); checkResourcesPopulated(); } /****************************************************************************** * Called when a resource's events have been loaded. * Emits a signal if all collections have been populated. */ void Resources::notifyResourcePopulated(const ResourceType* res) { if (res) { Resource r = resource(res->id()); if (r.isValid()) Q_EMIT instance()->resourcePopulated(r); } // Check whether all resources have now loaded at least once. checkResourcesPopulated(); } +/****************************************************************************** +* Called to notify that migration/creation of resources has completed. +*/ +void Resources::notifyResourcesMigrated() +{ + Q_EMIT instance()->migrationCompleted(); +} + /****************************************************************************** * Called by a resource to notify that its settings have changed. * Emits the settingsChanged() signal. * If the resource is now read-only and was standard, clear its standard status. * If the resource has newly enabled alarm types, ensure that it doesn't * duplicate any existing standard setting. */ void Resources::notifySettingsChanged(ResourceType* res, ResourceType::Changes change, CalEvent::Types oldEnabled) { if (!res) return; Resource r = resource(res->id()); if (!r.isValid()) return; Resources* manager = instance(); if (change & ResourceType::Enabled) { ResourceType::Changes change = ResourceType::Enabled; // Find which alarm types (if any) have been newly enabled. const CalEvent::Types extra = res->enabledTypes() & ~oldEnabled; CalEvent::Types std = res->configStandardTypes(); const CalEvent::Types extraStd = std & extra; if (extraStd && res->isWritable()) { // Alarm type(s) have been newly enabled, and are set as standard. // Don't allow the resource to be set as standard for those types if // another resource is already the standard. CalEvent::Types disallowedStdTypes{}; for (auto it = manager->mResources.constBegin(); it != manager->mResources.constEnd(); ++it) { const Resource& resit = it.value(); if (resit.id() != res->id() && resit.isWritable()) { disallowedStdTypes |= extraStd & resit.configStandardTypes() & resit.enabledTypes(); if (extraStd == disallowedStdTypes) break; // all the resource's newly enabled standard types are disallowed } } if (disallowedStdTypes) { std &= ~disallowedStdTypes; res->configSetStandard(std); } } if (std) change |= ResourceType::Standard; } Q_EMIT manager->settingsChanged(r, change); if ((change & ResourceType::ReadOnly) && res->readOnly()) { qCDebug(KALARM_LOG) << "Resources::notifySettingsChanged:" << res->id() << "ReadOnly"; // A read-only resource can't be the default for any alarm type const CalEvent::Types std = standardTypes(r, false); if (std != CalEvent::EMPTY) { setStandard(r, CalEvent::EMPTY); bool singleType = true; QString msg; switch (std) { case CalEvent::ACTIVE: msg = xi18n("The calendar %1 has been made read-only. " "This was the default calendar for active alarms.", res->displayName()); break; case CalEvent::ARCHIVED: msg = xi18n("The calendar %1 has been made read-only. " "This was the default calendar for archived alarms.", res->displayName()); break; case CalEvent::TEMPLATE: msg = xi18n("The calendar %1 has been made read-only. " "This was the default calendar for alarm templates.", res->displayName()); break; default: msg = xi18nc("@info", "The calendar %1 has been made read-only. " "This was the default calendar for:%2" "Please select new default calendars.", res->displayName(), ResourceDataModelBase::typeListForDisplay(std)); singleType = false; break; } if (singleType) msg = xi18nc("@info", "%1Please select a new default calendar.", msg); notifyResourceMessage(res->id(), ResourceType::MessageType::Info, msg, QString()); } } } void Resources::notifyResourceMessage(ResourceType* res, ResourceType::MessageType type, const QString& message, const QString& details) { if (res) notifyResourceMessage(res->id(), type, message, details); } void Resources::notifyResourceMessage(ResourceId id, ResourceType::MessageType type, const QString& message, const QString& details) { Resource r = resource(id); if (r.isValid()) Q_EMIT instance()->resourceMessage(r, type, message, details); } void Resources::notifyEventsAdded(ResourceType* res, const QList& events) { if (res) { Resource r = resource(res->id()); if (r.isValid()) Q_EMIT instance()->eventsAdded(r, events); } } void Resources::notifyEventUpdated(ResourceType* res, const KAEvent& event) { if (res) { Resource r = resource(res->id()); if (r.isValid()) Q_EMIT instance()->eventUpdated(r, event); } } void Resources::notifyEventsToBeRemoved(ResourceType* res, const QList& events) { if (res) { Resource r = resource(res->id()); if (r.isValid()) Q_EMIT instance()->eventsToBeRemoved(r, events); } } bool Resources::addResource(ResourceType* instance, Resource& resource) { if (!instance || instance->id() < 0) { // Instance is invalid - return an invalid resource. delete instance; resource = Resource::null(); return false; } auto it = mResources.constFind(instance->id()); if (it != mResources.constEnd()) { // Instance ID already exists - return the existing resource. delete instance; resource = it.value(); return false; } // Add a new resource. resource = Resource(instance); mResources[instance->id()] = resource; return true; } void Resources::removeResource(ResourceId id) { if (mResources.remove(id) > 0) Q_EMIT instance()->resourceRemoved(id); } /****************************************************************************** * To be called when a resource has been created or loaded. * If all resources have now loaded for the first time, emit signal. */ void Resources::checkResourcesPopulated() { if (!mPopulated && mCreated) { // Check whether all resources have now loaded at least once. for (auto it = mResources.constBegin(); it != mResources.constEnd(); ++it) { if (!it.value().isPopulated()) return; } mPopulated = true; Q_EMIT instance()->resourcesPopulated(); } } #if 0 /****************************************************************************** * Return whether one or all enabled collections have been loaded. */ bool Resources::isPopulated(ResourceId id) { if (id >= 0) { const Resource res = resource(id); return res.isPopulated() || res.enabledTypes() == CalEvent::EMPTY; } for (auto it = mResources.constBegin(); it != mResources.constEnd(); ++it) { const Resource& res = it.value(); if (!res.isPopulated() && res.enabledTypes() != CalEvent::EMPTY) return false; } return true; } #endif // vim: et sw=4: diff --git a/src/resources/resources.h b/src/resources/resources.h index 1f538830..bfecce25 100644 --- a/src/resources/resources.h +++ b/src/resources/resources.h @@ -1,287 +1,288 @@ /* * resources.h - container for all ResourceType instances * Program: kalarm * Copyright © 2019 David Jarvie * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef RESOURCES_H #define RESOURCES_H +#include "datamodel.h" #include "resource.h" #include "resourcemodel.h" #include class QEventLoop; using namespace KAlarmCal; /** Class to contain all ResourceType instances. * It provides connection to signals from all ResourceType instances. */ class Resources : public QObject { Q_OBJECT public: /** Creates the unique Resources instance. */ static Resources* instance(); ~Resources() {} Resources(const Resources&) = delete; Resources& operator=(const Resources&) const = delete; /** Return a copy of the resource with a given ID. * @return The resource, or invalid if the ID doesn't already exist or is invalid. */ static Resource resource(ResourceId); /** Remove a resource. The calendar file is not removed. * @return true if the resource has been removed or a removal job has been scheduled. */ static bool removeResource(Resource&); /** Return all resources of a kind which contain a specified alarm type. * @tparam Type Resource type to fetch, default = all types. * @param alarmType Alarm type to check for, or CalEvent::EMPTY for any type. */ template static QVector allResources(CalEvent::Type alarmType = CalEvent::EMPTY); /** Return the enabled resources which contain a specified alarm type. * @param type Alarm type to check for, or CalEvent::EMPTY for any type. * @param writable If true, only writable resources are included. */ static QVector enabledResources(CalEvent::Type type = CalEvent::EMPTY, bool writable = false); /** Return the standard resource for an alarm type. This is the resource * which can be set as the default to add new alarms to. * Only enabled and writable resources can be standard. * In the case of archived alarm resources, if no resource is specified * as standard and there is exactly one writable archived alarm resource, * that resource will be automatically set as standard. * * @param type alarm type * @return standard resource, or null if none. */ static Resource getStandard(CalEvent::Type type); /** Return whether a resource is the standard resource for a specified alarm * type. Only enabled and writable resources can be standard. * In the case of archived alarms, if no resource is specified as standard * and the resource is the only writable archived alarm resource, it will * be automatically set as standard. */ static bool isStandard(const Resource& resource, CalEvent::Type); /** Return the alarm type(s) for which a resource is the standard resource. * Only enabled and writable resources can be standard. * @param useDefault false to return the defined standard types, if any; * true to return the types for which it is the standard * or only resource. */ static CalEvent::Types standardTypes(const Resource& resource, bool useDefault = false); /** Set or clear a resource as the standard resource for a specified alarm * type. This does not affect its status for other alarm types. * The resource must be writable and enabled for the type, to set * standard = true. * If the resource is being set as standard, the standard status for the * alarm type is cleared for any other resources. */ static void setStandard(Resource& resource, CalEvent::Type, bool standard); /** Set which alarm types a resource is the standard resource for. * Its standard status is cleared for other alarm types. * The resource must be writable and enabled for the type, to set * standard = true. * If the resource is being set as standard for any alarm types, the * standard status is cleared for those alarm types for any other resources. */ static void setStandard(Resource& resource, CalEvent::Types); /** Find the resource to be used to store an event of a given type. * This will be the standard resource for the type, but if this is not valid, * the user will be prompted to select a resource. * @param type The event type * @param promptParent The parent widget for the prompt * @param noPrompt Don't prompt the user even if the standard resource is not valid * @param cancelled If non-null: set to true if the user cancelled the * prompt dialogue; set to false if any other error */ - template static Resource destination(CalEvent::Type type, QWidget* promptParent = nullptr, bool noPrompt = false, bool* cancelled = nullptr); /** Return whether all configured resources have been created. */ static bool allCreated(); /** Return whether all configured resources have been loaded at least once. */ static bool allPopulated(); /** Return the resource which an event belongs to, provided that the event's * alarm type is enabled. */ static Resource resourceForEvent(const QString& eventId); /** Return the resource which an event belongs to, and the event, provided * that the event's alarm type is enabled. */ static Resource resourceForEvent(const QString& eventId, KAEvent& event); /** Return the resource which has a given configuration identifier. */ static Resource resourceForConfigName(const QString& configName); /** Called to notify that a new resource has completed its initialisation, * in order to emit the resourceAdded() signal. */ static void notifyNewResourceInitialised(Resource&); /** Called to notify that all configured resources have now been created. */ static void notifyResourcesCreated(); /** Called by a resource to notify that loading of events has successfully completed. */ static void notifyResourcePopulated(const ResourceType*); + /** Called to notify that migration/creation of resources has completed. */ + static void notifyResourcesMigrated(); + /** Called by a resource to notify that its settings have changed. * This will cause the settingsChanged() signal to be emitted. */ static void notifySettingsChanged(ResourceType*, ResourceType::Changes, CalEvent::Types oldEnabled); /** Called by a resource when a user message should be displayed. * This will cause the resourceMessage() signal to be emitted. * @param message Must include the resource's display name in order to * identify the resource to the user. */ static void notifyResourceMessage(ResourceType*, ResourceType::MessageType, const QString& message, const QString& details); /** Called when a user message should be displayed for a resource. * This will cause the resourceMessage() signal to be emitted. * @param message Must include the resource's display name in order to * identify the resource to the user. */ static void notifyResourceMessage(ResourceId, ResourceType::MessageType, const QString& message, const QString& details); /** Called by a resource to notify that it has added events. */ static void notifyEventsAdded(ResourceType*, const QList&); /** Called by a resource to notify that it has changed an event. */ static void notifyEventUpdated(ResourceType*, const KAEvent& event); /** Called by a resource to notify that it is about to delete events. */ static void notifyEventsToBeRemoved(ResourceType*, const QList&); Q_SIGNALS: /** Emitted when a resource's settings have changed. */ void settingsChanged(Resource&, ResourceType::Changes); /** Emitted when all configured resource have been created (but not - * necessarily populated). */ + * necessarily populated). Note that after this, resource migration and + * the creation of default resources is performed and notified by the + * signal migrationCompleted(). + */ void resourcesCreated(); /** Emitted when all configured resources have been loaded for the first time. */ void resourcesPopulated(); + /** Signal emitted when resource migration/creation at startup has completed. */ + void migrationCompleted(); + /** Emitted when a new resource has been created. */ void resourceAdded(Resource&); /** Emitted when a resource's events have been successfully loaded. */ void resourcePopulated(Resource&); /** Emitted when a resource's config and settings have been removed. */ void resourceRemoved(ResourceId); /** Emitted when a resource message should be displayed to the user. * @note Connections to this signal should use Qt::QueuedConnection type * to allow processing to continue while the user message is displayed. */ void resourceMessage(Resource&, ResourceType::MessageType, const QString& message, const QString& details); /** Emitted when events have been added to a resource. * Events are only notified whose alarm type is enabled. */ void eventsAdded(Resource&, const QList&); /** Emitted when an event has been updated in a resource. * Events are only notified whose alarm type is enabled. */ void eventUpdated(Resource&, const KAEvent&); /** Emitted when events are about to be deleted from a resource. * Events are only notified whose alarm type is enabled. */ void eventsToBeRemoved(Resource&, const QList&); private: Resources(); - static Resource destination(ResourceListModel* model, CalEvent::Type type, QWidget* promptParent, bool noPrompt, bool* cancelled); /** Add a new ResourceType instance, with a Resource owner. * Once the resource has completed its initialisation, call * notifyNewResourceInitialised() to emit the resourceAdded() signal. * is require * @param type Newly constructed ResourceType instance, which will belong to * 'resource' if successful. On error, it will be deleted. * @param resource If type is invalid, updated to an invalid resource; * If type ID already exists, updated to the existing resource with that ID; * If type ID doesn't exist, updated to the new resource containing res. * @return true if a new resource has been created, false if invalid or already exists. */ static bool addResource(ResourceType* type, Resource& resource); /** Remove the resource with a given ID. * @note The ResourceType instance will only be deleted once all Resource * instances which refer to this ID go out of scope. */ static void removeResource(ResourceId); static void checkResourcesPopulated(); static Resources* mInstance; // the unique instance static QHash mResources; // contains all ResourceType instances with an ID static bool mCreated; // all resources have been created static bool mPopulated; // all resources have been loaded once friend class ResourceType; }; /*============================================================================= * Template definitions. *============================================================================*/ template QVector Resources::allResources(CalEvent::Type type) { const CalEvent::Types types = (type == CalEvent::EMPTY) ? CalEvent::ACTIVE | CalEvent::ARCHIVED | CalEvent::TEMPLATE : type; QVector result; for (auto it = mResources.constBegin(); it != mResources.constEnd(); ++it) { const Resource& res = it.value(); if (res.is() && (res.alarmTypes() & types)) result += res; } return result; } -template -Resource Resources::destination(CalEvent::Type type, QWidget* promptParent, bool noPrompt, bool* cancelled) -{ - ResourceListModel* model = ResourceListModel::create(promptParent); - return destination(model, type, promptParent, noPrompt, cancelled); -} - #endif // RESOURCES_H // vim: et sw=4: diff --git a/src/resourceselector.cpp b/src/resourceselector.cpp index b7b0269e..236d3166 100644 --- a/src/resourceselector.cpp +++ b/src/resourceselector.cpp @@ -1,575 +1,576 @@ /* * resourceselector.cpp - calendar resource selection widget * Program: kalarm * Copyright © 2006-2019 David Jarvie * Based on KOrganizer's ResourceView class and KAddressBook's ResourceSelection class, * Copyright (C) 2003,2004 Cornelius Schumacher * Copyright (C) 2003-2004 Reinhold Kainhofer * Copyright (c) 2004 Tobias Koenig * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "resourceselector.h" #include "alarmcalendar.h" #include "kalarmapp.h" #include "preferences.h" #include "resources/akonadidatamodel.h" #include "resources/akonadiresourcecreator.h" #include "resources/akonadiresourcemigrator.h" +#include "resources/datamodel.h" #include "resources/resources.h" #include "resources/resourcemodel.h" #include "lib/autoqpointer.h" #include "lib/messagebox.h" #include "lib/packedlayout.h" #include "kalarm_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include using namespace KCalendarCore; ResourceSelector::ResourceSelector(QWidget* parent) : QFrame(parent) { QBoxLayout* topLayout = new QVBoxLayout(this); QLabel* label = new QLabel(i18nc("@title:group", "Calendars"), this); topLayout->addWidget(label, 0, Qt::AlignHCenter); mAlarmType = new QComboBox(this); mAlarmType->addItem(i18nc("@item:inlistbox", "Active Alarms")); mAlarmType->addItem(i18nc("@item:inlistbox", "Archived Alarms")); mAlarmType->addItem(i18nc("@item:inlistbox", "Alarm Templates")); mAlarmType->setFixedHeight(mAlarmType->sizeHint().height()); mAlarmType->setWhatsThis(i18nc("@info:whatsthis", "Choose which type of data to show alarm calendars for")); topLayout->addWidget(mAlarmType); // No spacing between combo box and listview. - ResourceFilterCheckListModel* model = ResourceFilterCheckListModel::create(this); + ResourceFilterCheckListModel* model = DataModel::createResourceFilterCheckListModel(this); mListView = new ResourceView(model, this); connect(mListView->selectionModel(), &QItemSelectionModel::selectionChanged, this, &ResourceSelector::selectionChanged); mListView->setContextMenuPolicy(Qt::CustomContextMenu); connect(mListView, &ResourceView::customContextMenuRequested, this, &ResourceSelector::contextMenuRequested); mListView->setWhatsThis(i18nc("@info:whatsthis", "List of available calendars of the selected type. The checked state shows whether a calendar " "is enabled (checked) or disabled (unchecked). The default calendar is shown in bold.")); topLayout->addWidget(mListView, 1); PackedLayout* blayout = new PackedLayout(Qt::AlignHCenter); blayout->setContentsMargins(0, 0, 0, 0); topLayout->addLayout(blayout); mAddButton = new QPushButton(i18nc("@action:button", "Add..."), this); mEditButton = new QPushButton(i18nc("@action:button", "Edit..."), this); mDeleteButton = new QPushButton(i18nc("@action:button", "Remove"), this); blayout->addWidget(mAddButton); blayout->addWidget(mEditButton); blayout->addWidget(mDeleteButton); mEditButton->setWhatsThis(i18nc("@info:whatsthis", "Edit the highlighted calendar")); mDeleteButton->setWhatsThis(xi18nc("@info:whatsthis", "Remove the highlighted calendar from the list." "The calendar itself is left intact, and may subsequently be reinstated in the list if desired.")); mEditButton->setDisabled(true); mDeleteButton->setDisabled(true); connect(mAddButton, &QPushButton::clicked, this, &ResourceSelector::addResource); connect(mEditButton, &QPushButton::clicked, this, &ResourceSelector::editResource); connect(mDeleteButton, &QPushButton::clicked, this, &ResourceSelector::removeResource); connect(Resources::instance(), &Resources::resourceRemoved, this, &ResourceSelector::selectionChanged); connect(mAlarmType, static_cast(&QComboBox::activated), this, &ResourceSelector::alarmTypeSelected); QTimer::singleShot(0, this, SLOT(alarmTypeSelected())); Preferences::connect(SIGNAL(archivedKeepDaysChanged(int)), this, SLOT(archiveDaysChanged(int))); } /****************************************************************************** * Called when an alarm type has been selected. * Filter the resource list to show resources of the selected alarm type, and * add appropriate whatsThis texts to the list and to the Add button. */ void ResourceSelector::alarmTypeSelected() { QString addTip; switch (mAlarmType->currentIndex()) { case 0: mCurrentAlarmType = CalEvent::ACTIVE; addTip = i18nc("@info:tooltip", "Add a new active alarm calendar"); break; case 1: mCurrentAlarmType = CalEvent::ARCHIVED; addTip = i18nc("@info:tooltip", "Add a new archived alarm calendar"); break; case 2: mCurrentAlarmType = CalEvent::TEMPLATE; addTip = i18nc("@info:tooltip", "Add a new alarm template calendar"); break; } // WORKAROUND: Switch scroll bars off to avoid crash (see explanation // in reinstateAlarmTypeScrollBars() description). mListView->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); mListView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); mListView->resourceModel()->setEventTypeFilter(mCurrentAlarmType); mAddButton->setWhatsThis(addTip); mAddButton->setToolTip(addTip); // WORKAROUND: Switch scroll bars back on after allowing geometry to update ... QTimer::singleShot(0, this, &ResourceSelector::reinstateAlarmTypeScrollBars); selectionChanged(); // enable/disable buttons } /****************************************************************************** * WORKAROUND for crash due to presumed Qt bug. * Switch scroll bars off. This is to avoid a crash which can very occasionally * happen when changing from a list of calendars which requires vertical scroll * bars, to a list whose text is very slightly wider but which doesn't require * scroll bars at all. (The suspicion is that the width is such that it would * require horizontal scroll bars if the vertical scroll bars were still * present.) Presumably due to a Qt bug, this can result in a recursive call to * ResourceView::viewportEvent() with a Resize event. * * The crash only occurs if the ResourceSelector happens to have exactly (within * one pixel) the "right" width to create the crash. */ void ResourceSelector::reinstateAlarmTypeScrollBars() { mListView->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); mListView->setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded); } /****************************************************************************** * Prompt the user for a new resource to add to the list. */ void ResourceSelector::addResource() { AkonadiResourceCreator* creator = new AkonadiResourceCreator(mCurrentAlarmType, this); connect(creator, &AkonadiResourceCreator::resourceAdded, this, &ResourceSelector::slotResourceAdded); creator->createResource(); } /****************************************************************************** -* Called when a collection is added to the AkonadiDataModel, after being +* Called when a resource is added to the calendar data model, after being * created by addResource(). */ void ResourceSelector::slotResourceAdded(Resource& resource, CalEvent::Type alarmType) { const CalEvent::Types types = resource.alarmTypes(); resource.setEnabled(types); if (!(types & alarmType)) { - // The user has selected alarm types for the resource - // which don't include the currently displayed type. - // Show a collection list which includes a selected type. + // The user has selected alarm types for the resource which don't + // include the currently displayed type. + // Show a resource list which includes a selected type. int index = -1; if (types & CalEvent::ACTIVE) index = 0; else if (types & CalEvent::ARCHIVED) index = 1; else if (types & CalEvent::TEMPLATE) index = 2; if (index >= 0) { mAlarmType->setCurrentIndex(index); alarmTypeSelected(); } } } /****************************************************************************** * Edit the currently selected resource. */ void ResourceSelector::editResource() { currentResource().editResource(this); } /****************************************************************************** * Update the backend storage format for the currently selected resource in the * displayed list. */ void ResourceSelector::updateResource() { const Resource resource = currentResource(); if (!resource.isValid()) return; AkonadiResourceMigrator::updateToCurrentFormat(resource, true, this); } /****************************************************************************** * Remove the currently selected resource from the displayed list. */ void ResourceSelector::removeResource() { Resource resource = currentResource(); if (!resource.isValid()) return; const QString name = resource.configName(); // Check if it's the standard or only resource for at least one type. const CalEvent::Types allTypes = resource.alarmTypes(); const CalEvent::Types standardTypes = Resources::standardTypes(resource, true); const CalEvent::Type currentType = currentResourceType(); const CalEvent::Type stdType = (standardTypes & CalEvent::ACTIVE) ? CalEvent::ACTIVE : (standardTypes & CalEvent::ARCHIVED) ? CalEvent::ARCHIVED : CalEvent::EMPTY; if (stdType == CalEvent::ACTIVE) { KAMessageBox::sorry(this, i18nc("@info", "You cannot remove your default active alarm calendar.")); return; } if (stdType == CalEvent::ARCHIVED && Preferences::archivedKeepDays()) { // Only allow the archived alarms standard resource to be removed if // we're not saving archived alarms. KAMessageBox::sorry(this, i18nc("@info", "You cannot remove your default archived alarm calendar " "while expired alarms are configured to be kept.")); return; } QString text; if (standardTypes) { // It's a standard resource for at least one alarm type if (allTypes != currentType) { // It also contains alarm types other than the currently displayed type const QString stdTypes = ResourceDataModelBase::typeListForDisplay(standardTypes); QString otherTypes; const CalEvent::Types nonStandardTypes(allTypes & ~standardTypes); if (nonStandardTypes != currentType) otherTypes = xi18nc("@info", "It also contains:%1", ResourceDataModelBase::typeListForDisplay(nonStandardTypes)); text = xi18nc("@info", "%1 is the default calendar for:%2%3" "Do you really want to remove it from all calendar lists?", name, stdTypes, otherTypes); } else text = xi18nc("@info", "Do you really want to remove your default calendar (%1) from the list?", name); } else if (allTypes != currentType) text = xi18nc("@info", "%1 contains:%2Do you really want to remove it from all calendar lists?", name, ResourceDataModelBase::typeListForDisplay(allTypes)); else text = xi18nc("@info", "Do you really want to remove the calendar %1 from the list?", name); if (KAMessageBox::warningContinueCancel(this, text, QString(), KStandardGuiItem::remove()) == KMessageBox::Cancel) return; resource.removeResource(); } /****************************************************************************** * Called when the current selection changes, to enable/disable the * Delete and Edit buttons accordingly. */ void ResourceSelector::selectionChanged() { bool state = mListView->selectionModel()->selectedRows().count(); mDeleteButton->setEnabled(state); mEditButton->setEnabled(state); } /****************************************************************************** * Initialise the button and context menu actions. */ void ResourceSelector::initActions(KActionCollection* actions) { mActionReload = new QAction(QIcon::fromTheme(QStringLiteral("view-refresh")), i18nc("@action Reload calendar", "Re&load"), this); actions->addAction(QStringLiteral("resReload"), mActionReload); connect(mActionReload, &QAction::triggered, this, &ResourceSelector::reloadResource); mActionShowDetails = new QAction(QIcon::fromTheme(QStringLiteral("help-about")), i18nc("@action", "Show &Details"), this); actions->addAction(QStringLiteral("resDetails"), mActionShowDetails); connect(mActionShowDetails, &QAction::triggered, this, &ResourceSelector::showInfo); mActionSetColour = new QAction(QIcon::fromTheme(QStringLiteral("color-picker")), i18nc("@action", "Set &Color..."), this); actions->addAction(QStringLiteral("resSetColour"), mActionSetColour); connect(mActionSetColour, &QAction::triggered, this, &ResourceSelector::setColour); mActionClearColour = new QAction(i18nc("@action", "Clear C&olor"), this); actions->addAction(QStringLiteral("resClearColour"), mActionClearColour); connect(mActionClearColour, &QAction::triggered, this, &ResourceSelector::clearColour); mActionEdit = new QAction(QIcon::fromTheme(QStringLiteral("document-properties")), i18nc("@action", "&Edit..."), this); actions->addAction(QStringLiteral("resEdit"), mActionEdit); connect(mActionEdit, &QAction::triggered, this, &ResourceSelector::editResource); mActionUpdate = new QAction(i18nc("@action", "&Update Calendar Format"), this); actions->addAction(QStringLiteral("resUpdate"), mActionUpdate); connect(mActionUpdate, &QAction::triggered, this, &ResourceSelector::updateResource); mActionRemove = new QAction(QIcon::fromTheme(QStringLiteral("edit-delete")), i18nc("@action", "&Remove"), this); actions->addAction(QStringLiteral("resRemove"), mActionRemove); connect(mActionRemove, &QAction::triggered, this, &ResourceSelector::removeResource); mActionSetDefault = new KToggleAction(this); actions->addAction(QStringLiteral("resDefault"), mActionSetDefault); connect(mActionSetDefault, &KToggleAction::triggered, this, &ResourceSelector::setStandard); QAction* action = new QAction(QIcon::fromTheme(QStringLiteral("document-new")), i18nc("@action", "&Add..."), this); actions->addAction(QStringLiteral("resAdd"), action); connect(action, &QAction::triggered, this, &ResourceSelector::addResource); mActionImport = new QAction(i18nc("@action", "Im&port..."), this); actions->addAction(QStringLiteral("resImport"), mActionImport); connect(mActionImport, &QAction::triggered, this, &ResourceSelector::importCalendar); mActionExport = new QAction(i18nc("@action", "E&xport..."), this); actions->addAction(QStringLiteral("resExport"), mActionExport); connect(mActionExport, &QAction::triggered, this, &ResourceSelector::exportCalendar); } void ResourceSelector::setContextMenu(QMenu* menu) { mContextMenu = menu; } /****************************************************************************** * Display the context menu for the selected calendar. */ void ResourceSelector::contextMenuRequested(const QPoint& viewportPos) { if (!mContextMenu) return; bool active = false; bool writable = false; bool updatable = false; Resource resource; if (mListView->selectionModel()->hasSelection()) { const QModelIndex index = mListView->indexAt(viewportPos); if (index.isValid()) resource = mListView->resourceModel()->resource(index); else mListView->clearSelection(); } CalEvent::Type type = currentResourceType(); bool haveCalendar = resource.isValid(); if (haveCalendar) { active = resource.isEnabled(type); const int rw = resource.writableStatus(type); writable = (rw > 0); const KACalendar::Compat compatibility = resource.compatibility(); if (!rw && (compatibility & ~KACalendar::Converted) && !(compatibility & ~(KACalendar::Convertible | KACalendar::Converted))) updatable = true; // the calendar format is convertible to the current KAlarm format if (!(resource.alarmTypes() & type)) type = CalEvent::EMPTY; } mActionReload->setEnabled(active); mActionShowDetails->setEnabled(haveCalendar); mActionSetColour->setEnabled(haveCalendar); mActionClearColour->setEnabled(haveCalendar); mActionClearColour->setVisible(resource.backgroundColour().isValid()); mActionEdit->setEnabled(haveCalendar); mActionUpdate->setEnabled(updatable); mActionRemove->setEnabled(haveCalendar); mActionImport->setEnabled(active && writable); mActionExport->setEnabled(active); QString text; switch (type) { case CalEvent::ACTIVE: text = i18nc("@action", "Use as &Default for Active Alarms"); break; case CalEvent::ARCHIVED: text = i18nc("@action", "Use as &Default for Archived Alarms"); break; case CalEvent::TEMPLATE: text = i18nc("@action", "Use as &Default for Alarm Templates"); break; default: break; } mActionSetDefault->setText(text); bool standard = Resources::isStandard(resource, type); mActionSetDefault->setChecked(active && writable && standard); mActionSetDefault->setEnabled(active && writable); mContextMenu->popup(mListView->viewport()->mapToGlobal(viewportPos)); } /****************************************************************************** * Called from the context menu to reload the selected resource. */ void ResourceSelector::reloadResource() { - const Resource resource = currentResource(); + Resource resource = currentResource(); if (resource.isValid()) - AkonadiDataModel::instance()->reloadResource(resource); + DataModel::reload(resource); } /****************************************************************************** * Called from the context menu to save the selected resource. */ void ResourceSelector::saveResource() { // Save resource is not applicable to Akonadi } /****************************************************************************** * Called when the length of time archived alarms are to be stored changes. * If expired alarms are now to be stored, this also sets any single archived * alarm resource to be the default. */ void ResourceSelector::archiveDaysChanged(int days) { if (days) { const Resource resource = Resources::getStandard(CalEvent::ARCHIVED); if (resource.isValid()) theApp()->purgeNewArchivedDefault(resource); } } /****************************************************************************** * Called from the context menu to set the selected resource as the default * for its alarm type. The resource is automatically made active. */ void ResourceSelector::setStandard() { Resource resource = currentResource(); if (resource.isValid()) { CalEvent::Type alarmType = currentResourceType(); bool standard = mActionSetDefault->isChecked(); if (standard) resource.setEnabled(alarmType, true); Resources::setStandard(resource, alarmType, standard); if (alarmType == CalEvent::ARCHIVED) theApp()->purgeNewArchivedDefault(resource); } } /****************************************************************************** * Called from the context menu to merge alarms from an external calendar into * the selected resource (if any). */ void ResourceSelector::importCalendar() { Resource resource = currentResource(); AlarmCalendar::resources()->importAlarms(this, &resource); } /****************************************************************************** * Called from the context menu to copy the selected resource's alarms to an * external calendar. */ void ResourceSelector::exportCalendar() { const Resource resource = currentResource(); if (resource.isValid()) AlarmCalendar::exportAlarms(AlarmCalendar::resources()->events(resource), this); } /****************************************************************************** * Called from the context menu to set a colour for the selected resource. */ void ResourceSelector::setColour() { Resource resource = currentResource(); if (resource.isValid()) { QColor colour = resource.backgroundColour(); if (!colour.isValid()) colour = QApplication::palette().color(QPalette::Base); colour = QColorDialog::getColor(colour, this); if (colour.isValid()) resource.setBackgroundColour(colour); } } /****************************************************************************** * Called from the context menu to clear the display colour for the selected * resource. */ void ResourceSelector::clearColour() { Resource resource = currentResource(); if (resource.isValid()) resource.setBackgroundColour(QColor()); } /****************************************************************************** * Called from the context menu to display information for the selected resource. */ void ResourceSelector::showInfo() { const Resource resource = currentResource(); if (resource.isValid()) { const QString name = resource.displayName(); const QString id = resource.configName(); // resource name const QString calType = resource.storageTypeString(true); const CalEvent::Type alarmType = currentResourceType(); const QString storage = resource.storageTypeString(false); const QString location = resource.displayLocation(); const CalEvent::Types altypes = resource.alarmTypes(); QStringList alarmTypes; if (altypes & CalEvent::ACTIVE) alarmTypes << i18nc("@info", "Active alarms"); if (altypes & CalEvent::ARCHIVED) alarmTypes << i18nc("@info", "Archived alarms"); if (altypes & CalEvent::TEMPLATE) alarmTypes << i18nc("@info", "Alarm templates"); const QString alarmTypeString = alarmTypes.join(i18nc("@info List separator", ", ")); QString perms = ResourceDataModelBase::readOnlyTooltip(resource); if (perms.isEmpty()) perms = i18nc("@info", "Read-write"); const QString enabled = resource.isEnabled(alarmType) ? i18nc("@info", "Enabled") : i18nc("@info", "Disabled"); const QString std = Resources::isStandard(resource, alarmType) ? i18nc("@info Parameter in 'Default calendar: Yes/No'", "Yes") : i18nc("@info Parameter in 'Default calendar: Yes/No'", "No"); const QString text = xi18nc("@info", "%1" "ID: %2" "Calendar type: %3" "Contents: %4" "%5: %6" "Permissions: %7" "Status: %8" "Default calendar: %9", name, id, calType, alarmTypeString, storage, location, perms, enabled, std); // Display the resource information. Because the user requested // the information, don't raise a KNotify event. KAMessageBox::information(this, text, QString(), QString(), KMessageBox::Options()); } } /****************************************************************************** * Return the currently selected resource in the list. */ Resource ResourceSelector::currentResource() const { return mListView->resource(mListView->selectionModel()->currentIndex()); } /****************************************************************************** * Return the currently selected resource type. */ CalEvent::Type ResourceSelector::currentResourceType() const { switch (mAlarmType->currentIndex()) { case 0: return CalEvent::ACTIVE; case 1: return CalEvent::ARCHIVED; case 2: return CalEvent::TEMPLATE; default: return CalEvent::EMPTY; } } void ResourceSelector::resizeEvent(QResizeEvent* re) { Q_EMIT resized(re->oldSize(), re->size()); } // vim: et sw=4: diff --git a/src/templatedlg.cpp b/src/templatedlg.cpp index 8eb40829..6136e18b 100644 --- a/src/templatedlg.cpp +++ b/src/templatedlg.cpp @@ -1,243 +1,243 @@ /* * templatedlg.cpp - dialog to create, edit and delete alarm templates * Program: kalarm * Copyright © 2004-2019 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 "templatedlg.h" #include "alarmcalendar.h" #include "editdlg.h" #include "functions.h" #include "newalarmaction.h" #include "templatelistview.h" #include "undo.h" -#include "resources/akonadidatamodel.h" +#include "resources/datamodel.h" #include "resources/resources.h" #include "resources/eventmodel.h" #include "lib/config.h" #include "lib/messagebox.h" #include "lib/shellprocess.h" #include "kalarm_debug.h" #include #include #include #include #include #include #include #include #include #include #include using namespace KCal; static const char TMPL_DIALOG_NAME[] = "TemplateDialog"; TemplateDlg* TemplateDlg::mInstance = nullptr; TemplateDlg::TemplateDlg(QWidget* parent) : QDialog(parent) { setModal(false); setWindowTitle(i18nc("@title:window", "Alarm Templates")); QBoxLayout* topLayout = new QVBoxLayout(this); QBoxLayout* hlayout = new QHBoxLayout(this); topLayout->addLayout(hlayout); QBoxLayout* layout = new QVBoxLayout(); layout->setContentsMargins(0, 0, 0, 0); hlayout->addLayout(layout); - mListFilterModel = TemplateListModel::create(this); + mListFilterModel = DataModel::createTemplateListModel(this); if (!ShellProcess::authorised()) mListFilterModel->setAlarmActionFilter(static_cast(KAEvent::ACT_ALL & ~KAEvent::ACT_COMMAND)); mListView = new TemplateListView(this); mListView->setModel(mListFilterModel); mListView->sortByColumn(TemplateListModel::TemplateNameColumn, Qt::AscendingOrder); mListView->setSizePolicy(QSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding)); mListView->setWhatsThis(i18nc("@info:whatsthis", "The list of alarm templates")); mListView->setItemDelegate(new TemplateListDelegate(mListView)); connect(mListView->selectionModel(), &QItemSelectionModel::selectionChanged, this, &TemplateDlg::slotSelectionChanged); layout->addWidget(mListView); layout = new QVBoxLayout(); layout->setContentsMargins(0, 0, 0, 0); hlayout->addLayout(layout); QPushButton* button = new QPushButton(i18nc("@action:button", "New")); mNewAction = new NewAlarmAction(true, i18nc("@action", "New"), this); button->setMenu(mNewAction->menu()); connect(mNewAction, &NewAlarmAction::selected, this, &TemplateDlg::slotNew); button->setWhatsThis(i18nc("@info:whatsthis", "Create a new alarm template")); layout->addWidget(button); mEditButton = new QPushButton(i18nc("@action:button", "Edit...")); connect(mEditButton, &QPushButton::clicked, this, &TemplateDlg::slotEdit); mEditButton->setWhatsThis(i18nc("@info:whatsthis", "Edit the currently highlighted alarm template")); layout->addWidget(mEditButton); mCopyButton = new QPushButton(i18nc("@action:button", "Copy")); connect(mCopyButton, &QPushButton::clicked, this, &TemplateDlg::slotCopy); mCopyButton->setWhatsThis(i18nc("@info:whatsthis", "Create a new alarm template based on a copy of the currently highlighted template")); layout->addWidget(mCopyButton); mDeleteButton = new QPushButton(i18nc("@action:button", "Delete")); connect(mDeleteButton, &QPushButton::clicked, this, &TemplateDlg::slotDelete); mDeleteButton->setWhatsThis(i18nc("@info:whatsthis", "Delete the currently highlighted alarm template")); layout->addWidget(mDeleteButton); layout->addStretch(); topLayout->addWidget(new KSeparator(Qt::Horizontal, this)); QDialogButtonBox* buttonBox = new QDialogButtonBox(this); buttonBox->addButton(QDialogButtonBox::Close); connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::close); topLayout->addWidget(buttonBox); KActionCollection* actions = new KActionCollection(this); QAction* act = KStandardAction::selectAll(mListView, SLOT(selectAll()), actions); topLevelWidget()->addAction(act); act = KStandardAction::deselect(mListView, SLOT(clearSelection()), actions); topLevelWidget()->addAction(act); slotSelectionChanged(); // enable/disable buttons as appropriate QSize s; if (Config::readWindowSize(TMPL_DIALOG_NAME, s)) resize(s); } /****************************************************************************** * Destructor. */ TemplateDlg::~TemplateDlg() { mInstance = nullptr; } /****************************************************************************** * Create an instance, if none already exists. */ TemplateDlg* TemplateDlg::create(QWidget* parent) { if (mInstance) return nullptr; mInstance = new TemplateDlg(parent); return mInstance; } /****************************************************************************** * Called when the New Template button is clicked to create a new template. */ void TemplateDlg::slotNew(EditAlarmDlg::Type type) { KAlarm::editNewTemplate(type, mListView); } /****************************************************************************** * Called when the Copy button is clicked to edit a copy of an existing alarm, * to add to the list. */ void TemplateDlg::slotCopy() { KAEvent event = mListView->selectedEvent(); if (event.isValid()) KAlarm::editNewTemplate(&event, mListView); } /****************************************************************************** * Called when the Modify button is clicked to edit the currently highlighted * alarm in the list. */ void TemplateDlg::slotEdit() { KAEvent event = mListView->selectedEvent(); if (event.isValid()) KAlarm::editTemplate(&event, mListView); } /****************************************************************************** * Called when the Delete button is clicked to delete the currently highlighted * alarms in the list. */ void TemplateDlg::slotDelete() { QVector events = mListView->selectedEvents(); int n = events.count(); if (KAMessageBox::warningContinueCancel(this, i18ncp("@info", "Do you really want to delete the selected alarm template?", "Do you really want to delete the %1 selected alarm templates?", n), i18ncp("@title:window", "Delete Alarm Template", "Delete Alarm Templates", n), KGuiItem(i18nc("@action:button", "&Delete"), QStringLiteral("edit-delete"))) != KMessageBox::Continue) return; KAEvent::List delEvents; Undo::EventList undos; for (int i = 0; i < n; ++i) { KAEvent* event = &events[i]; delEvents.append(event); undos.append(*event, Resources::resourceForEvent(event->id())); } KAlarm::deleteTemplates(delEvents, this); Undo::saveDeletes(undos); } /****************************************************************************** * Called when the group of items selected changes. * Enable/disable the buttons depending on whether/how many templates are * currently highlighted. */ void TemplateDlg::slotSelectionChanged() { AlarmCalendar* resources = AlarmCalendar::resources(); QVector events = mListView->selectedEvents(); int count = events.count(); bool readOnly = false; for (int i = 0; i < count; ++i) { const KAEvent* event = &events[i]; if (resources->eventReadOnly(event->id())) { readOnly = true; break; } } mEditButton->setEnabled(count == 1); mCopyButton->setEnabled(count == 1); mDeleteButton->setEnabled(count && !readOnly); } /****************************************************************************** * Called when the dialog's size has changed. * Records the new size in the config file. */ void TemplateDlg::resizeEvent(QResizeEvent* re) { if (isVisible()) Config::writeWindowSize(TMPL_DIALOG_NAME, re->size()); QDialog::resizeEvent(re); } // vim: et sw=4: diff --git a/src/templatepickdlg.cpp b/src/templatepickdlg.cpp index 49780f56..0147df88 100644 --- a/src/templatepickdlg.cpp +++ b/src/templatepickdlg.cpp @@ -1,118 +1,118 @@ /* * templatepickdlg.cpp - dialog to choose an alarm template * Program: kalarm * Copyright © 2004-2019 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 "templatepickdlg.h" #include "functions.h" #include "templatelistview.h" -#include "resources/akonadidatamodel.h" +#include "resources/datamodel.h" #include "resources/eventmodel.h" #include "lib/config.h" #include "lib/shellprocess.h" #include "kalarm_debug.h" #include #include #include #include #include static const char TMPL_PICK_DIALOG_NAME[] = "TemplatePickDialog"; TemplatePickDlg::TemplatePickDlg(KAEvent::Actions type, QWidget* parent) : QDialog(parent) { QWidget* topWidget = new QWidget(this); QVBoxLayout* mainLayout = new QVBoxLayout; setLayout(mainLayout); mainLayout->addWidget(topWidget); setWindowTitle(i18nc("@title:window", "Choose Alarm Template")); QDialogButtonBox* buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok|QDialogButtonBox::Cancel); mainLayout->addWidget(buttonBox); mOkButton = buttonBox->button(QDialogButtonBox::Ok); mOkButton->setDefault(true); mOkButton->setShortcut(Qt::CTRL | Qt::Key_Return); connect(buttonBox, &QDialogButtonBox::accepted, this, &TemplatePickDlg::accept); connect(buttonBox, &QDialogButtonBox::rejected, this, &TemplatePickDlg::reject); QVBoxLayout* topLayout = new QVBoxLayout(topWidget); topLayout->setContentsMargins(0, 0, 0, 0); // Display the list of templates, but exclude command alarms if in kiosk mode. KAEvent::Actions shown = KAEvent::ACT_ALL; if (!ShellProcess::authorised()) { type = static_cast(type & ~KAEvent::ACT_COMMAND); shown = static_cast(shown & ~KAEvent::ACT_COMMAND); } - mListFilterModel = TemplateListModel::create(this); + mListFilterModel = DataModel::createTemplateListModel(this); mListFilterModel->setAlarmActionsEnabled(type); mListFilterModel->setAlarmActionFilter(shown); mListView = new TemplateListView(topWidget); mainLayout->addWidget(mListView); mListView->setModel(mListFilterModel); mListView->sortByColumn(TemplateListModel::TemplateNameColumn, Qt::AscendingOrder); mListView->setSelectionMode(QAbstractItemView::SingleSelection); mListView->setWhatsThis(i18nc("@info:whatsthis", "Select a template to base the new alarm on.")); connect(mListView->selectionModel(), &QItemSelectionModel::selectionChanged, this, &TemplatePickDlg::slotSelectionChanged); // Require a real double click (even if KDE is in single-click mode) to accept the selection connect(mListView, &TemplateListView::doubleClicked, this, &TemplatePickDlg::accept); topLayout->addWidget(mListView); slotSelectionChanged(); // enable or disable the OK button QSize s; if (Config::readWindowSize(TMPL_PICK_DIALOG_NAME, s)) resize(s); } /****************************************************************************** * Return the currently selected alarm template, or 0 if none. */ KAEvent TemplatePickDlg::selectedTemplate() const { return mListView->selectedEvent(); } /****************************************************************************** * Called when the template selection changes. * Enable/disable the OK button depending on whether anything is selected. */ void TemplatePickDlg::slotSelectionChanged() { bool enable = !mListView->selectionModel()->selectedRows().isEmpty(); if (enable) enable = mListView->model()->flags(mListView->selectedIndex()) & Qt::ItemIsEnabled; mOkButton->setEnabled(enable); } /****************************************************************************** * Called when the dialog's size has changed. * Records the new size in the config file. */ void TemplatePickDlg::resizeEvent(QResizeEvent* re) { if (isVisible()) Config::writeWindowSize(TMPL_PICK_DIALOG_NAME, re->size()); QDialog::resizeEvent(re); } // vim: et sw=4: diff --git a/src/traywindow.cpp b/src/traywindow.cpp index 999fd8ba..000f836f 100644 --- a/src/traywindow.cpp +++ b/src/traywindow.cpp @@ -1,428 +1,428 @@ /* * traywindow.cpp - the KDE system tray applet * Program: kalarm * Copyright © 2002-2019 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 "traywindow.h" #include "alarmcalendar.h" #include "functions.h" #include "kalarmapp.h" #include "mainwindow.h" #include "messagewin.h" #include "newalarmaction.h" #include "prefdlg.h" #include "preferences.h" #include "templatemenuaction.h" -#include "resources/akonadidatamodel.h" +#include "resources/datamodel.h" #include "resources/eventmodel.h" #include "lib/synchtimer.h" #include "kalarm_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace KAlarmCal; struct TipItem { QDateTime dateTime; QString text; }; /*============================================================================= = Class: TrayWindow = The KDE system tray window. =============================================================================*/ TrayWindow::TrayWindow(MainWindow* parent) : KStatusNotifierItem(parent) , mAssocMainWindow(parent) , mStatusUpdateTimer(new QTimer(this)) { qCDebug(KALARM_LOG) << "TrayWindow:"; setToolTipIconByName(QStringLiteral("kalarm")); setToolTipTitle(KAboutData::applicationData().displayName()); setIconByName(QStringLiteral("kalarm")); setStatus(KStatusNotifierItem::Active); // Set up the context menu mActionEnabled = KAlarm::createAlarmEnableAction(this); addAction(QStringLiteral("tAlarmsEnable"), mActionEnabled); contextMenu()->addAction(mActionEnabled); connect(theApp(), &KAlarmApp::alarmEnabledToggled, this, &TrayWindow::setEnabledStatus); contextMenu()->addSeparator(); mActionNew = new NewAlarmAction(false, i18nc("@action", "&New Alarm"), this); addAction(QStringLiteral("tNew"), mActionNew); contextMenu()->addAction(mActionNew); connect(mActionNew, &NewAlarmAction::selected, this, &TrayWindow::slotNewAlarm); connect(mActionNew->fromTemplateAlarmAction(QString()), &TemplateMenuAction::selected, this, &TrayWindow::slotNewFromTemplate); contextMenu()->addSeparator(); QAction* a = KAlarm::createStopPlayAction(this); addAction(QStringLiteral("tStopPlay"), a); contextMenu()->addAction(a); QObject::connect(theApp(), &KAlarmApp::audioPlaying, a, &QAction::setVisible); QObject::connect(theApp(), &KAlarmApp::audioPlaying, this, &TrayWindow::updateStatus); a = KAlarm::createSpreadWindowsAction(this); addAction(QStringLiteral("tSpread"), a); contextMenu()->addAction(a); contextMenu()->addSeparator(); contextMenu()->addAction(KStandardAction::preferences(this, SLOT(slotPreferences()), this)); // Disable standard quit behaviour. We have to intercept the quit even, // if the main window is hidden. QAction* act = action(QStringLiteral("quit")); if (act) { act->disconnect(SIGNAL(triggered(bool)), this, SLOT(maybeQuit())); connect(act, &QAction::triggered, this, &TrayWindow::slotQuit); } // Set icon to correspond with the alarms enabled menu status setEnabledStatus(theApp()->alarmsEnabled()); connect(AlarmCalendar::resources(), &AlarmCalendar::haveDisabledAlarmsChanged, this, &TrayWindow::slotHaveDisabledAlarms); connect(this, &TrayWindow::activateRequested, this, &TrayWindow::slotActivateRequested); connect(this, &TrayWindow::secondaryActivateRequested, this, &TrayWindow::slotSecondaryActivateRequested); slotHaveDisabledAlarms(AlarmCalendar::resources()->haveDisabledAlarms()); // Hack: KSNI does not let us know when it is about to show the tooltip, // so we need to update it whenever something change in it. // This timer ensures that updateToolTip() is not called several times in a row mToolTipUpdateTimer = new QTimer(this); mToolTipUpdateTimer->setInterval(0); mToolTipUpdateTimer->setSingleShot(true); connect(mToolTipUpdateTimer, &QTimer::timeout, this, &TrayWindow::updateToolTip); // Update every minute to show accurate deadlines MinuteTimer::connect(mToolTipUpdateTimer, SLOT(start())); // Update when alarms are modified - AlarmListModel* all = AlarmListModel::all(); + AlarmListModel* all = DataModel::allAlarmListModel(); connect(all, SIGNAL(dataChanged(QModelIndex,QModelIndex)), mToolTipUpdateTimer, SLOT(start())); connect(all, SIGNAL(rowsInserted(QModelIndex,int,int)), mToolTipUpdateTimer, SLOT(start())); connect(all, SIGNAL(rowsMoved(QModelIndex,int,int,QModelIndex,int)), mToolTipUpdateTimer, SLOT(start())); connect(all, SIGNAL(rowsRemoved(QModelIndex,int,int)), mToolTipUpdateTimer, SLOT(start())); connect(all, SIGNAL(modelReset()), mToolTipUpdateTimer, SLOT(start())); // Set auto-hide status when next alarm or preferences change mStatusUpdateTimer->setSingleShot(true); connect(mStatusUpdateTimer, &QTimer::timeout, this, &TrayWindow::updateStatus); connect(AlarmCalendar::resources(), &AlarmCalendar::earliestAlarmChanged, this, &TrayWindow::updateStatus); Preferences::connect(SIGNAL(autoHideSystemTrayChanged(int)), this, SLOT(updateStatus())); updateStatus(); // Update when tooltip preferences are modified Preferences::connect(SIGNAL(tooltipPreferencesChanged()), mToolTipUpdateTimer, SLOT(start())); } TrayWindow::~TrayWindow() { qCDebug(KALARM_LOG) << "~TrayWindow"; theApp()->removeWindow(this); Q_EMIT deleted(); } /****************************************************************************** * Called when the "New Alarm" menu item is selected to edit a new alarm. */ void TrayWindow::slotNewAlarm(EditAlarmDlg::Type type) { KAlarm::editNewAlarm(type); } /****************************************************************************** * Called when the "New Alarm" menu item is selected to edit a new alarm from a * template. */ void TrayWindow::slotNewFromTemplate(const KAEvent* event) { KAlarm::editNewAlarm(event); } /****************************************************************************** * Called when the "Configure KAlarm" menu item is selected. */ void TrayWindow::slotPreferences() { KAlarmPrefDlg::display(); } /****************************************************************************** * Called when the Quit context menu item is selected. * Note that KAlarmApp::doQuit() must be called by the event loop, not directly * from the menu item, since otherwise the tray icon will be deleted while still * processing the menu, resulting in a crash. * Ideally, the connect() call setting up this slot in the constructor would use * Qt::QueuedConnection, but the slot is never called in that case. */ void TrayWindow::slotQuit() { // Note: QTimer::singleShot(0, ...) never calls the slot. QTimer::singleShot(1, this, &TrayWindow::slotQuitAfter); } void TrayWindow::slotQuitAfter() { theApp()->doQuit(static_cast(parent())); } /****************************************************************************** * Called when the Alarms Enabled action status has changed. * Updates the alarms enabled menu item check state, and the icon pixmap. */ void TrayWindow::setEnabledStatus(bool status) { qCDebug(KALARM_LOG) << "TrayWindow::setEnabledStatus:" << status; updateIcon(); updateStatus(); updateToolTip(); } /****************************************************************************** * Called when individual alarms are enabled or disabled. * Set the enabled icon to show or hide a disabled indication. */ void TrayWindow::slotHaveDisabledAlarms(bool haveDisabled) { qCDebug(KALARM_LOG) << "TrayWindow::slotHaveDisabledAlarms:" << haveDisabled; mHaveDisabledAlarms = haveDisabled; updateIcon(); updateToolTip(); } /****************************************************************************** * Show the associated main window. */ void TrayWindow::showAssocMainWindow() { if (mAssocMainWindow) { mAssocMainWindow->show(); mAssocMainWindow->raise(); mAssocMainWindow->activateWindow(); } } /****************************************************************************** * A left click displays the KAlarm main window. */ void TrayWindow::slotActivateRequested() { // Left click: display/hide the first main window if (mAssocMainWindow && mAssocMainWindow->isVisible()) { mAssocMainWindow->raise(); mAssocMainWindow->activateWindow(); } } /****************************************************************************** * A middle button click displays the New Alarm window. */ void TrayWindow::slotSecondaryActivateRequested() { if (mActionNew->isEnabled()) mActionNew->trigger(); // display a New Alarm dialog } /****************************************************************************** * Adjust icon auto-hide status according to when the next alarm is due. * The icon is always shown if audio is playing, to give access to the 'stop' * menu option. */ void TrayWindow::updateStatus() { mStatusUpdateTimer->stop(); int period = Preferences::autoHideSystemTray(); // If the icon is always to be shown (AutoHideSystemTray = 0), // or audio is playing, show the icon. bool active = !period || MessageWin::isAudioPlaying(); if (!active) { // Show the icon only if the next active alarm complies active = theApp()->alarmsEnabled(); if (active) { KAEvent* event = AlarmCalendar::resources()->earliestAlarm(); active = static_cast(event); if (event && period > 0) { const KADateTime dt = event->nextTrigger(KAEvent::ALL_TRIGGER).effectiveKDateTime(); qint64 delay = KADateTime::currentLocalDateTime().secsTo(dt); delay -= static_cast(period) * 60; // delay until icon to be shown active = (delay <= 0); if (!active) { // First alarm trigger is too far in future, so tray icon is to // be auto-hidden. Set timer for when it should be shown again. delay *= 1000; // convert to msec int delay_int = static_cast(delay); if (delay_int != delay) delay_int = INT_MAX; mStatusUpdateTimer->setInterval(delay_int); mStatusUpdateTimer->start(); } } } } setStatus(active ? Active : Passive); } /****************************************************************************** * Adjust tooltip according to the app state. * The tooltip text shows alarms due in the next 24 hours. The limit of 24 * hours is because only times, not dates, are displayed. */ void TrayWindow::updateToolTip() { bool enabled = theApp()->alarmsEnabled(); QString subTitle; if (enabled && Preferences::tooltipAlarmCount()) subTitle = tooltipAlarmText(); if (!enabled) subTitle = i18n("Disabled"); else if (mHaveDisabledAlarms) { if (!subTitle.isEmpty()) subTitle += QLatin1String("
"); subTitle += i18nc("@info:tooltip Brief: some alarms are disabled", "(Some alarms disabled)"); } setToolTipSubTitle(subTitle); } /****************************************************************************** * Adjust icon according to the app state. */ void TrayWindow::updateIcon() { setIconByName(!theApp()->alarmsEnabled() ? QStringLiteral("kalarm-disabled") : mHaveDisabledAlarms ? QStringLiteral("kalarm-partdisabled") : QStringLiteral("kalarm")); } /****************************************************************************** * Return the tooltip text showing alarms due in the next 24 hours. * The limit of 24 hours is because only times, not dates, are displayed. */ QString TrayWindow::tooltipAlarmText() const { KAEvent event; const QString& prefix = Preferences::tooltipTimeToPrefix(); int maxCount = Preferences::tooltipAlarmCount(); const KADateTime now = KADateTime::currentLocalDateTime(); const KADateTime tomorrow = now.addDays(1); // Get today's and tomorrow's alarms, sorted in time order int i, iend; QList items; QVector events = KAlarm::getSortedActiveEvents(const_cast(this), &mAlarmsModel); for (i = 0, iend = events.count(); i < iend; ++i) { KAEvent* event = &events[i]; if (event->actionSubType() == KAEvent::MESSAGE) { TipItem item; QDateTime dateTime = event->nextTrigger(KAEvent::DISPLAY_TRIGGER).effectiveKDateTime().toLocalZone().qDateTime(); if (dateTime > tomorrow.qDateTime()) break; // ignore alarms after tomorrow at the current clock time item.dateTime = dateTime; // The alarm is due today, or early tomorrow if (Preferences::showTooltipAlarmTime()) { item.text += QLocale().toString(item.dateTime.time(), QLocale::ShortFormat); item.text += QLatin1Char(' '); } if (Preferences::showTooltipTimeToAlarm()) { int mins = (now.qDateTime().secsTo(item.dateTime) + 59) / 60; if (mins < 0) mins = 0; char minutes[3] = "00"; minutes[0] = static_cast((mins%60) / 10 + '0'); minutes[1] = static_cast((mins%60) % 10 + '0'); if (Preferences::showTooltipAlarmTime()) item.text += i18nc("@info prefix + hours:minutes", "(%1%2:%3)", prefix, mins/60, QLatin1String(minutes)); else item.text += i18nc("@info prefix + hours:minutes", "%1%2:%3", prefix, mins/60, QLatin1String(minutes)); item.text += QLatin1Char(' '); } item.text += AlarmText::summary(*event); // Insert the item into the list in time-sorted order int it = 0; for (int itend = items.count(); it < itend; ++it) { if (item.dateTime <= items[it].dateTime) break; } items.insert(it, item); } } qCDebug(KALARM_LOG) << "TrayWindow::tooltipAlarmText"; QString text; int count = 0; for (i = 0, iend = items.count(); i < iend; ++i) { qCDebug(KALARM_LOG) << "TrayWindow::tooltipAlarmText: --" << (count+1) << ")" << items[i].text; if (i > 0) text += QLatin1String("
"); text += items[i].text; if (++count == maxCount) break; } return text; } /****************************************************************************** * Called when the associated main window is closed. */ void TrayWindow::removeWindow(MainWindow* win) { if (win == mAssocMainWindow) mAssocMainWindow = nullptr; } // vim: et sw=4: