diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 5a8b8b59..b43d7559 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,203 +1,204 @@ include_directories( ${CMAKE_CURRENT_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/ ) add_subdirectory(appicons) add_subdirectory(pixmaps) add_subdirectory(autostart) add_subdirectory(kconf_update) set(libkalarm_common_SRCS) ecm_qt_declare_logging_category(libkalarm_common_SRCS HEADER kalarm_debug.h IDENTIFIER KALARM_LOG CATEGORY_NAME org.kde.pim.kalarm DEFAULT_SEVERITY Warning DESCRIPTION "kalarm (kalarm)" OLD_CATEGORY_NAMES log_kalarm EXPORT KALARM ) ########### next target ############### set(libkalarm_SRCS lib/buttongroup.cpp lib/checkbox.cpp lib/colourbutton.cpp lib/combobox.cpp lib/config.cpp lib/desktop.cpp lib/file.cpp lib/filedialog.cpp lib/groupbox.cpp lib/label.cpp lib/locale.cpp lib/messagebox.cpp lib/packedlayout.cpp lib/pushbutton.cpp lib/radiobutton.cpp lib/timeedit.cpp lib/timespinbox.cpp lib/timeperiod.cpp lib/timezonecombo.cpp lib/shellprocess.cpp lib/slider.cpp lib/spinbox.cpp lib/spinbox2.cpp lib/stackedwidgets.cpp lib/lineedit.cpp lib/synchtimer.cpp ) set(kalarm_bin_SRCS ${libkalarm_SRCS} ${libkalarm_common_SRCS} birthdaydlg.cpp birthdaymodel.cpp main.cpp editdlg.cpp editdlgtypes.cpp soundpicker.cpp sounddlg.cpp alarmcalendar.cpp undo.cpp kalarmapp.cpp mainwindowbase.cpp mainwindow.cpp messagewin.cpp preferences.cpp prefdlg.cpp traywindow.cpp dbushandler.cpp recurrenceedit.cpp deferdlg.cpp functions.cpp fontcolour.cpp fontcolourbutton.cpp alarmtime.cpp alarmtimewidget.cpp specialactions.cpp reminder.cpp startdaytimer.cpp eventlistview.cpp alarmlistdelegate.cpp alarmlistview.cpp templatelistview.cpp kamail.cpp timeselector.cpp latecancel.cpp repetitionbutton.cpp emailidcombo.cpp find.cpp pickfileradio.cpp newalarmaction.cpp commandoptions.cpp resourceselector.cpp templatepickdlg.cpp templatedlg.cpp templatemenuaction.cpp wakedlg.cpp ) set(kalarm_bin_SRCS ${kalarm_bin_SRCS} + resources/calendarfunctions.cpp resources/resourcetype.cpp resources/resource.cpp resources/resources.cpp resources/resourcedatamodelbase.cpp resources/resourcemodel.cpp resources/resourceselectdialog.cpp resources/resourcecreator.cpp resources/eventmodel.cpp resources/datamodel.cpp resources/akonadidatamodel.cpp resources/akonadiresource.cpp resources/akonadiresourcecreator.cpp resources/akonadiresourcemigrator.cpp resources/calendarupdater.cpp resources/akonadicalendarupdater.cpp kalarmmigrateapplication.cpp akonadicollectionsearch.cpp eventid.cpp ) ki18n_wrap_ui(kalarm_bin_SRCS wakedlg.ui ) qt5_add_dbus_adaptor(kalarm_bin_SRCS org.kde.kalarm.kalarm.xml dbushandler.h DBusHandler) qt5_add_dbus_interfaces(kalarm_bin_SRCS kmail/org.kde.kmail.kmail.xml) kcfg_generate_dbus_interface(${CMAKE_CURRENT_SOURCE_DIR}/kalarmresource.kcfg org.kde.Akonadi.KAlarm.Settings) qt5_add_dbus_interface(kalarm_bin_SRCS ${CMAKE_CURRENT_BINARY_DIR}/org.kde.Akonadi.KAlarm.Settings.xml kalarmsettings KAlarmSettings) kcfg_generate_dbus_interface(${CMAKE_CURRENT_SOURCE_DIR}/kalarmdirresource.kcfg org.kde.Akonadi.KAlarmDir.Settings) qt5_add_dbus_interface(kalarm_bin_SRCS ${CMAKE_CURRENT_BINARY_DIR}/org.kde.Akonadi.KAlarmDir.Settings.xml kalarmdirsettings KAlarmDirSettings) kconfig_add_kcfg_files(kalarm_bin_SRCS GENERATE_MOC kalarmconfig.kcfgc) #if (UNIX) file(GLOB ICONS_SRCS "${CMAKE_CURRENT_SOURCE_DIR}/appicons/*-apps-kalarm.png") ecm_add_app_icon(kalarm_bin_SRCS ICONS ${ICONS_SRCS}) add_executable(kalarm_bin ${kalarm_bin_SRCS}) set_target_properties(kalarm_bin PROPERTIES OUTPUT_NAME kalarm) target_compile_definitions(kalarm_bin PRIVATE -DVERSION="${KALARM_VERSION}") target_link_libraries(kalarm_bin KF5::Codecs KF5::ConfigCore KF5::Completion KF5::DBusAddons KF5::GlobalAccel KF5::GuiAddons KF5::Holidays KF5::KIOWidgets KF5::Notifications KF5::TextWidgets KF5::WindowSystem KF5::XmlGui KF5::KIOFileWidgets KF5::Crash Phonon::phonon4qt5 KF5::AkonadiCore KF5::AkonadiMime KF5::AkonadiContact KF5::AkonadiWidgets KF5::AlarmCalendar KF5::CalendarCore KF5::CalendarUtils KF5::Contacts KF5::IdentityManagement KF5::Libkdepim KF5::MailTransportAkonadi KF5::Mime KF5::PimCommon ) if (Qt5X11Extras_FOUND) target_link_libraries(kalarm_bin Qt5::X11Extras ${X11_X11_LIB}) endif() install(TARGETS kalarm_bin ${KDE_INSTALL_TARGETS_DEFAULT_ARGS}) #endif (UNIX) ########### install files ############### install(FILES org.kde.kalarm.desktop DESTINATION ${KDE_INSTALL_APPDIR}) install(FILES kalarm.autostart.desktop DESTINATION ${KDE_INSTALL_AUTOSTARTDIR}) install(FILES org.kde.kalarm.appdata.xml DESTINATION ${KDE_INSTALL_METAINFODIR}) install(FILES kalarmconfig.kcfg DESTINATION ${KDE_INSTALL_KCFGDIR}) install(FILES kalarmui.rc DESTINATION ${KDE_INSTALL_KXMLGUI5DIR}/kalarm) install(FILES org.kde.kalarm.kalarm.xml DESTINATION ${KDE_INSTALL_DBUSINTERFACEDIR}) ########### KAuth helper ############### add_executable(kalarm_helper rtcwakeaction.cpp ${libkalarm_common_SRCS}) target_link_libraries(kalarm_helper KF5::AuthCore KF5::I18n) install(TARGETS kalarm_helper DESTINATION ${KAUTH_HELPER_INSTALL_DIR}) kauth_install_helper_files(kalarm_helper org.kde.kalarm.rtcwake root) kauth_install_actions(org.kde.kalarm.rtcwake org.kde.kalarm.rtcwake.actions) diff --git a/src/alarmcalendar.cpp b/src/alarmcalendar.cpp index 920df0a3..5ebcab8b 100644 --- a/src/alarmcalendar.cpp +++ b/src/alarmcalendar.cpp @@ -1,1325 +1,1323 @@ /* * alarmcalendar.cpp - KAlarm calendar file access * Program: kalarm * Copyright © 2001-2020 David Jarvie * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "alarmcalendar.h" #include "kalarm.h" #include "functions.h" #include "kalarmapp.h" #include "mainwindow.h" #include "preferences.h" #include "resources/datamodel.h" #include "resources/resources.h" #include "lib/messagebox.h" #include "kalarm_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include using namespace KCalendarCore; using namespace KAlarmCal; static const QString displayCalendarName = QStringLiteral("displaying.ics"); static const ResourceId DISPLAY_RES_ID = -1; // resource ID used for displaying calendar ResourcesCalendar* ResourcesCalendar::mInstance = nullptr; DisplayCalendar* DisplayCalendar::mInstance = nullptr; /****************************************************************************** * Initialise backend calendar access. */ void AlarmCalendar::initialise() { - Preferences::setBackend(Preferences::Akonadi); - Preferences::self()->save(); KACalendar::setProductId(KALARM_NAME, KALARM_VERSION); CalFormat::setApplication(QStringLiteral(KALARM_NAME), QString::fromLatin1(KACalendar::icalProductId())); } /****************************************************************************** * Initialise the resource alarm calendars, and ensure that their file names are * different. The resources calendar contains the active alarms, archived alarms * and alarm templates; */ void ResourcesCalendar::initialise() { AlarmCalendar::initialise(); mInstance = new ResourcesCalendar(); } /****************************************************************************** * Initialise the display alarm calendar. * It is user-specific, and contains details of alarms which are currently being * displayed to that user and which have not yet been acknowledged; */ void DisplayCalendar::initialise() { QDir dir; dir.mkpath(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation)); const QString displayCal = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QLatin1Char('/') + displayCalendarName; mInstance = new DisplayCalendar(displayCal); } /****************************************************************************** * Terminate access to the resource calendars. */ void ResourcesCalendar::terminate() { delete mInstance; mInstance = nullptr; } /****************************************************************************** * Terminate access to the display calendar. */ void DisplayCalendar::terminate() { delete mInstance; mInstance = nullptr; } /****************************************************************************** * Return the display calendar, opening it first if necessary. */ DisplayCalendar* DisplayCalendar::instanceOpen() { if (mInstance->open()) return mInstance; qCCritical(KALARM_LOG) << "DisplayCalendar::instanceOpen: Open error"; return nullptr; } /****************************************************************************** * Find and return the event with the specified ID. * The calendar searched is determined by the calendar identifier in the ID. */ KAEvent* ResourcesCalendar::getEvent(const EventId& eventId) { if (eventId.eventId().isEmpty()) return nullptr; return mInstance->event(eventId); } /****************************************************************************** * Constructor for the resources calendar. */ AlarmCalendar::AlarmCalendar() { } /****************************************************************************** * Constructor for the resources calendar. */ ResourcesCalendar::ResourcesCalendar() : AlarmCalendar() { Resources* resources = Resources::instance(); connect(resources, &Resources::resourceAdded, this, &ResourcesCalendar::slotResourceAdded); connect(resources, &Resources::eventsAdded, this, &ResourcesCalendar::slotEventsAdded); connect(resources, &Resources::eventsToBeRemoved, this, &ResourcesCalendar::slotEventsToBeRemoved); connect(resources, &Resources::eventUpdated, this, &ResourcesCalendar::slotEventUpdated); connect(resources, &Resources::resourcesPopulated, this, &ResourcesCalendar::slotResourcesPopulated); connect(resources, &Resources::settingsChanged, this, &ResourcesCalendar::slotResourceSettingsChanged); // Fetch events from all resources which already exist. QVector allResources = Resources::enabledResources(); for (Resource& resource : allResources) slotResourceAdded(resource); } /****************************************************************************** * Constructor for the display calendar file. */ DisplayCalendar::DisplayCalendar(const QString& path) : AlarmCalendar() , mDisplayCalPath(path) , mDisplayICalPath(path) { mDisplayICalPath.replace(QStringLiteral("\\.vcs$"), QStringLiteral(".ics")); mCalType = (mDisplayCalPath == mDisplayICalPath) ? LOCAL_ICAL : LOCAL_VCAL; // is the calendar in ICal or VCal format? } ResourcesCalendar::~ResourcesCalendar() { close(); } DisplayCalendar::~DisplayCalendar() { close(); } /****************************************************************************** * Open the calendar if not already open, and load it into memory. */ bool DisplayCalendar::open() { if (isOpen()) return true; // Open the display calendar. qCDebug(KALARM_LOG) << "DisplayCalendar::open:" << mDisplayCalPath; if (!mCalendarStorage) { MemoryCalendar::Ptr calendar(new MemoryCalendar(Preferences::timeSpecAsZone())); mCalendarStorage = FileStorage::Ptr(new FileStorage(calendar, mDisplayCalPath)); } // Check for file's existence, assuming that it does exist when uncertain, // to avoid overwriting it. QFileInfo fi(mDisplayCalPath); if (!fi.exists() || !fi.isFile() || load() == 0) { // The calendar file doesn't yet exist, or it's zero length, so create a new one if (saveCal(mDisplayICalPath)) load(); } if (!mOpen) { mCalendarStorage->calendar().clear(); mCalendarStorage.clear(); } return isOpen(); } /****************************************************************************** * Load the calendar into memory. * Reply = 1 if success * = 0 if zero-length file exists. * = -1 if failure to load calendar file * = -2 if instance uninitialised. */ int DisplayCalendar::load() { // Load the display calendar. if (!mCalendarStorage) return -2; qCDebug(KALARM_LOG) << "DisplayCalendar::load:" << mDisplayCalPath; if (!mCalendarStorage->load()) { // Load error. Check if the file is zero length QFileInfo fi(mDisplayCalPath); if (fi.exists() && !fi.size()) return 0; // file is zero length qCCritical(KALARM_LOG) << "DisplayCalendar::load: Error loading calendar file '" << mDisplayCalPath <<"'"; KAMessageBox::error(MainWindow::mainMainWindow(), xi18nc("@info", "Error loading calendar:%1Please fix or delete the file.", mDisplayCalPath)); // load() could have partially populated the calendar, so clear it out mCalendarStorage->calendar()->close(); mCalendarStorage->calendar().clear(); mCalendarStorage.clear(); mOpen = false; return -1; } QString versionString; KACalendar::updateVersion(mCalendarStorage, versionString); // convert events to current KAlarm format for when calendar is saved updateKAEvents(); mOpen = true; return 1; } /****************************************************************************** * Reload the calendar resources into memory. */ bool ResourcesCalendar::reload() { qCDebug(KALARM_LOG) << "ResourcesCalendar::reload"; bool ok = true; QVector resources = Resources::enabledResources(); for (Resource& resource : resources) ok = ok && resource.reload(); return ok; } /****************************************************************************** * Reload the calendar file into memory. */ bool DisplayCalendar::reload() { if (!mCalendarStorage) return false; qCDebug(KALARM_LOG) << "DisplayCalendar::reload:" << mDisplayCalPath; close(); return open(); } /****************************************************************************** * Save the calendar. */ bool ResourcesCalendar::save() { bool ok = true; // Get all enabled, writable resources. QVector resources = Resources::enabledResources(CalEvent::EMPTY, true); for (Resource& resource : resources) ok = ok && resource.save(); return ok; } /****************************************************************************** * Save the calendar. */ bool DisplayCalendar::save() { return saveCal(); } /****************************************************************************** * Save the calendar from memory to file. * If a filename is specified, create a new calendar file. */ bool DisplayCalendar::saveCal(const QString& newFile) { if (!mCalendarStorage) return false; if (!mOpen && newFile.isEmpty()) return false; qCDebug(KALARM_LOG) << "DisplayCalendar::saveCal:" << "\"" << newFile; QString saveFilename = newFile.isEmpty() ? mDisplayCalPath : newFile; if (mCalType == LOCAL_VCAL && newFile.isNull()) saveFilename = mDisplayICalPath; mCalendarStorage->setFileName(saveFilename); mCalendarStorage->setSaveFormat(new ICalFormat); if (!mCalendarStorage->save()) { qCCritical(KALARM_LOG) << "DisplayCalendar::saveCal: Saving" << saveFilename << "failed."; KAMessageBox::error(MainWindow::mainMainWindow(), xi18nc("@info", "Failed to save calendar to %1", mDisplayICalPath)); return false; } if (mCalType == LOCAL_VCAL) { // The file was in vCalendar format, but has now been saved in iCalendar format. mDisplayCalPath = mDisplayICalPath; mCalType = LOCAL_ICAL; } return true; } /****************************************************************************** * Close display calendar file at program exit. */ void ResourcesCalendar::close() { // Resource map should be empty, but just in case... while (!mResourceMap.isEmpty()) removeKAEvents(mResourceMap.begin().key(), true, CalEvent::ACTIVE | CalEvent::ARCHIVED | CalEvent::TEMPLATE | CalEvent::DISPLAYING); } /****************************************************************************** * Close display calendar file at program exit. */ void DisplayCalendar::close() { if (mCalendarStorage) { mCalendarStorage->calendar()->close(); mCalendarStorage->calendar().clear(); mCalendarStorage.clear(); } // Flag as closed now to prevent removeKAEvents() doing silly things // when it's called again mOpen = false; // Resource map should be empty, but just in case... while (!mResourceMap.isEmpty()) removeKAEvents(mResourceMap.begin().key(), CalEvent::ACTIVE | CalEvent::ARCHIVED | CalEvent::TEMPLATE | CalEvent::DISPLAYING); } /****************************************************************************** * Create a KAEvent instance corresponding to each KCalendarCore::Event in the * display calendar, and store them in the event map in place of the old set. * Called after the display calendar has completed loading. */ void DisplayCalendar::updateKAEvents() { qCDebug(KALARM_LOG) << "DisplayCalendar::updateKAEvents"; const ResourceId key = DISPLAY_RES_ID; KAEvent::List& events = mResourceMap[key]; for (KAEvent* event : events) { mEventMap.remove(EventId(key, event->id())); delete event; } events.clear(); Calendar::Ptr cal = mCalendarStorage->calendar(); if (!cal) return; const Event::List kcalevents = cal->rawEvents(); for (Event::Ptr kcalevent : kcalevents) { if (kcalevent->alarms().isEmpty()) continue; // ignore events without alarms KAEvent* event = new KAEvent(kcalevent); if (!event->isValid()) { qCWarning(KALARM_LOG) << "DisplayCalendar::updateKAEvents: Ignoring unusable event" << kcalevent->uid(); delete event; continue; // ignore events without usable alarms } event->setResourceId(key); events += event; mEventMap[EventId(key, kcalevent->uid())] = event; } } /****************************************************************************** * Delete a calendar and all its KAEvent instances of specified alarm types from * the lists. * Called after the calendar is deleted or alarm types have been disabled, or * the AlarmCalendar is closed. */ bool AlarmCalendar::removeKAEvents(ResourceId key, CalEvent::Types types) { bool removed = false; ResourceMap::Iterator rit = mResourceMap.find(key); if (rit != mResourceMap.end()) { KAEvent::List retained; KAEvent::List& events = rit.value(); for (int i = 0, end = events.count(); i < end; ++i) { KAEvent* event = events[i]; bool remove = (event->resourceId() != key); if (remove) { if (key != DISPLAY_RES_ID) qCCritical(KALARM_LOG) << "AlarmCalendar::removeKAEvents: Event" << event->id() << ", resource" << event->resourceId() << "Indexed under resource" << key; } else remove = event->category() & types; if (remove) { mEventMap.remove(EventId(key, event->id())); delete event; removed = true; } else retained.push_back(event); } if (retained.empty()) mResourceMap.erase(rit); else events.swap(retained); } return removed; } bool ResourcesCalendar::removeKAEvents(ResourceId key, bool closing, CalEvent::Types types) { if (!AlarmCalendar::removeKAEvents(key, types)) return false; mEarliestAlarm.remove(key); // Emit signal only if we're not in the process of closing the calendar if (!closing) { Q_EMIT earliestAlarmChanged(); if (mHaveDisabledAlarms) checkForDisabledAlarms(); } return true; } /****************************************************************************** * Called when the enabled or read-only status of a resource has changed. * If the resource is now disabled, remove its events from the calendar. */ void ResourcesCalendar::slotResourceSettingsChanged(Resource& resource, ResourceType::Changes change) { if (change & ResourceType::Enabled) { if (resource.isValid()) { // For each alarm type which has been disabled, remove the // resource's events from the map, but not from the resource. const CalEvent::Types enabled = resource.enabledTypes(); const CalEvent::Types disabled = ~enabled & (CalEvent::ACTIVE | CalEvent::ARCHIVED | CalEvent::TEMPLATE); removeKAEvents(resource.id(), false, disabled); // For each alarm type which has been enabled, add the resource's // events to the map. if (enabled != CalEvent::EMPTY) slotEventsAdded(resource, resource.events()); } } } /****************************************************************************** * Called when all resources have been populated for the first time. */ void ResourcesCalendar::slotResourcesPopulated() { // Now that all calendars have been processed, all repeat-at-login alarms // will have been triggered. Prevent any new or updated repeat-at-login // alarms (e.g. when they are edited by the user) triggering from now on. mIgnoreAtLogin = true; } /****************************************************************************** * Called when a resource has been added. * Add its KAEvent instances to those held by AlarmCalendar. * All events must have their resource ID set. */ void ResourcesCalendar::slotResourceAdded(Resource& resource) { slotEventsAdded(resource, resource.events()); } /****************************************************************************** * Called when events have been added to a resource. * Add corresponding KAEvent instances to those held by AlarmCalendar. * All events must have their resource ID set. */ void ResourcesCalendar::slotEventsAdded(Resource& resource, const QList& events) { for (const KAEvent& event : events) slotEventUpdated(resource, event); } /****************************************************************************** * Called when an event has been changed in a resource. * Change the corresponding KAEvent instance held by AlarmCalendar. * The event must have its resource ID set. */ void ResourcesCalendar::slotEventUpdated(Resource& resource, const KAEvent& event) { bool added = true; bool updated = false; KAEventMap::Iterator it = mEventMap.find(EventId(event)); if (it != mEventMap.end()) { // The event ID already exists - remove the existing event first KAEvent* storedEvent = it.value(); if (event.category() == storedEvent->category()) { // The existing event is the same type - update it in place *storedEvent = event; addNewEvent(resource, storedEvent, true); updated = true; } else delete storedEvent; added = false; } if (!updated) addNewEvent(resource, new KAEvent(event)); if (event.category() == CalEvent::ACTIVE) { bool enabled = event.enabled(); checkForDisabledAlarms(!enabled, enabled); if (!mIgnoreAtLogin && added && enabled && event.repeatAtLogin()) Q_EMIT atLoginEventAdded(event); } } /****************************************************************************** * Called when events are about to be removed from a resource. * Remove the corresponding KAEvent instances held by AlarmCalendar. */ void ResourcesCalendar::slotEventsToBeRemoved(Resource& resource, const QList& events) { for (const KAEvent& event : events) { if (mEventMap.contains(EventId(event))) deleteEventInternal(event, resource, false); } } /****************************************************************************** * This method must only be called from the main KAlarm queue processing loop, * to prevent asynchronous calendar operations interfering with one another. * * Purge a list of archived events from the calendar. */ void ResourcesCalendar::purgeEvents(const KAEvent::List& events) { for (const KAEvent* event : events) { deleteEventInternal(*event); } if (mHaveDisabledAlarms) checkForDisabledAlarms(); } /****************************************************************************** * Add the specified event to the calendar. * If it is an active event and 'useEventID' is false, a new event ID is * created. In all other cases, the event ID is taken from 'evnt' (if non-null). * 'evnt' is updated with the actual event ID. * The event is added to 'resource' if specified; otherwise the default resource * is used or the user is prompted, depending on policy. If 'noPrompt' is true, * the user will not be prompted so that if no default resource is defined, the * function will fail. * Reply = true if 'evnt' was written to the calendar. 'evnt' is updated. * = false if an error occurred, in which case 'evnt' is unchanged. */ bool ResourcesCalendar::addEvent(KAEvent& evnt, QWidget* promptParent, bool useEventID, Resource* resourceptr, bool noPrompt, bool* cancelled) { if (cancelled) *cancelled = false; Resource nullresource; Resource& resource(resourceptr ? *resourceptr : nullresource); qCDebug(KALARM_LOG) << "ResourcesCalendar::addEvent:" << evnt.id() << ", resource" << resource.displayId(); // Check that the event type is valid for the calendar const CalEvent::Type type = evnt.category(); switch (type) { case CalEvent::ACTIVE: case CalEvent::ARCHIVED: case CalEvent::TEMPLATE: break; default: return false; } const ResourceId key = resource.id(); KAEvent* event = new KAEvent(evnt); QString id = event->id(); if (type == CalEvent::ACTIVE) { if (id.isEmpty()) useEventID = false; else if (!useEventID) id.clear(); } else useEventID = true; if (id.isEmpty()) id = CalFormat::createUniqueId(); if (useEventID) id = CalEvent::uid(id, type); // include the alarm type tag in the ID event->setEventId(id); bool ok = false; bool remove = false; Resource res; if (resource.isEnabled(type)) res = resource; else { res = Resources::destination(type, promptParent, noPrompt, cancelled); if (!res.isValid()) { const char* typeStr = (type == CalEvent::ACTIVE) ? "Active alarm" : (type == CalEvent::ARCHIVED) ? "Archived alarm" : "alarm Template"; qCWarning(KALARM_LOG) << "ResourcesCalendar::addEvent: Error! Cannot create" << typeStr << "(No default calendar is defined)"; } } if (res.isValid()) { // Don't add event to mEventMap yet - its Akonadi item id is not yet known. // It will be added once it is inserted into AkonadiDataModel. ok = res.addEvent(*event); remove = ok; // if success, delete the local event instance on exit if (ok && type == CalEvent::ACTIVE && !event->enabled()) checkForDisabledAlarms(true, false); } if (!ok) { if (remove) { // Adding to mCalendar failed, so undo AlarmCalendar::addEvent() mEventMap.remove(EventId(key, event->id())); KAEvent::List& events = mResourceMap[key]; int i = events.indexOf(event); if (i >= 0) events.remove(i); if (mEarliestAlarm[key] == event) findEarliestAlarm(key); } delete event; return false; } evnt = *event; if (remove) delete event; return true; } /****************************************************************************** * Add the specified event to the calendar. * Reply = true if 'evnt' was written to the calendar. 'evnt' is updated. * = false if an error occurred, in which case 'evnt' is unchanged. */ bool DisplayCalendar::addEvent(KAEvent& evnt) { if (!mOpen) return false; qCDebug(KALARM_LOG) << "DisplayCalendar::addEvent:" << evnt.id(); // Check that the event type is valid for the calendar const CalEvent::Type type = evnt.category(); if (type != CalEvent::DISPLAYING) return false; Event::Ptr kcalEvent(new Event); KAEvent* event = new KAEvent(evnt); QString id = event->id(); if (id.isEmpty()) id = kcalEvent->uid(); id = CalEvent::uid(id, type); // include the alarm type tag in the ID kcalEvent->setUid(id); event->setEventId(id); event->updateKCalEvent(kcalEvent, KAEvent::UID_IGNORE); bool ok = false; bool remove = false; const ResourceId key = DISPLAY_RES_ID; if (!mEventMap.contains(EventId(key, event->id()))) { addNewEvent(Resource(), event); ok = mCalendarStorage->calendar()->addEvent(kcalEvent); remove = !ok; } if (!ok) { if (remove) { // Adding to mCalendar failed, so undo AlarmCalendar::addEvent() mEventMap.remove(EventId(key, event->id())); KAEvent::List& events = mResourceMap[key]; int i = events.indexOf(event); if (i >= 0) events.remove(i); } delete event; return false; } evnt = *event; if (remove) delete event; return true; } /****************************************************************************** * Internal method to add an already checked event to the calendar. * mEventMap takes ownership of the KAEvent. * If 'replace' is true, an existing event is being updated (NOTE: its category() * must remain the same). */ void AlarmCalendar::addNewEvent(const Resource& resource, KAEvent* event, bool replace) { const ResourceId key = resource.id(); event->setResourceId(key); if (!replace) { mResourceMap[key] += event; mEventMap[EventId(key, event->id())] = event; } } /****************************************************************************** * Internal method to add an already checked event to the calendar. * mEventMap takes ownership of the KAEvent. * If 'replace' is true, an existing event is being updated (NOTE: its category() * must remain the same). */ void ResourcesCalendar::addNewEvent(const Resource& resource, KAEvent* event, bool replace) { AlarmCalendar::addNewEvent(resource, event, replace); if ((resource.alarmTypes() & CalEvent::ACTIVE) && event->category() == CalEvent::ACTIVE) { // Update the earliest alarm to trigger const ResourceId key = resource.id(); const KAEvent* earliest = mEarliestAlarm.value(key, (KAEvent*)nullptr); if (replace && earliest == event) findEarliestAlarm(key); else { const KADateTime dt = event->nextTrigger(KAEvent::ALL_TRIGGER).effectiveKDateTime(); if (dt.isValid() && (!earliest || dt < earliest->nextTrigger(KAEvent::ALL_TRIGGER))) { mEarliestAlarm[key] = event; Q_EMIT earliestAlarmChanged(); } } } } /****************************************************************************** * Modify the specified event in the calendar with its new contents. * The new event must have a different event ID from the old one. * It is assumed to be of the same event type as the old one (active, etc.) * Reply = true if 'newEvent' was written to the calendar. 'newEvent' is updated. * = false if an error occurred, in which case 'newEvent' is unchanged. */ bool ResourcesCalendar::modifyEvent(const EventId& oldEventId, KAEvent& newEvent) { const EventId newId(oldEventId.resourceId(), newEvent.id()); qCDebug(KALARM_LOG) << "ResourcesCalendar::modifyEvent:" << oldEventId << "->" << newId; bool noNewId = newId.isEmpty(); if (!noNewId && oldEventId == newId) { qCCritical(KALARM_LOG) << "ResourcesCalendar::modifyEvent: Same IDs"; return false; } // Set the event's ID, and update the old event in the resources calendar. const KAEvent* storedEvent = event(oldEventId); if (!storedEvent) { qCCritical(KALARM_LOG) << "ResourcesCalendar::modifyEvent: Old event not found"; return false; } if (noNewId) newEvent.setEventId(CalFormat::createUniqueId()); Resource resource = Resources::resource(oldEventId.resourceId()); if (!resource.isValid()) return false; // Don't add new event to mEventMap yet - its Akonadi item id is not yet known if (!resource.addEvent(newEvent)) return false; // Note: deleteEventInternal() will delete storedEvent before using the // event parameter, so need to pass a copy as the parameter. deleteEventInternal(KAEvent(*storedEvent), resource); if (mHaveDisabledAlarms) checkForDisabledAlarms(); return true; } /****************************************************************************** * Update the specified event in the calendar with its new contents. * The event retains the same ID. The event must be in the resource calendar. * Reply = event which has been updated * = 0 if error. */ KAEvent* ResourcesCalendar::updateEvent(const KAEvent& evnt) { KAEvent* kaevnt = event(EventId(evnt)); if (kaevnt) { Resource resource = Resources::resourceForEvent(evnt.id()); if (resource.updateEvent(evnt)) { *kaevnt = evnt; return kaevnt; } } qCDebug(KALARM_LOG) << "ResourcesCalendar::updateEvent: error"; return nullptr; } /****************************************************************************** * Delete the specified event from the resource calendar, if it exists. * The calendar is then optionally saved. */ bool ResourcesCalendar::deleteEvent(const KAEvent& event, bool saveit) { Q_UNUSED(saveit); const CalEvent::Type status = deleteEventInternal(event); if (mHaveDisabledAlarms) checkForDisabledAlarms(); return status != CalEvent::EMPTY; } /****************************************************************************** * Delete the specified event from the calendar, if it exists. * The calendar is then optionally saved. */ bool DisplayCalendar::deleteEvent(const QString& eventID, bool saveit) { if (mOpen) { Event::Ptr kcalEvent; if (mCalendarStorage) kcalEvent = mCalendarStorage->calendar()->event(eventID); // display calendar Resource resource; deleteEventBase(eventID, resource); CalEvent::Type status = CalEvent::EMPTY; if (kcalEvent) { status = CalEvent::status(kcalEvent); mCalendarStorage->calendar()->deleteEvent(kcalEvent); } if (status != CalEvent::EMPTY) { if (saveit) return save(); return true; } } return false; } /****************************************************************************** * Internal method to delete the specified event from the calendar and lists. * Reply = event status, if it was found in the resource calendar/calendar * resource or local calendar * = CalEvent::EMPTY otherwise. */ CalEvent::Type ResourcesCalendar::deleteEventInternal(const KAEvent& event, bool deleteFromResources) { Resource resource = Resources::resource(event.resourceId()); if (!resource.isValid()) return CalEvent::EMPTY; return deleteEventInternal(event.id(), event, resource, deleteFromResources); } CalEvent::Type ResourcesCalendar::deleteEventInternal(const KAEvent& event, Resource& resource, bool deleteFromResources) { if (!resource.isValid()) return CalEvent::EMPTY; if (event.resourceId() != resource.id()) { qCCritical(KALARM_LOG) << "ResourcesCalendar::deleteEventInternal: Event" << event.id() << ": resource" << event.resourceId() << "differs from 'resource'" << resource.id(); return CalEvent::EMPTY; } return deleteEventInternal(event.id(), event, resource, deleteFromResources); } CalEvent::Type ResourcesCalendar::deleteEventInternal(const QString& eventID, const KAEvent& event, Resource& resource, bool deleteFromResources) { // Make a copy of the KAEvent and the ID QString, since the supplied // references might be destructed when the event is deleted below. const QString id = eventID; const KAEvent paramEvent = event; const ResourceId key = resource.id(); KAEvent* ev = deleteEventBase(eventID, resource); if (ev) { if (mEarliestAlarm[key] == ev) findEarliestAlarm(resource); } else { for (EarliestMap::Iterator eit = mEarliestAlarm.begin(); eit != mEarliestAlarm.end(); ++eit) { ev = eit.value(); if (ev && ev->id() == id) { findEarliestAlarm(eit.key()); break; } } } CalEvent::Type status = CalEvent::EMPTY; if (deleteFromResources) { // Delete from the resources calendar CalEvent::Type s = paramEvent.category(); if (resource.deleteEvent(paramEvent)) status = s; } return status; } /****************************************************************************** * Internal method to delete the specified event from the calendar and lists. * Reply = event which was deleted, or null if not found. */ KAEvent* AlarmCalendar::deleteEventBase(const QString& eventID, Resource& resource) { // Make a copy of the ID QString, since the supplied reference might be // destructed when the event is deleted below. const QString id = eventID; const ResourceId key = resource.id(); KAEventMap::Iterator it = mEventMap.find(EventId(key, id)); if (it == mEventMap.end()) return nullptr; KAEvent* ev = it.value(); mEventMap.erase(it); KAEvent::List& events = mResourceMap[key]; int i = events.indexOf(ev); if (i >= 0) events.remove(i); delete ev; return ev; } /****************************************************************************** * Return the event with the specified ID. * If 'findUniqueId' is true, and the resource ID is invalid, if there is a * unique event with the given ID, it will be returned. */ KAEvent* AlarmCalendar::event(const EventId& uniqueID) { if (!isValid()) return nullptr; KAEventMap::ConstIterator it = mEventMap.constFind(uniqueID); if (it == mEventMap.constEnd()) return nullptr; return it.value(); } /****************************************************************************** * Return the event with the specified ID. * If 'findUniqueId' is true, and the resource ID is invalid, if there is a * unique event with the given ID, it will be returned. */ KAEvent* ResourcesCalendar::event(const EventId& uniqueID, bool findUniqueId) { if (!isValid()) return nullptr; if (uniqueID.resourceId() == -1 && findUniqueId) { // The resource isn't known, but use the event ID if it is unique among // all resources. const QString eventId = uniqueID.eventId(); const KAEvent::List list = events(eventId); if (list.count() > 1) { qCWarning(KALARM_LOG) << "ResourcesCalendar::event: Multiple events found with ID" << eventId; return nullptr; } if (list.isEmpty()) return nullptr; return list[0]; } KAEventMap::ConstIterator it = mEventMap.constFind(uniqueID); if (it == mEventMap.constEnd()) return nullptr; return it.value(); } /****************************************************************************** * Find the alarm template with the specified name. * Reply = 0 if not found. */ KAEvent* ResourcesCalendar::templateEvent(const QString& templateName) { if (templateName.isEmpty()) return nullptr; const KAEvent::List eventlist = events(CalEvent::TEMPLATE); for (KAEvent* event : eventlist) { if (event->templateName() == templateName) return event; } return nullptr; } /****************************************************************************** * Return all events with the specified ID, from all calendars. */ KAEvent::List ResourcesCalendar::events(const QString& uniqueId) const { KAEvent::List list; if (isValid()) { for (ResourceMap::ConstIterator rit = mResourceMap.constBegin(); rit != mResourceMap.constEnd(); ++rit) { const ResourceId id = rit.key(); KAEventMap::ConstIterator it = mEventMap.constFind(EventId(id, uniqueId)); if (it != mEventMap.constEnd()) list += it.value(); } } return list; } KAEvent::List ResourcesCalendar::events(const Resource& resource, CalEvent::Types type) const { return AlarmCalendar::events(type, resource); } KAEvent::List ResourcesCalendar::events(CalEvent::Types type) const { Resource resource; return AlarmCalendar::events(type, resource); } /****************************************************************************** * Return all events in the calendar which contain alarms. * Optionally the event type can be filtered, using an OR of event types. */ KAEvent::List DisplayCalendar::events(CalEvent::Types type) const { if (!mCalendarStorage) return KAEvent::List(); Resource resource; return AlarmCalendar::events(type, resource); } KAEvent::List AlarmCalendar::events(CalEvent::Types type, const Resource& resource) const { KAEvent::List list; if (resource.isValid()) { const ResourceId key = resource.id(); ResourceMap::ConstIterator rit = mResourceMap.constFind(key); if (rit == mResourceMap.constEnd()) return list; const KAEvent::List events = rit.value(); if (type == CalEvent::EMPTY) return events; for (KAEvent* const event : events) if (type & event->category()) list += event; } else { for (ResourceMap::ConstIterator rit = mResourceMap.constBegin(); rit != mResourceMap.constEnd(); ++rit) { const KAEvent::List events = rit.value(); if (type == CalEvent::EMPTY) list += events; else { for (KAEvent* const event : events) if (type & event->category()) list += event; } } } return list; } /****************************************************************************** * Return the event with the specified ID. * This method is for the display calendar only. */ Event::Ptr DisplayCalendar::kcalEvent(const QString& uniqueID) { if (!mCalendarStorage) return Event::Ptr(); return mCalendarStorage->calendar()->event(uniqueID); } /****************************************************************************** * Return all events in the calendar which contain usable alarms. * This method is for the display calendar only. * Optionally the event type can be filtered, using an OR of event types. */ Event::List DisplayCalendar::kcalEvents(CalEvent::Type type) { Event::List list; if (!mCalendarStorage) return list; list = mCalendarStorage->calendar()->rawEvents(); for (int i = 0; i < list.count(); ) { Event::Ptr event = list.at(i); if (event->alarms().isEmpty() || (type != CalEvent::EMPTY && !(type & CalEvent::status(event))) || !KAEvent(event).isValid()) list.remove(i); else ++i; } return list; } /****************************************************************************** * Return whether an event is read-only. * Display calendar events are always returned as read-only. */ bool ResourcesCalendar::eventReadOnly(const QString& eventId) const { KAEvent event; const Resource resource = Resources::resourceForEvent(eventId, event); return !event.isValid() || event.isReadOnly() || !resource.isWritable(event.category()); //TODO || compatibility(event) != KACalendar::Current; } /****************************************************************************** * Called when an alarm's enabled status has changed. */ void ResourcesCalendar::disabledChanged(const KAEvent* event) { if (event->category() == CalEvent::ACTIVE) { bool status = event->enabled(); checkForDisabledAlarms(!status, status); } } /****************************************************************************** * Check whether there are any individual disabled alarms, following an alarm * creation or modification. Must only be called for an ACTIVE alarm. */ void ResourcesCalendar::checkForDisabledAlarms(bool oldEnabled, bool newEnabled) { if (newEnabled != oldEnabled) { if (newEnabled && mHaveDisabledAlarms) checkForDisabledAlarms(); else if (!newEnabled && !mHaveDisabledAlarms) { mHaveDisabledAlarms = true; Q_EMIT haveDisabledAlarmsChanged(true); } } } /****************************************************************************** * Check whether there are any individual disabled alarms. */ void ResourcesCalendar::checkForDisabledAlarms() { bool disabled = false; const KAEvent::List eventlist = events(CalEvent::ACTIVE); for (const KAEvent* const event : eventlist) { if (!event->enabled()) { disabled = true; break; } } if (disabled != mHaveDisabledAlarms) { mHaveDisabledAlarms = disabled; Q_EMIT haveDisabledAlarmsChanged(disabled); } } /****************************************************************************** * Find and note the active alarm with the earliest trigger time for a calendar. */ void ResourcesCalendar::findEarliestAlarm(const Resource& resource) { if (!(resource.alarmTypes() & CalEvent::ACTIVE)) return; findEarliestAlarm(resource.id()); } void ResourcesCalendar::findEarliestAlarm(ResourceId key) { EarliestMap::Iterator eit = mEarliestAlarm.find(key); if (eit != mEarliestAlarm.end()) eit.value() = nullptr; if (key < 0) return; ResourceMap::ConstIterator rit = mResourceMap.constFind(key); if (rit == mResourceMap.constEnd()) return; const KAEvent::List& events = rit.value(); KAEvent* earliest = nullptr; KADateTime earliestTime; for (KAEvent* event : events) { if (event->category() != CalEvent::ACTIVE || mPendingAlarms.contains(event->id())) continue; const KADateTime dt = event->nextTrigger(KAEvent::ALL_TRIGGER).effectiveKDateTime(); if (dt.isValid() && (!earliest || dt < earliestTime)) { earliestTime = dt; earliest = event; } } mEarliestAlarm[key] = earliest; Q_EMIT earliestAlarmChanged(); } /****************************************************************************** * Return the active alarm with the earliest trigger time. * Reply = 0 if none. */ KAEvent* ResourcesCalendar::earliestAlarm() const { KAEvent* earliest = nullptr; KADateTime earliestTime; for (EarliestMap::ConstIterator eit = mEarliestAlarm.constBegin(); eit != mEarliestAlarm.constEnd(); ++eit) { KAEvent* event = eit.value(); if (!event) continue; const KADateTime dt = event->nextTrigger(KAEvent::ALL_TRIGGER).effectiveKDateTime(); if (dt.isValid() && (!earliest || dt < earliestTime)) { earliestTime = dt; earliest = event; } } return earliest; } /****************************************************************************** * Note that an alarm which has triggered is now being processed. While pending, * it will be ignored for the purposes of finding the earliest trigger time. */ void ResourcesCalendar::setAlarmPending(KAEvent* event, bool pending) { const QString id = event->id(); bool wasPending = mPendingAlarms.contains(id); qCDebug(KALARM_LOG) << "ResourcesCalendar::setAlarmPending:" << id << "," << pending << "(was" << wasPending << ")"; if (pending) { if (wasPending) return; mPendingAlarms += id; } else { if (!wasPending) return; mPendingAlarms.remove(id); } // Now update the earliest alarm to trigger for its calendar findEarliestAlarm(Resources::resourceForEvent(event->id())); } /****************************************************************************** * Called when the user changes the start-of-day time. * Adjust the start times of all date-only alarms' recurrences. */ void AlarmCalendar::adjustStartOfDay() { if (!isValid()) return; for (ResourceMap::ConstIterator rit = mResourceMap.constBegin(); rit != mResourceMap.constEnd(); ++rit) KAEvent::adjustStartOfDay(rit.value()); } // vim: et sw=4: diff --git a/src/functions.cpp b/src/functions.cpp index bbafe568..c62aae2e 100644 --- a/src/functions.cpp +++ b/src/functions.cpp @@ -1,1917 +1,1807 @@ /* * functions.cpp - miscellaneous functions * Program: kalarm * Copyright © 2001-2020 David Jarvie * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "functions.h" #include "functions_p.h" #include "akonadicollectionsearch.h" #include "alarmcalendar.h" #include "alarmtime.h" #include "editdlg.h" #include "kalarmapp.h" #include "kamail.h" #include "mainwindow.h" #include "messagewin.h" #include "preferences.h" #include "templatelistview.h" #include "templatemenuaction.h" +#include "resources/calendarfunctions.h" #include "resources/datamodel.h" #include "resources/resources.h" #include "resources/eventmodel.h" #include "lib/autoqpointer.h" #include "lib/filedialog.h" #include "lib/messagebox.h" #include "lib/shellprocess.h" #include "config-kalarm.h" #include "kalarm_debug.h" #include #include #include #include #include #include #include #include using namespace KCalendarCore; #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace { bool refreshAlarmsQueued = false; QUrl lastImportUrl; // last URL for Import Alarms file dialogue 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); -bool updateCalendarFormat(const FileStorage::Ptr&); -bool importCalendarFile(const QUrl&, CalEvent::Types alarmTypes, QWidget* parent, QHash>&); } namespace KAlarm { Private* Private::mInstance = nullptr; /****************************************************************************** * Display a main window with the specified event selected. */ MainWindow* displayMainWindowSelected(const QString& eventId) { MainWindow* win = MainWindow::firstWindow(); if (!win) { if (theApp()->checkCalendar()) // ensure calendar is open { win = MainWindow::create(); win->show(); } } else { // There is already a main window, so make it the active window #pragma message("Don't hide unless necessary, since it moves the window") win->hide(); // in case it's on a different desktop win->setWindowState(win->windowState() & ~Qt::WindowMinimized); win->show(); win->raise(); win->activateWindow(); } if (win) win->selectEvent(eventId); return win; } /****************************************************************************** * Create an "Alarms Enabled/Enable Alarms" action. */ KToggleAction* createAlarmEnableAction(QObject* parent) { KToggleAction* action = new KToggleAction(i18nc("@action", "Enable &Alarms"), parent); action->setChecked(theApp()->alarmsEnabled()); QObject::connect(action, &QAction::toggled, theApp(), &KAlarmApp::setAlarmsEnabled); // The following line ensures that all instances are kept in the same state QObject::connect(theApp(), &KAlarmApp::alarmEnabledToggled, action, &QAction::setChecked); return action; } /****************************************************************************** * Create a "Stop Play" action. */ QAction* createStopPlayAction(QObject* parent) { QAction* action = new QAction(QIcon::fromTheme(QStringLiteral("media-playback-stop")), i18nc("@action", "Stop Play"), parent); action->setEnabled(MessageWin::isAudioPlaying()); QObject::connect(action, &QAction::triggered, theApp(), &KAlarmApp::stopAudio); // The following line ensures that all instances are kept in the same state QObject::connect(theApp(), &KAlarmApp::audioPlaying, action, &QAction::setEnabled); return action; } /****************************************************************************** * Create a "Spread Windows" action. */ KToggleAction* createSpreadWindowsAction(QObject* parent) { KToggleAction* action = new KToggleAction(i18nc("@action", "Spread Windows"), parent); QObject::connect(action, &QAction::triggered, theApp(), &KAlarmApp::spreadWindows); // The following line ensures that all instances are kept in the same state QObject::connect(theApp(), &KAlarmApp::spreadWindowsToggled, action, &QAction::setChecked); return action; } /****************************************************************************** * Add a new active (non-archived) alarm. * Save it in the calendar file and add it to every main window instance. * Parameters: msgParent = parent widget for any calendar selection prompt or * error message. * event - is updated with the actual event ID. */ UpdateResult addEvent(KAEvent& event, Resource* resource, QWidget* msgParent, int options, bool showKOrgErr) { qCDebug(KALARM_LOG) << "KAlarm::addEvent:" << event.id(); bool cancelled = false; UpdateStatusData status; if (!theApp()->checkCalendar()) // ensure calendar is open status.status = UPDATE_FAILED; else { // Save the event details in the calendar file, and get the new event ID ResourcesCalendar* cal = ResourcesCalendar::instance(); // Note that AlarmCalendar::addEvent() updates 'event'. if (!cal->addEvent(event, msgParent, (options & USE_EVENT_ID), resource, (options & NO_RESOURCE_PROMPT), &cancelled)) { status.status = UPDATE_FAILED; } else { if (!cal->save()) status.status = SAVE_FAILED; } if (status.status == UPDATE_OK) { if ((options & ALLOW_KORG_UPDATE) && event.copyToKOrganizer()) { UpdateResult st = sendToKOrganizer(event); // tell KOrganizer to show the event status.korgUpdate(st); } } } if (status.status != UPDATE_OK && !cancelled && msgParent) displayUpdateError(msgParent, ERR_ADD, status, showKOrgErr); return status.status; } /****************************************************************************** * Add a list of new active (non-archived) alarms. * Save them in the calendar file and add them to every main window instance. * The events are updated with their actual event IDs. */ UpdateResult addEvents(QVector& events, QWidget* msgParent, bool allowKOrgUpdate, bool showKOrgErr) { qCDebug(KALARM_LOG) << "KAlarm::addEvents:" << events.count(); if (events.isEmpty()) return UpdateResult(UPDATE_OK); UpdateStatusData status; if (!theApp()->checkCalendar()) // ensure calendar is open status.status = UPDATE_FAILED; else { Resource resource = Resources::destination(CalEvent::ACTIVE, msgParent); if (!resource.isValid()) { qCDebug(KALARM_LOG) << "KAlarm::addEvents: No calendar"; status.status = UPDATE_FAILED; } else { ResourcesCalendar* cal = ResourcesCalendar::instance(); for (int i = 0, end = events.count(); i < end; ++i) { // Save the event details in the calendar file, and get the new event ID KAEvent& event = events[i]; if (!cal->addEvent(event, msgParent, false, &resource)) { status.setError(UPDATE_ERROR); continue; } if (allowKOrgUpdate && event.copyToKOrganizer()) { UpdateResult st = sendToKOrganizer(event); // tell KOrganizer to show the event status.korgUpdate(st); } } if (status.warnErr == events.count()) status.status = UPDATE_FAILED; else if (!cal->save()) status.setError(SAVE_FAILED, events.count()); // everything failed } } if (status.status != UPDATE_OK && msgParent) displayUpdateError(msgParent, ERR_ADD, status, showKOrgErr); return status.status; } /****************************************************************************** * Save the event in the archived calendar and adjust every main window instance. * The event's ID is changed to an archived ID if necessary. */ bool addArchivedEvent(KAEvent& event, Resource* resourceptr) { qCDebug(KALARM_LOG) << "KAlarm::addArchivedEvent:" << event.id(); bool archiving = (event.category() == CalEvent::ACTIVE); if (archiving && !Preferences::archivedKeepDays()) return false; // expired alarms aren't being kept ResourcesCalendar* cal = ResourcesCalendar::instance(); KAEvent newevent(event); KAEvent* const newev = &newevent; if (archiving) { newev->setCategory(CalEvent::ARCHIVED); // this changes the event ID newev->setCreatedDateTime(KADateTime::currentUtcDateTime()); // time stamp to control purging } // Note that archived resources are automatically saved after changes are made if (!cal->addEvent(newevent, nullptr, false, resourceptr)) return false; event = *newev; // update event ID etc. return true; } /****************************************************************************** * Add a new template. * Save it in the calendar file and add it to every template list view. * 'event' is updated with the actual event ID. * Parameters: promptParent = parent widget for any calendar selection prompt. */ UpdateResult addTemplate(KAEvent& event, Resource* resourceptr, QWidget* msgParent) { qCDebug(KALARM_LOG) << "KAlarm::addTemplate:" << event.id(); UpdateStatusData status; // Add the template to the calendar file ResourcesCalendar* cal = ResourcesCalendar::instance(); KAEvent newev(event); if (!cal->addEvent(newev, msgParent, false, resourceptr)) status.status = UPDATE_FAILED; else { event = newev; // update event ID etc. if (!cal->save()) status.status = SAVE_FAILED; else { return UpdateResult(UPDATE_OK); } } if (msgParent) displayUpdateError(msgParent, ERR_TEMPLATE, status); return status.status; } /****************************************************************************** * Modify an active (non-archived) alarm in the calendar file and in every main * window instance. * The new event must have a different event ID from the old one. */ UpdateResult modifyEvent(KAEvent& oldEvent, KAEvent& newEvent, QWidget* msgParent, bool showKOrgErr) { qCDebug(KALARM_LOG) << "KAlarm::modifyEvent:" << oldEvent.id(); UpdateStatusData status; if (!newEvent.isValid()) { deleteEvent(oldEvent, true); status.status = UPDATE_FAILED; } else { EventId oldId(oldEvent); if (oldEvent.copyToKOrganizer()) { // Tell KOrganizer to delete its old event. // But ignore errors, because the user could have manually // deleted it since KAlarm asked KOrganizer to set it up. deleteFromKOrganizer(oldId.eventId()); } // Update the event in the calendar file, and get the new event ID ResourcesCalendar* cal = ResourcesCalendar::instance(); if (!cal->modifyEvent(oldId, newEvent)) status.status = UPDATE_FAILED; else { if (!cal->save()) status.status = SAVE_FAILED; if (status.status == UPDATE_OK) { if (newEvent.copyToKOrganizer()) { UpdateResult st = sendToKOrganizer(newEvent); // tell KOrganizer to show the new event status.korgUpdate(st); } // Remove "Don't show error messages again" for the old alarm setDontShowErrors(oldId); } } } if (status.status != UPDATE_OK && msgParent) displayUpdateError(msgParent, ERR_MODIFY, status, showKOrgErr); return status.status; } /****************************************************************************** * Update an active (non-archived) alarm from the calendar file and from every * main window instance. * The new event will have the same event ID as the old one. * The event is not updated in KOrganizer, since this function is called when an * existing alarm is rescheduled (due to recurrence or deferral). */ UpdateResult updateEvent(KAEvent& event, QWidget* msgParent, bool archiveOnDelete) { qCDebug(KALARM_LOG) << "KAlarm::updateEvent:" << event.id(); if (!event.isValid()) deleteEvent(event, archiveOnDelete); else { // Update the event in the calendar file. ResourcesCalendar* cal = ResourcesCalendar::instance(); cal->updateEvent(event); if (!cal->save()) { if (msgParent) displayUpdateError(msgParent, ERR_ADD, UpdateStatusData(SAVE_FAILED)); return UpdateResult(SAVE_FAILED); } } return UpdateResult(UPDATE_OK); } /****************************************************************************** * Update a template in the calendar file and in every template list view. * If 'selectionView' is non-null, the selection highlight is moved to the * updated event in that listView instance. */ UpdateResult updateTemplate(KAEvent& event, QWidget* msgParent) { ResourcesCalendar* cal = ResourcesCalendar::instance(); const KAEvent* newEvent = cal->updateEvent(event); UpdateStatus status = UPDATE_OK; if (!newEvent) status = UPDATE_FAILED; else if (!cal->save()) status = SAVE_FAILED; if (status != UPDATE_OK) { if (msgParent) displayUpdateError(msgParent, ERR_TEMPLATE, UpdateStatusData(status)); return UpdateResult(status); } return UpdateResult(UPDATE_OK); } /****************************************************************************** * Delete alarms from the calendar file and from every main window instance. * If the events are archived, the events' IDs are changed to archived IDs if necessary. */ UpdateResult deleteEvent(KAEvent& event, bool archive, QWidget* msgParent, bool showKOrgErr) { QVector events(1, event); return deleteEvents(events, archive, msgParent, showKOrgErr); } UpdateResult deleteEvents(QVector& events, bool archive, QWidget* msgParent, bool showKOrgErr) { qCDebug(KALARM_LOG) << "KAlarm::deleteEvents:" << events.count(); if (events.isEmpty()) return UpdateResult(UPDATE_OK); UpdateStatusData status; ResourcesCalendar* cal = ResourcesCalendar::instance(); bool deleteWakeFromSuspendAlarm = false; const QString wakeFromSuspendId = checkRtcWakeConfig().value(0); for (int i = 0, end = events.count(); i < end; ++i) { // Save the event details in the calendar file, and get the new event ID KAEvent* event = &events[i]; const QString id = event->id(); // Delete the event from the calendar file if (event->category() != CalEvent::ARCHIVED) { if (event->copyToKOrganizer()) { // The event was shown in KOrganizer, so tell KOrganizer to // delete it. But ignore errors, because the user could have // manually deleted it from KOrganizer since it was set up. UpdateResult st = deleteFromKOrganizer(id); status.korgUpdate(st); } if (archive && event->toBeArchived()) { KAEvent ev(*event); addArchivedEvent(ev); // this changes the event ID to an archived ID } } if (!cal->deleteEvent(*event, false)) // don't save calendar after deleting status.setError(UPDATE_ERROR); if (id == wakeFromSuspendId) deleteWakeFromSuspendAlarm = true; // Remove "Don't show error messages again" for this alarm setDontShowErrors(EventId(*event)); } if (status.warnErr == events.count()) status.status = UPDATE_FAILED; else if (!cal->save()) // save the calendars now status.setError(SAVE_FAILED, events.count()); if (status.status != UPDATE_OK && msgParent) displayUpdateError(msgParent, ERR_DELETE, status, showKOrgErr); // Remove any wake-from-suspend scheduled for a deleted alarm if (deleteWakeFromSuspendAlarm && !wakeFromSuspendId.isEmpty()) cancelRtcWake(msgParent, wakeFromSuspendId); return status.status; } /****************************************************************************** * Delete templates from the calendar file and from every template list view. */ UpdateResult deleteTemplates(const KAEvent::List& events, QWidget* msgParent) { int count = events.count(); qCDebug(KALARM_LOG) << "KAlarm::deleteTemplates:" << count; if (!count) return UpdateResult(UPDATE_OK); UpdateStatusData status; ResourcesCalendar* cal = ResourcesCalendar::instance(); for (const KAEvent* event : events) { // Update the window lists // Delete the template from the calendar file if (!cal->deleteEvent(*event, false)) // don't save calendar after deleting status.setError(UPDATE_ERROR); } if (status.warnErr == count) status.status = UPDATE_FAILED; else if (!cal->save()) // save the calendars now status.setError(SAVE_FAILED, count); if (status.status != UPDATE_OK && msgParent) displayUpdateError(msgParent, ERR_TEMPLATE, status); return status.status; } /****************************************************************************** * Delete an alarm from the display calendar. */ void deleteDisplayEvent(const QString& eventID) { qCDebug(KALARM_LOG) << "KAlarm::deleteDisplayEvent:" << eventID; DisplayCalendar* cal = DisplayCalendar::instanceOpen(); if (cal) cal->deleteEvent(eventID, true); // save calendar after deleting } /****************************************************************************** * Undelete archived alarms, and update every main window instance. * The archive bit is set to ensure that they get re-archived if deleted again. * Parameters: * calendar - the active alarms calendar to restore the alarms into, or null * to use the default way of determining the active alarm calendar. * ineligibleIDs - will be filled in with the IDs of any ineligible events. */ UpdateResult reactivateEvent(KAEvent& event, Resource* resourceptr, QWidget* msgParent, bool showKOrgErr) { QVector ids; QVector events(1, event); return reactivateEvents(events, ids, resourceptr, msgParent, showKOrgErr); } UpdateResult reactivateEvents(QVector& events, QVector& ineligibleIDs, Resource* resourceptr, QWidget* msgParent, bool showKOrgErr) { qCDebug(KALARM_LOG) << "KAlarm::reactivateEvents:" << events.count(); ineligibleIDs.clear(); if (events.isEmpty()) return UpdateResult(UPDATE_OK); UpdateStatusData status; Resource resource; if (resourceptr) resource = *resourceptr; if (!resource.isValid()) resource = Resources::destination(CalEvent::ACTIVE, msgParent); if (!resource.isValid()) { qCDebug(KALARM_LOG) << "KAlarm::reactivateEvents: No calendar"; status.setError(UPDATE_FAILED, events.count()); } else { int count = 0; ResourcesCalendar* cal = ResourcesCalendar::instance(); const KADateTime now = KADateTime::currentUtcDateTime(); for (int i = 0, end = events.count(); i < end; ++i) { // Delete the event from the archived resource KAEvent* event = &events[i]; if (event->category() != CalEvent::ARCHIVED || !event->occursAfter(now, true)) { ineligibleIDs += EventId(*event); continue; } ++count; KAEvent newevent(*event); KAEvent* const newev = &newevent; newev->setCategory(CalEvent::ACTIVE); // this changes the event ID if (newev->recurs() || newev->repetition()) newev->setNextOccurrence(now); // skip any recurrences in the past newev->setArchive(); // ensure that it gets re-archived if it is deleted // Save the event details in the calendar file. // This converts the event ID. if (!cal->addEvent(newevent, msgParent, true, &resource)) { status.setError(UPDATE_ERROR); continue; } if (newev->copyToKOrganizer()) { UpdateResult st = sendToKOrganizer(*newev); // tell KOrganizer to show the event status.korgUpdate(st); } if (cal->event(EventId(*event)) // no error if event doesn't exist in archived resource && !cal->deleteEvent(*event, false)) // don't save calendar after deleting status.setError(UPDATE_ERROR); events[i] = newevent; } if (status.warnErr == count) status.status = UPDATE_FAILED; // Save the calendars, even if all events failed, since more than one calendar was updated if (!cal->save() && status.status != UPDATE_FAILED) status.setError(SAVE_FAILED, count); } if (status.status != UPDATE_OK && msgParent) displayUpdateError(msgParent, ERR_REACTIVATE, status, showKOrgErr); return status.status; } /****************************************************************************** * Enable or disable alarms in the calendar file and in every main window instance. * The new events will have the same event IDs as the old ones. */ UpdateResult enableEvents(QVector& events, bool enable, QWidget* msgParent) { qCDebug(KALARM_LOG) << "KAlarm::enableEvents:" << events.count(); if (events.isEmpty()) return UpdateResult(UPDATE_OK); UpdateStatusData status; ResourcesCalendar* cal = ResourcesCalendar::instance(); bool deleteWakeFromSuspendAlarm = false; const QString wakeFromSuspendId = checkRtcWakeConfig().value(0); for (int i = 0, end = events.count(); i < end; ++i) { KAEvent* event = &events[i]; if (event->category() == CalEvent::ACTIVE && enable != event->enabled()) { event->setEnabled(enable); if (!enable && event->id() == wakeFromSuspendId) deleteWakeFromSuspendAlarm = true; // Update the event in the calendar file const KAEvent* newev = cal->updateEvent(*event); if (!newev) qCCritical(KALARM_LOG) << "KAlarm::enableEvents: Error updating event in calendar:" << event->id(); else { cal->disabledChanged(newev); // If we're disabling a display alarm, close any message window if (!enable && (event->actionTypes() & KAEvent::ACT_DISPLAY)) { MessageWin* win = MessageWin::findEvent(EventId(*event)); delete win; } } } } if (!cal->save()) status.setError(SAVE_FAILED, events.count()); if (status.status != UPDATE_OK && msgParent) displayUpdateError(msgParent, ERR_ADD, status); // Remove any wake-from-suspend scheduled for a disabled alarm if (deleteWakeFromSuspendAlarm && !wakeFromSuspendId.isEmpty()) cancelRtcWake(msgParent, wakeFromSuspendId); return status.status; } /****************************************************************************** * This method must only be called from the main KAlarm queue processing loop, * to prevent asynchronous calendar operations interfering with one another. * * Purge all archived events from the default archived alarm resource whose end * time is longer ago than 'purgeDays'. All events are deleted if 'purgeDays' is * zero. */ void purgeArchive(int purgeDays) { if (purgeDays < 0) return; qCDebug(KALARM_LOG) << "KAlarm::purgeArchive:" << purgeDays; const QDate cutoff = KADateTime::currentLocalDate().addDays(-purgeDays); const Resource resource = Resources::getStandard(CalEvent::ARCHIVED); if (!resource.isValid()) return; KAEvent::List events = ResourcesCalendar::instance()->events(resource); for (int i = 0; i < events.count(); ) { if (purgeDays && events.at(i)->createdDateTime().date() >= cutoff) events.remove(i); else ++i; } if (!events.isEmpty()) ResourcesCalendar::instance()->purgeEvents(events); // delete the events and save the calendar } /****************************************************************************** * Display an error message about an error when saving an event. * If 'model' is non-null, the AlarmListModel* which it points to is used; if * that is null, it is created. */ QVector getSortedActiveEvents(QObject* parent, AlarmListModel** model) { AlarmListModel* mdl = nullptr; if (!model) model = &mdl; if (!*model) { *model = DataModel::createAlarmListModel(parent); (*model)->setEventTypeFilter(CalEvent::ACTIVE); (*model)->sort(AlarmListModel::TimeColumn); } QVector result; for (int i = 0, count = (*model)->rowCount(); i < count; ++i) { const KAEvent event = (*model)->event(i); if (event.enabled() && !event.expired()) result += event; } return result; } /****************************************************************************** * Import alarms from an external calendar and merge them into KAlarm's calendar. * The alarms are given new unique event IDs. * Parameters: parent = parent widget for error message boxes * Reply = true if all alarms in the calendar were successfully imported * = false if any alarms failed to be imported. */ bool importAlarms(Resource& resource, QWidget* parent) { qCDebug(KALARM_LOG) << "KAlarm::importAlarms" << resource.displayId(); const QList urls = QFileDialog::getOpenFileUrls( parent, i18nc("@title:window", "Import Calendar Files"), lastImportUrl, QStringLiteral("%1 (*.ics *.vcs)").arg(i18nc("@info", "Calendar Files"))); if (urls.isEmpty()) return false; lastImportUrl = urls[0].adjusted(QUrl::RemoveFilename); const CalEvent::Types alarmTypes = resource.isValid() ? resource.alarmTypes() : CalEvent::ACTIVE | CalEvent::ARCHIVED | CalEvent::TEMPLATE; // Read all the selected calendar files and extract their alarms. QHash> events; for (const QUrl& url : urls) { if (!url.isValid()) { qCDebug(KALARM_LOG) << "KAlarm::importAlarms: Invalid URL"; continue; } qCDebug(KALARM_LOG) << "KAlarm::importAlarms:" << url.toDisplayString(); - importCalendarFile(url, alarmTypes, parent, events); + importCalendarFile(url, alarmTypes, true, parent, events); } if (events.isEmpty()) return false; // Add the alarms to the destination resource. bool success = true; for (auto it = events.constBegin(); it != events.constEnd(); ++it) { Resource res; if (resource.isValid()) res = resource; else res = Resources::destination(it.key()); - for (const KAEvent& event : it.value()) + for (const KAEvent& event : qAsConst(it.value())) { if (!res.addEvent(event)) success = false; } } return success; } /****************************************************************************** * Export all selected alarms to an external calendar. * The alarms are given new unique event IDs. * Parameters: parent = parent widget for error message boxes * Reply = true if all alarms in the calendar were successfully exported * = false if any alarms failed to be exported. */ bool exportAlarms(const KAEvent::List& events, QWidget* parent) { bool append; //TODO: exportalarms shows up afterwards in other file dialogues QString file = FileDialog::getSaveFileName(QUrl(QStringLiteral("kfiledialog:///exportalarms")), QStringLiteral("*.ics|%1").arg(i18nc("@info", "Calendar Files")), parent, i18nc("@title:window", "Choose Export Calendar"), &append); if (file.isEmpty()) return false; const QUrl url = QUrl::fromLocalFile(file); if (!url.isValid()) { qCDebug(KALARM_LOG) << "KAlarm::exportAlarms: Invalid URL" << url; return false; } qCDebug(KALARM_LOG) << "KAlarm::exportAlarms:" << url.toDisplayString(); MemoryCalendar::Ptr calendar(new MemoryCalendar(Preferences::timeSpecAsZone())); FileStorage::Ptr calStorage(new FileStorage(calendar, file)); if (append && !calStorage->load()) { auto statJob = KIO::statDetails(url, KIO::StatJob::SourceSide, KIO::StatDetail::StatDefaultDetails); KJobWidgets::setWindow(statJob, parent); statJob->exec(); KFileItem fi(statJob->statResult(), url); if (fi.size()) { qCCritical(KALARM_LOG) << "KAlarm::exportAlarms: Error loading calendar file" << file << "for append"; KAMessageBox::error(MainWindow::mainMainWindow(), xi18nc("@info", "Error loading calendar to append to:%1", url.toDisplayString())); return false; } } KACalendar::setKAlarmVersion(calendar); // Add the alarms to the calendar bool success = true; bool exported = false; for (int i = 0, end = events.count(); i < end; ++i) { const KAEvent* event = events[i]; Event::Ptr kcalEvent(new Event); const CalEvent::Type type = event->category(); const QString id = CalEvent::uid(kcalEvent->uid(), type); kcalEvent->setUid(id); event->updateKCalEvent(kcalEvent, KAEvent::UID_IGNORE); if (calendar->addEvent(kcalEvent)) exported = true; else success = false; } if (exported) { // One or more alarms have been exported to the calendar. // Save the calendar to file. QTemporaryFile* tempFile = nullptr; bool local = url.isLocalFile(); if (!local) { tempFile = new QTemporaryFile; file = tempFile->fileName(); } calStorage->setFileName(file); calStorage->setSaveFormat(new ICalFormat); if (!calStorage->save()) { qCCritical(KALARM_LOG) << "KAlarm::exportAlarms:" << file << ": failed"; KAMessageBox::error(MainWindow::mainMainWindow(), xi18nc("@info", "Failed to save new calendar to:%1", url.toDisplayString())); success = false; } else if (!local) { QFile qFile(file); qFile.open(QIODevice::ReadOnly); auto uploadJob = KIO::storedPut(&qFile, url, -1); KJobWidgets::setWindow(uploadJob, parent); if (!uploadJob->exec()) { qCCritical(KALARM_LOG) << "KAlarm::exportAlarms:" << file << ": upload failed"; KAMessageBox::error(MainWindow::mainMainWindow(), xi18nc("@info", "Cannot upload new calendar to:%1", url.toDisplayString())); success = false; } } delete tempFile; } calendar->close(); return success; } /****************************************************************************** * Display an error message corresponding to a specified alarm update error code. */ void displayKOrgUpdateError(QWidget* parent, UpdateError code, const UpdateResult& korgError, int nAlarms) { QString errmsg; switch (code) { case ERR_ADD: case ERR_REACTIVATE: errmsg = (nAlarms > 1) ? i18nc("@info", "Unable to show alarms in KOrganizer") : i18nc("@info", "Unable to show alarm in KOrganizer"); break; case ERR_MODIFY: errmsg = i18nc("@info", "Unable to update alarm in KOrganizer"); break; case ERR_DELETE: errmsg = (nAlarms > 1) ? i18nc("@info", "Unable to delete alarms from KOrganizer") : i18nc("@info", "Unable to delete alarm from KOrganizer"); break; case ERR_TEMPLATE: return; } bool showDetail = !korgError.message.isEmpty(); QString msg; switch (korgError.status) { case UPDATE_KORG_ERRINIT: msg = xi18nc("@info", "%1(Could not start KOrganizer)", errmsg); break; case UPDATE_KORG_ERRSTART: msg = xi18nc("@info", "%1(KOrganizer not fully started)", errmsg); break; case UPDATE_KORG_ERR: msg = xi18nc("@info", "%1(Error communicating with KOrganizer)", errmsg); break; default: msg = errmsg; showDetail = false; break; } if (showDetail) KAMessageBox::detailedError(parent, msg, korgError.message); else KAMessageBox::error(parent, msg); } /****************************************************************************** * Execute a New Alarm dialog for the specified alarm type. */ void editNewAlarm(EditAlarmDlg::Type type, QWidget* parent) { execNewAlarmDlg(EditAlarmDlg::create(false, type, parent)); } /****************************************************************************** * Execute a New Alarm dialog for the specified alarm type. */ void editNewAlarm(KAEvent::SubAction action, QWidget* parent, const AlarmText* text) { bool setAction = false; EditAlarmDlg::Type type; switch (action) { case KAEvent::MESSAGE: case KAEvent::FILE: type = EditAlarmDlg::DISPLAY; setAction = true; break; case KAEvent::COMMAND: type = EditAlarmDlg::COMMAND; break; case KAEvent::EMAIL: type = EditAlarmDlg::EMAIL; break; case KAEvent::AUDIO: type = EditAlarmDlg::AUDIO; break; default: return; } EditAlarmDlg* editDlg = EditAlarmDlg::create(false, type, parent); if (setAction || text) editDlg->setAction(action, *text); execNewAlarmDlg(editDlg); } /****************************************************************************** * Execute a New Alarm dialog, optionally either presetting it to the supplied * event, or setting the action and text. */ void editNewAlarm(const KAEvent* preset, QWidget* parent) { execNewAlarmDlg(EditAlarmDlg::create(false, preset, true, parent)); } /****************************************************************************** * Common code for editNewAlarm() variants. */ void execNewAlarmDlg(EditAlarmDlg* editDlg) { // Create a PrivateNewAlarmDlg parented by editDlg. // It will be deleted when editDlg is closed. new PrivateNewAlarmDlg(editDlg); editDlg->show(); editDlg->raise(); editDlg->activateWindow(); } PrivateNewAlarmDlg::PrivateNewAlarmDlg(EditAlarmDlg* dlg) : QObject(dlg) { connect(dlg, &QDialog::accepted, this, &PrivateNewAlarmDlg::okClicked); connect(dlg, &QDialog::rejected, this, &PrivateNewAlarmDlg::cancelClicked); } /****************************************************************************** * Called when the dialogue is accepted (e.g. by clicking the OK button). * Creates the event specified in the instance's dialogue. */ void PrivateNewAlarmDlg::okClicked() { accept(static_cast(parent())); } /****************************************************************************** * Creates the event specified in a given dialogue. */ void PrivateNewAlarmDlg::accept(EditAlarmDlg* editDlg) { KAEvent event; Resource resource; editDlg->getEvent(event, resource); // Add the alarm to the displayed lists and to the calendar file const UpdateResult status = addEvent(event, &resource, editDlg); switch (status.status) { case UPDATE_FAILED: return; case UPDATE_KORG_ERR: case UPDATE_KORG_ERRINIT: case UPDATE_KORG_ERRSTART: case UPDATE_KORG_FUNCERR: displayKOrgUpdateError(editDlg, ERR_ADD, status); break; default: break; } Undo::saveAdd(event, resource); outputAlarmWarnings(editDlg, &event); editDlg->deleteLater(); } /****************************************************************************** * Called when the dialogue is rejected (e.g. by clicking the Cancel button). */ void PrivateNewAlarmDlg::cancelClicked() { static_cast(parent())->deleteLater(); } /****************************************************************************** * Display the alarm edit dialog to edit a new alarm, preset with a template. */ bool editNewAlarm(const QString& templateName, QWidget* parent) { if (!templateName.isEmpty()) { KAEvent* templateEvent = ResourcesCalendar::instance()->templateEvent(templateName); if (templateEvent->isValid()) { editNewAlarm(templateEvent, parent); return true; } qCWarning(KALARM_LOG) << "KAlarm::editNewAlarm:" << templateName << ": template not found"; } return false; } /****************************************************************************** * Create a new template. */ void editNewTemplate(EditAlarmDlg::Type type, QWidget* parent) { ::editNewTemplate(type, nullptr, parent); } /****************************************************************************** * Create a new template, based on an existing event or template. */ void editNewTemplate(const KAEvent* preset, QWidget* parent) { ::editNewTemplate(EditAlarmDlg::Type(0), preset, parent); } /****************************************************************************** * Check the config as to whether there is a wake-on-suspend alarm pending, and * if so, delete it from the config if it has expired. * If 'checkExists' is true, the config entry will only be returned if the * event exists. * Reply = config entry: [0] = event's resource ID, * [1] = event ID, * [2] = trigger time (int64 seconds since epoch). * = empty list if none or expired. */ QStringList checkRtcWakeConfig(bool checkEventExists) { KConfigGroup config(KSharedConfig::openConfig(), "General"); const QStringList params = config.readEntry("RtcWake", QStringList()); #if KALARMCAL_VERSION >= QT_VERSION_CHECK(5,12,1) if (params.count() == 3 && params[2].toLongLong() > KADateTime::currentUtcDateTime().toSecsSinceEpoch()) #else if (params.count() == 3 && params[2].toUInt() > KADateTime::currentUtcDateTime().toTime_t()) #endif { if (checkEventExists && !ResourcesCalendar::getEvent(EventId(params[0].toLongLong(), params[1]))) return QStringList(); return params; // config entry is valid } if (!params.isEmpty()) { config.deleteEntry("RtcWake"); // delete the expired config entry config.sync(); } return QStringList(); } /****************************************************************************** * Delete any wake-on-suspend alarm from the config. */ void deleteRtcWakeConfig() { KConfigGroup config(KSharedConfig::openConfig(), "General"); config.deleteEntry("RtcWake"); config.sync(); } /****************************************************************************** * Delete any wake-on-suspend alarm, optionally only for a specified event. */ void cancelRtcWake(QWidget* msgParent, const QString& eventId) { const QStringList wakeup = checkRtcWakeConfig(); if (!wakeup.isEmpty() && (eventId.isEmpty() || wakeup[0] == eventId)) { Private::instance()->mMsgParent = msgParent ? msgParent : MainWindow::mainMainWindow(); QTimer::singleShot(0, Private::instance(), &Private::cancelRtcWake); } } /****************************************************************************** * Delete any wake-on-suspend alarm. */ void Private::cancelRtcWake() { // setRtcWakeTime will only work with a parent window specified setRtcWakeTime(0, mMsgParent); deleteRtcWakeConfig(); KAMessageBox::information(mMsgParent, i18nc("info", "The scheduled Wake from Suspend has been cancelled.")); } /****************************************************************************** * Set the wakeup time for the system. * Set 'triggerTime' to zero to cancel the wakeup. * Reply = true if successful. */ bool setRtcWakeTime(unsigned triggerTime, QWidget* parent) { QVariantMap args; args[QStringLiteral("time")] = triggerTime; KAuth::Action action(QStringLiteral("org.kde.kalarm.rtcwake.settimer")); action.setHelperId(QStringLiteral("org.kde.kalarm.rtcwake")); action.setParentWidget(parent); action.setArguments(args); KAuth::ExecuteJob* job = action.execute(); if (!job->exec()) { QString errmsg = job->errorString(); qCDebug(KALARM_LOG) << "KAlarm::setRtcWakeTime: Error code=" << job->error() << errmsg; if (errmsg.isEmpty()) { int errcode = job->error(); switch (errcode) { case KAuth::ActionReply::AuthorizationDeniedError: case KAuth::ActionReply::UserCancelledError: qCDebug(KALARM_LOG) << "KAlarm::setRtcWakeTime: Authorization error:" << errcode; return false; // the user should already know about this default: break; } errmsg = i18nc("@info", "Error obtaining authorization (%1)", errcode); } KAMessageBox::information(parent, errmsg); return false; } return true; } } // namespace KAlarm namespace { /****************************************************************************** * Create a new template. * 'preset' is non-null to base it on an existing event or template; otherwise, * the alarm type is set to 'type'. */ void editNewTemplate(EditAlarmDlg::Type type, const KAEvent* preset, QWidget* parent) { if (Resources::enabledResources(CalEvent::TEMPLATE, true).isEmpty()) { KAMessageBox::sorry(parent, i18nc("@info", "You must enable a template calendar to save the template in")); return; } // Use AutoQPointer to guard against crash on application exit while // the dialogue is still open. It prevents double deletion (both on // deletion of parent, and on return from this function). AutoQPointer editDlg; if (preset) editDlg = EditAlarmDlg::create(true, preset, true, parent); else editDlg = EditAlarmDlg::create(true, type, parent); if (editDlg->exec() == QDialog::Accepted) { KAEvent event; Resource resource; editDlg->getEvent(event, resource); // Add the template to the displayed lists and to the calendar file KAlarm::addTemplate(event, &resource, editDlg); Undo::saveAdd(event, resource); } } } // namespace namespace KAlarm { /****************************************************************************** * Open the Edit Alarm dialog to edit the specified alarm. * If the alarm is read-only or archived, the dialog is opened read-only. */ void editAlarm(KAEvent* event, QWidget* parent) { if (event->expired() || ResourcesCalendar::instance()->eventReadOnly(event->id())) { viewAlarm(event, parent); return; } const EventId id(*event); // Use AutoQPointer to guard against crash on application exit while // the dialogue is still open. It prevents double deletion (both on // deletion of parent, and on return from this function). AutoQPointer editDlg = EditAlarmDlg::create(false, event, false, parent, EditAlarmDlg::RES_USE_EVENT_ID); if (editDlg->exec() == QDialog::Accepted) { if (!ResourcesCalendar::instance()->event(id)) { // Event has been deleted while the user was editing the alarm, // so treat it as a new alarm. PrivateNewAlarmDlg().accept(editDlg); return; } KAEvent newEvent; Resource resource; bool changeDeferral = !editDlg->getEvent(newEvent, resource); // Update the event in the displays and in the calendar file const Undo::Event undo(*event, resource); if (changeDeferral) { // The only change has been to an existing deferral if (updateEvent(newEvent, editDlg, true) != UPDATE_OK) // keep the same event ID return; // failed to save event } else { const UpdateResult status = modifyEvent(*event, newEvent, editDlg); if (status.status != UPDATE_OK && status.status <= UPDATE_KORG_ERR) displayKOrgUpdateError(editDlg, ERR_MODIFY, status); } Undo::saveEdit(undo, newEvent); outputAlarmWarnings(editDlg, &newEvent); } } /****************************************************************************** * Display the alarm edit dialog to edit the alarm with the specified ID. * An error occurs if the alarm is not found, if there is more than one alarm * with the same ID, or if it is read-only or expired. */ bool editAlarmById(const EventId& id, QWidget* parent) { const QString eventID(id.eventId()); KAEvent* event = ResourcesCalendar::instance()->event(id, true); if (!event) { if (id.resourceId() != -1) qCWarning(KALARM_LOG) << "KAlarm::editAlarmById: Event ID not found, or duplicated:" << eventID; else qCWarning(KALARM_LOG) << "KAlarm::editAlarmById: Event ID not found:" << eventID; return false; } if (ResourcesCalendar::instance()->eventReadOnly(event->id())) { qCCritical(KALARM_LOG) << "KAlarm::editAlarmById:" << eventID << ": read-only"; return false; } switch (event->category()) { case CalEvent::ACTIVE: case CalEvent::TEMPLATE: break; default: qCCritical(KALARM_LOG) << "KAlarm::editAlarmById:" << eventID << ": event not active or template"; return false; } editAlarm(event, parent); return true; } /****************************************************************************** * Open the Edit Alarm dialog to edit the specified template. * If the template is read-only, the dialog is opened read-only. */ void editTemplate(KAEvent* event, QWidget* parent) { if (ResourcesCalendar::instance()->eventReadOnly(event->id())) { // The template is read-only, so make the dialogue read-only. // Use AutoQPointer to guard against crash on application exit while // the dialogue is still open. It prevents double deletion (both on // deletion of parent, and on return from this function). AutoQPointer editDlg = EditAlarmDlg::create(true, event, false, parent, EditAlarmDlg::RES_PROMPT, true); editDlg->exec(); return; } // Use AutoQPointer to guard against crash on application exit while // the dialogue is still open. It prevents double deletion (both on // deletion of parent, and on return from this function). AutoQPointer editDlg = EditAlarmDlg::create(true, event, false, parent, EditAlarmDlg::RES_USE_EVENT_ID); if (editDlg->exec() == QDialog::Accepted) { KAEvent newEvent; Resource resource; editDlg->getEvent(newEvent, resource); const QString id = event->id(); newEvent.setEventId(id); newEvent.setResourceId(event->resourceId()); // Update the event in the displays and in the calendar file const Undo::Event undo(*event, resource); updateTemplate(newEvent, editDlg); Undo::saveEdit(undo, newEvent); } } /****************************************************************************** * Open the Edit Alarm dialog to view the specified alarm (read-only). */ void viewAlarm(const KAEvent* event, QWidget* parent) { // Use AutoQPointer to guard against crash on application exit while // the dialogue is still open. It prevents double deletion (both on // deletion of parent, and on return from this function). AutoQPointer editDlg = EditAlarmDlg::create(false, event, false, parent, EditAlarmDlg::RES_PROMPT, true); editDlg->exec(); } /****************************************************************************** * Called when OK is clicked in the alarm edit dialog invoked by the Edit button * in an alarm message window. * Updates the alarm calendar and closes the dialog. */ void updateEditedAlarm(EditAlarmDlg* editDlg, KAEvent& event, Resource& resource) { qCDebug(KALARM_LOG) << "KAlarm::updateEditedAlarm"; KAEvent newEvent; Resource res; editDlg->getEvent(newEvent, res); // Update the displayed lists and the calendar file UpdateResult status; if (ResourcesCalendar::instance()->event(EventId(event))) { // The old alarm hasn't expired yet, so replace it const Undo::Event undo(event, resource); status = modifyEvent(event, newEvent, editDlg); Undo::saveEdit(undo, newEvent); } else { // The old event has expired, so simply create a new one status = addEvent(newEvent, &resource, editDlg); Undo::saveAdd(newEvent, resource); } if (status.status != UPDATE_OK && status.status <= UPDATE_KORG_ERR) displayKOrgUpdateError(editDlg, ERR_MODIFY, status); outputAlarmWarnings(editDlg, &newEvent); editDlg->close(); } /****************************************************************************** * Returns a list of all alarm templates. * If shell commands are disabled, command alarm templates are omitted. */ KAEvent::List templateList() { KAEvent::List templates; const bool includeCmdAlarms = ShellProcess::authorised(); const KAEvent::List events = ResourcesCalendar::instance()->events(CalEvent::TEMPLATE); for (KAEvent* event : events) { if (includeCmdAlarms || !(event->actionTypes() & KAEvent::ACT_COMMAND)) templates.append(event); } return templates; } /****************************************************************************** * To be called after an alarm has been edited. * Prompt the user to re-enable alarms if they are currently disabled, and if * it's an email alarm, warn if no 'From' email address is configured. */ void outputAlarmWarnings(QWidget* parent, const KAEvent* event) { if (event && event->actionTypes() == KAEvent::ACT_EMAIL && Preferences::emailAddress().isEmpty()) KAMessageBox::information(parent, xi18nc("@info Please set the 'From' email address...", "%1Please set it in the Configuration dialog.", KAMail::i18n_NeedFromEmailAddress())); if (!theApp()->alarmsEnabled()) { if (KAMessageBox::warningYesNo(parent, xi18nc("@info", "Alarms are currently disabled.Do you want to enable alarms now?"), QString(), KGuiItem(i18nc("@action:button", "Enable")), KGuiItem(i18nc("@action:button", "Keep Disabled")), QStringLiteral("EditEnableAlarms")) == KMessageBox::Yes) theApp()->setAlarmsEnabled(true); } } /****************************************************************************** * Reload the calendar. */ void refreshAlarms() { qCDebug(KALARM_LOG) << "KAlarm::refreshAlarms"; if (!refreshAlarmsQueued) { refreshAlarmsQueued = true; theApp()->processQueue(); } } /****************************************************************************** * This method must only be called from the main KAlarm queue processing loop, * to prevent asynchronous calendar operations interfering with one another. * * If refreshAlarms() has been called, reload the calendars. */ void refreshAlarmsIfQueued() { if (refreshAlarmsQueued) { qCDebug(KALARM_LOG) << "KAlarm::refreshAlarmsIfQueued"; ResourcesCalendar::instance()->reload(); // Close any message windows for alarms which are now disabled const KAEvent::List events = ResourcesCalendar::instance()->events(CalEvent::ACTIVE); for (KAEvent* event : events) { if (!event->enabled() && (event->actionTypes() & KAEvent::ACT_DISPLAY)) { MessageWin* win = MessageWin::findEvent(EventId(*event)); delete win; } } MainWindow::refresh(); refreshAlarmsQueued = false; } } /****************************************************************************** * Start KMail if it isn't already running, optionally minimised. * Reply = reason for failure to run KMail * = null string if success. */ QString runKMail() { const QDBusReply reply = QDBusConnection::sessionBus().interface()->isServiceRegistered(KMAIL_DBUS_SERVICE); if (!reply.isValid() || !reply.value()) { // Program is not already running, so start it const QDBusReply startReply = QDBusConnection::sessionBus().interface()->startService(KMAIL_DBUS_SERVICE); if (!startReply.isValid()) { const QString errmsg = startReply.error().message(); qCCritical(KALARM_LOG) << "Couldn't start KMail (" << errmsg << ")"; return xi18nc("@info", "Unable to start KMail(%1)", errmsg); } } return QString(); } /****************************************************************************** * The "Don't show again" option for error messages is personal to the user on a * particular computer. For example, he may want to inhibit error messages only * on his laptop. So the status is not stored in the alarm calendar, but in the * user's local KAlarm data directory. ******************************************************************************/ /****************************************************************************** * Return the Don't-show-again error message tags set for a specified alarm ID. */ QStringList dontShowErrors(const EventId& eventId) { if (eventId.isEmpty()) return QStringList(); KConfig config(QStandardPaths::writableLocation(QStandardPaths::DataLocation) + QLatin1Char('/') + ALARM_OPTS_FILE); KConfigGroup group(&config, DONT_SHOW_ERRORS_GROUP); const QString id = QStringLiteral("%1:%2").arg(eventId.resourceId()).arg(eventId.eventId()); return group.readEntry(id, QStringList()); } /****************************************************************************** * Check whether the specified Don't-show-again error message tag is set for an * alarm ID. */ bool dontShowErrors(const EventId& eventId, const QString& tag) { if (tag.isEmpty()) return false; const QStringList tags = dontShowErrors(eventId); return tags.indexOf(tag) >= 0; } /****************************************************************************** * Reset the Don't-show-again error message tags for an alarm ID. * If 'tags' is empty, the config entry is deleted. */ void setDontShowErrors(const EventId& eventId, const QStringList& tags) { if (eventId.isEmpty()) return; KConfig config(QStandardPaths::writableLocation(QStandardPaths::DataLocation) + QLatin1Char('/') + ALARM_OPTS_FILE); KConfigGroup group(&config, DONT_SHOW_ERRORS_GROUP); const QString id = QStringLiteral("%1:%2").arg(eventId.resourceId()).arg(eventId.eventId()); if (tags.isEmpty()) group.deleteEntry(id); else group.writeEntry(id, tags); group.sync(); } /****************************************************************************** * Set the specified Don't-show-again error message tag for an alarm ID. * Existing tags are unaffected. */ void setDontShowErrors(const EventId& eventId, const QString& tag) { if (eventId.isEmpty() || tag.isEmpty()) return; KConfig config(QStandardPaths::writableLocation(QStandardPaths::DataLocation) + QLatin1Char('/') + ALARM_OPTS_FILE); KConfigGroup group(&config, DONT_SHOW_ERRORS_GROUP); const QString id = QStringLiteral("%1:%2").arg(eventId.resourceId()).arg(eventId.eventId()); QStringList tags = group.readEntry(id, QStringList()); if (tags.indexOf(tag) < 0) { tags += tag; group.writeEntry(id, tags); group.sync(); } } #ifndef NDEBUG /****************************************************************************** * Set up KAlarm test conditions based on environment variables. * KALARM_TIME: specifies current system time (format [[[yyyy-]mm-]dd-]hh:mm [TZ]). */ void setTestModeConditions() { const QByteArray newTime = qgetenv("KALARM_TIME"); if (!newTime.isEmpty()) { KADateTime dt; if (AlarmTime::convertTimeString(newTime, dt, KADateTime::realCurrentLocalDateTime(), true)) setSimulatedSystemTime(dt); } } /****************************************************************************** * Set the simulated system time. */ void setSimulatedSystemTime(const KADateTime& dt) { KADateTime::setSimulatedSystemTime(dt); qCDebug(KALARM_LOG) << "New time =" << qPrintable(KADateTime::currentLocalDateTime().toString(QStringLiteral("%Y-%m-%d %H:%M %:Z"))); } #endif } // namespace KAlarm namespace { /****************************************************************************** * Display an error message about an error when saving an event. */ void displayUpdateError(QWidget* parent, KAlarm::UpdateError code, const UpdateStatusData& status, bool showKOrgError) { QString errmsg; if (status.status.status > KAlarm::UPDATE_KORG_ERR) { switch (code) { case KAlarm::ERR_ADD: case KAlarm::ERR_MODIFY: errmsg = (status.warnErr > 1) ? i18nc("@info", "Error saving alarms") : i18nc("@info", "Error saving alarm"); break; case KAlarm::ERR_DELETE: errmsg = (status.warnErr > 1) ? i18nc("@info", "Error deleting alarms") : i18nc("@info", "Error deleting alarm"); break; case KAlarm::ERR_REACTIVATE: errmsg = (status.warnErr > 1) ? i18nc("@info", "Error saving reactivated alarms") : i18nc("@info", "Error saving reactivated alarm"); break; case KAlarm::ERR_TEMPLATE: errmsg = (status.warnErr > 1) ? i18nc("@info", "Error saving alarm templates") : i18nc("@info", "Error saving alarm template"); break; } KAMessageBox::error(parent, errmsg); } else if (showKOrgError) displayKOrgUpdateError(parent, code, status.status, status.warnKOrg); } /****************************************************************************** * Tell KOrganizer to put an alarm in its calendar. * It will be held by KOrganizer as a simple event, without alarms - KAlarm * is still responsible for alarming. */ KAlarm::UpdateResult sendToKOrganizer(const KAEvent& event) { Event::Ptr kcalEvent(new KCalendarCore::Event); event.updateKCalEvent(kcalEvent, KAEvent::UID_IGNORE); // Change the event ID to avoid duplicating the same unique ID as the original event const QString uid = uidKOrganizer(event.id()); kcalEvent->setUid(uid); kcalEvent->clearAlarms(); QString userEmail; switch (event.actionTypes()) { case KAEvent::ACT_DISPLAY: case KAEvent::ACT_COMMAND: case KAEvent::ACT_DISPLAY_COMMAND: kcalEvent->setSummary(event.cleanText()); userEmail = Preferences::emailAddress(); break; case KAEvent::ACT_EMAIL: { const QString from = event.emailFromId() ? Identities::identityManager()->identityForUoid(event.emailFromId()).fullEmailAddr() : Preferences::emailAddress(); AlarmText atext; atext.setEmail(event.emailAddresses(QStringLiteral(", ")), from, QString(), QString(), event.emailSubject(), QString()); kcalEvent->setSummary(atext.displayText()); userEmail = from; break; } case KAEvent::ACT_AUDIO: kcalEvent->setSummary(event.audioFile()); break; default: break; } const Person person(QString(), userEmail); kcalEvent->setOrganizer(person); kcalEvent->setDuration(Duration(Preferences::kOrgEventDuration() * 60, Duration::Seconds)); // Translate the event into string format ICalFormat format; format.setTimeZone(Preferences::timeSpecAsZone()); const QString iCal = format.toICalString(kcalEvent); // Send the event to KOrganizer KAlarm::UpdateResult status = runKOrganizer(); // start KOrganizer if it isn't already running, and create its D-Bus interface if (status != KAlarm::UPDATE_OK) return status; QDBusInterface korgInterface(KORG_DBUS_SERVICE, QStringLiteral(KORG_DBUS_PATH), KORG_DBUS_IFACE); const QList args{iCal}; QDBusReply reply = korgInterface.callWithArgumentList(QDBus::Block, QStringLiteral("addIncidence"), args); if (!reply.isValid()) { if (reply.error().type() == QDBusError::UnknownObject) { status = KAlarm::UPDATE_KORG_ERRSTART; qCCritical(KALARM_LOG) << "KAlarm::sendToKOrganizer: addIncidence() D-Bus error: still starting"; } else { status.set(KAlarm::UPDATE_KORG_ERR, reply.error().message()); qCCritical(KALARM_LOG) << "KAlarm::sendToKOrganizer: addIncidence(" << uid << ") D-Bus call failed:" << status.message; } } else if (!reply.value()) { status = KAlarm::UPDATE_KORG_FUNCERR; qCDebug(KALARM_LOG) << "KAlarm::sendToKOrganizer: addIncidence(" << uid << ") D-Bus call returned false"; } else qCDebug(KALARM_LOG) << "KAlarm::sendToKOrganizer:" << uid << ": success"; return status; } /****************************************************************************** * Tell KOrganizer to delete an event from its calendar. */ KAlarm::UpdateResult deleteFromKOrganizer(const QString& eventID) { const QString newID = uidKOrganizer(eventID); new AkonadiCollectionSearch(KORG_MIME_TYPE, QString(), newID, true); // this auto-deletes when complete // Ignore errors return KAlarm::UpdateResult(KAlarm::UPDATE_OK); } /****************************************************************************** * Start KOrganizer if not already running, and create its D-Bus interface. */ KAlarm::UpdateResult runKOrganizer() { KAlarm::UpdateResult status; // If Kontact is running, there is a load() method which needs to be called to // load KOrganizer into Kontact. But if KOrganizer is running independently, // the load() method doesn't exist. This call starts korganizer if needed, too. QDBusInterface iface(KORG_DBUS_SERVICE, QStringLiteral(KORG_DBUS_LOAD_PATH), QStringLiteral("org.kde.PIMUniqueApplication")); QDBusReply reply = iface.call(QStringLiteral("load")); if ((!reply.isValid() || !reply.value()) && iface.lastError().type() != QDBusError::UnknownMethod) { status.set(KAlarm::UPDATE_KORG_ERR, iface.lastError().message()); qCWarning(KALARM_LOG) << "Loading KOrganizer failed:" << status.message; return status; } return status; } /****************************************************************************** * Insert a KOrganizer string after the hyphen in the supplied event ID. */ QString uidKOrganizer(const QString& id) { if (id.startsWith(KORGANIZER_UID)) return id; QString result = id; return result.insert(0, KORGANIZER_UID); } -/****************************************************************************** -* Find the version of KAlarm which wrote the calendar file, and do any -* necessary conversions to the current format. -*/ -bool updateCalendarFormat(const FileStorage::Ptr& fileStorage) -{ - QString versionString; - int version = KACalendar::updateVersion(fileStorage, versionString); - if (version == KACalendar::IncompatibleFormat) - return false; // calendar was created by another program, or an unknown version of KAlarm - return true; -} - -/****************************************************************************** -* Import alarms from a calendar file. The alarms are converted to the current -* KAlarm format and are given new unique event IDs. -* Parameters: parent: parent widget for error message boxes -* alarmList: imported alarms are added to this list -*/ -bool importCalendarFile(const QUrl& url, CalEvent::Types alarmTypes, QWidget* parent, QHash>& alarmList) -{ - if (!url.isValid()) - { - qCDebug(KALARM_LOG) << "KAlarm::importCalendarFile: Invalid URL"; - return false; - } - - // If the URL is remote, download it into a temporary local file. - QString filename; - bool local = url.isLocalFile(); - if (local) - { - filename = url.toLocalFile(); - if (!QFile::exists(filename)) - { - qCDebug(KALARM_LOG) << "KAlarm::importCalendarFile:" << 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) << "KAlarm::importCalendarFile: 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) << "KAlarm::importCalendarFile: --- 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)); - bool success = calStorage->load(); - if (!local) - QFile::remove(filename); - if (!success) - { - qCDebug(KALARM_LOG) << "KAlarm::importCalendarFile: Error loading calendar '" << filename <<"'"; - KAMessageBox::error(parent, xi18nc("@info", "Could not load calendar %1.", url.toDisplayString())); - return false; - } - - const bool currentFormat = updateCalendarFormat(calStorage); - 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 (!currentFormat) - type = CalEvent::ACTIVE; - } - if (!(type & alarmTypes)) - continue; - - 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 list. - newev->setUid(CalEvent::uid(CalFormat::createUniqueId(), type)); - alarmList[type] += KAEvent(newev); - } - return true; -} - } // 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 f0fa0d2a..ceaef137 100644 --- a/src/kalarmapp.cpp +++ b/src/kalarmapp.cpp @@ -1,2699 +1,2699 @@ /* * kalarmapp.cpp - the KAlarm application object * Program: kalarm * Copyright © 2001-2020 David Jarvie * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kalarmapp.h" #include "alarmcalendar.h" #include "alarmtime.h" #include "commandoptions.h" #include "dbushandler.h" #include "editdlgtypes.h" #include "functions.h" #include "kamail.h" #include "mainwindow.h" #include "messagewin.h" #include "kalarmmigrateapplication.h" #include "preferences.h" #include "prefdlg.h" #include "startdaytimer.h" #include "traywindow.h" #include "resources/datamodel.h" #include "resources/resources.h" #include "resources/eventmodel.h" #include "lib/desktop.h" #include "lib/messagebox.h" #include "lib/shellprocess.h" #include "kalarm_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include -static const int AKONADI_TIMEOUT = 30; // timeout (seconds) for Akonadi collections to be populated +static const int RESOURCES_TIMEOUT = 30; // timeout (seconds) for resources to be populated /****************************************************************************** * Find the maximum number of seconds late which a late-cancel alarm is allowed * to be. This is calculated as the late cancel interval, plus a few seconds * leeway to cater for any timing irregularities. */ static inline int maxLateness(int lateCancel) { static const int LATENESS_LEEWAY = 5; int lc = (lateCancel >= 1) ? (lateCancel - 1)*60 : 0; return LATENESS_LEEWAY + lc; } KAlarmApp* KAlarmApp::mInstance = nullptr; int KAlarmApp::mActiveCount = 0; int KAlarmApp::mFatalError = 0; QString KAlarmApp::mFatalMessage; /****************************************************************************** * Construct the application. */ KAlarmApp::KAlarmApp(int& argc, char** argv) : QApplication(argc, argv) , mDBusHandler(new DBusHandler()) { qCDebug(KALARM_LOG) << "KAlarmApp:"; KAlarmMigrateApplication migrate; migrate.migrate(); #ifndef NDEBUG KAlarm::setTestModeConditions(); #endif setQuitOnLastWindowClosed(false); Preferences::self(); // read KAlarm configuration if (!Preferences::noAutoStart()) { // Strip out any "OnlyShowIn=KDE" list from kalarm.autostart.desktop Preferences::setNoAutoStart(false); // Enable kalarm.autostart.desktop to start KAlarm Preferences::setAutoStart(true); Preferences::self()->save(); } Preferences::connect(SIGNAL(startOfDayChanged(QTime)), this, SLOT(changeStartOfDay())); Preferences::connect(SIGNAL(workTimeChanged(QTime,QTime,QBitArray)), this, SLOT(slotWorkTimeChanged(QTime,QTime,QBitArray))); Preferences::connect(SIGNAL(holidaysChanged(KHolidays::HolidayRegion)), this, SLOT(slotHolidaysChanged(KHolidays::HolidayRegion))); Preferences::connect(SIGNAL(feb29TypeChanged(Feb29Type)), this, SLOT(slotFeb29TypeChanged(Feb29Type))); Preferences::connect(SIGNAL(showInSystemTrayChanged(bool)), this, SLOT(slotShowInSystemTrayChanged())); Preferences::connect(SIGNAL(archivedKeepDaysChanged(int)), this, SLOT(setArchivePurgeDays())); Preferences::connect(SIGNAL(messageFontChanged(QFont)), this, SLOT(slotMessageFontChanged(QFont))); slotFeb29TypeChanged(Preferences::defaultFeb29Type()); KAEvent::setStartOfDay(Preferences::startOfDay()); KAEvent::setWorkTime(Preferences::workDays(), Preferences::workDayStart(), Preferences::workDayEnd()); KAEvent::setHolidays(Preferences::holidays()); KAEvent::setDefaultFont(Preferences::messageFont()); // Check if KOrganizer is installed const QString korg = QStringLiteral("korganizer"); mKOrganizerEnabled = !QStandardPaths::findExecutable(korg).isEmpty(); if (!mKOrganizerEnabled) { qCDebug(KALARM_LOG) << "KAlarmApp: KOrganizer options disabled (KOrganizer not found)"; } // Check if the window manager can't handle keyboard focus transfer between windows mWindowFocusBroken = (Desktop::currentIdentity() == Desktop::Unity); if (mWindowFocusBroken) { qCDebug(KALARM_LOG) << "KAlarmApp: Window keyboard focus broken"; } } /****************************************************************************** */ KAlarmApp::~KAlarmApp() { while (!mCommandProcesses.isEmpty()) { ProcData* pd = mCommandProcesses.at(0); mCommandProcesses.pop_front(); delete pd; } ResourcesCalendar::terminate(); DisplayCalendar::terminate(); DataModel::terminate(); } /****************************************************************************** * Return the one and only KAlarmApp instance. * If it doesn't already exist, it is created first. */ KAlarmApp* KAlarmApp::create(int& argc, char** argv) { if (!mInstance) { mInstance = new KAlarmApp(argc, argv); if (mFatalError) mInstance->quitFatal(); } return mInstance; } /****************************************************************************** * Perform initialisations which may require the constructor to have completed * and KAboutData to have been set up. */ void KAlarmApp::initialise() { if (initialiseTimerResources()) // initialise calendars and alarm timer { Resources* resources = Resources::instance(); connect(resources, &Resources::resourceAdded, this, &KAlarmApp::slotResourceAdded); connect(resources, &Resources::resourcePopulated, this, &KAlarmApp::slotResourcePopulated); connect(resources, &Resources::resourcePopulated, this, &KAlarmApp::purgeNewArchivedDefault); connect(resources, &Resources::resourcesCreated, this, &KAlarmApp::slotResourcesCreated); connect(resources, &Resources::migrationCompleted, this, &KAlarmApp::checkWritableCalendar); connect(resources, &Resources::resourcesPopulated, this, &KAlarmApp::processQueue); KConfigGroup config(KSharedConfig::openConfig(), "General"); mNoSystemTray = config.readEntry("NoSystemTray", false); mOldShowInSystemTray = wantShowInSystemTray(); DateTime::setStartOfDay(Preferences::startOfDay()); mPrefsArchivedColour = Preferences::archivedColour(); } } /****************************************************************************** * Initialise or reinitialise things which are tidied up/closed by quitIf(). * Reinitialisation can be necessary if session restoration finds nothing to * restore and starts quitting the application, but KAlarm then starts up again * before the application has exited. * Reply = true if calendars were initialised successfully, * false if they were already initialised, or if initialisation failed. */ bool KAlarmApp::initialiseTimerResources() { if (!mAlarmTimer) { mAlarmTimer = new QTimer(this); mAlarmTimer->setSingleShot(true); connect(mAlarmTimer, &QTimer::timeout, this, &KAlarmApp::checkNextDueAlarm); } if (!ResourcesCalendar::instance()) { qCDebug(KALARM_LOG) << "KAlarmApp::initialise: initialising calendars"; DataModel::initialise(); ResourcesCalendar::initialise(); DisplayCalendar::initialise(); connect(ResourcesCalendar::instance(), &ResourcesCalendar::earliestAlarmChanged, this, &KAlarmApp::checkNextDueAlarm); connect(ResourcesCalendar::instance(), &ResourcesCalendar::atLoginEventAdded, this, &KAlarmApp::atLoginEventAdded); return true; } return false; } /****************************************************************************** * Restore the saved session if required. */ bool KAlarmApp::restoreSession() { if (!isSessionRestored()) return false; if (mFatalError) { quitFatal(); return false; } // Process is being restored by session management. qCDebug(KALARM_LOG) << "KAlarmApp::restoreSession: Restoring"; ++mActiveCount; // Create the session config object now. // This is necessary since if initCheck() below causes calendars to be updated, // the session config created after that points to an invalid file, resulting // in no windows being restored followed by a later crash. KConfigGui::sessionConfig(); // When KAlarm is session restored, automatically set start-at-login to true. Preferences::self()->load(); Preferences::setAutoStart(true); Preferences::setNoAutoStart(false); Preferences::setAskAutoStart(true); // cancel any start-at-login prompt suppression Preferences::self()->save(); if (!initCheck(true)) // open the calendar file (needed for main windows), don't process queue yet { --mActiveCount; quitIf(1, true); // error opening the main calendar - quit return false; } MainWindow* trayParent = nullptr; for (int i = 1; KMainWindow::canBeRestored(i); ++i) { const QString type = KMainWindow::classNameOfToplevel(i); if (type == QLatin1String("MainWindow")) { MainWindow* win = MainWindow::create(true); win->restore(i, false); if (win->isHiddenTrayParent()) trayParent = win; else win->show(); } else if (type == QLatin1String("MessageWin")) { MessageWin* win = new MessageWin; win->restore(i, false); if (win->isValid()) { if (Resources::allCreated()) win->show(); } else delete win; } } // Try to display the system tray icon if it is configured to be shown if (trayParent || wantShowInSystemTray()) { if (!MainWindow::count()) qCWarning(KALARM_LOG) << "KAlarmApp::restoreSession: no main window to be restored!?"; else { displayTrayIcon(true, trayParent); // Occasionally for no obvious reason, the main main window is // shown when it should be hidden, so hide it just to be sure. if (trayParent) trayParent->hide(); } } --mActiveCount; if (quitIf(0)) // quit if no windows are open return false; // quitIf() can sometimes return, despite calling exit() startProcessQueue(); // start processing the execution queue return true; } /****************************************************************************** * Called for a unique QApplication when a new instance of the application is * started. * Reply: exit code (>= 0), or -1 to continue execution. * If exit code >= 0, 'outputText' holds text to output before terminating. */ void KAlarmApp::activateByDBus(const QStringList& args, const QString& workingDirectory) { activateInstance(args, workingDirectory, nullptr); } /****************************************************************************** * Called to start a new instance of the application. * Reply: exit code (>= 0), or -1 to continue execution. * If exit code >= 0, 'outputText' holds text to output before terminating. */ int KAlarmApp::activateInstance(const QStringList& args, const QString& workingDirectory, QString* outputText) { Q_UNUSED(workingDirectory) qCDebug(KALARM_LOG) << "KAlarmApp::activateInstance"; if (outputText) outputText->clear(); if (mFatalError) { quitFatal(); return 1; } // The D-Bus call to activate a subsequent instance of KAlarm may not supply // any arguments, but we need one. if (!args.isEmpty() && mActivateArg0.isEmpty()) mActivateArg0 = args[0]; QStringList fixedArgs(args); if (args.isEmpty() && !mActivateArg0.isEmpty()) fixedArgs << mActivateArg0; // Parse and interpret command line arguments. QCommandLineParser parser; KAboutData::applicationData().setupCommandLine(&parser); parser.setApplicationDescription(QApplication::applicationDisplayName()); CommandOptions* options = new CommandOptions; const QStringList newArgs = options->setOptions(&parser, fixedArgs); options->parse(); KAboutData::applicationData().processCommandLine(&parser); ++mActiveCount; int exitCode = 0; // default = success static bool firstInstance = true; bool dontRedisplay = false; CommandOptions::Command command = CommandOptions::NONE; const bool processOptions = (!firstInstance || !isSessionRestored()); if (processOptions) { options->process(); #ifndef NDEBUG if (options->simulationTime().isValid()) KAlarm::setSimulatedSystemTime(options->simulationTime()); #endif command = options->command(); if (options->disableAll()) setAlarmsEnabled(false); // disable alarm monitoring // Handle options which exit with a terminal message, before // making the application a unique application, since a // unique application won't output to the terminal if another // instance is already running. switch (command) { case CommandOptions::CMD_ERROR: if (outputText) { *outputText = options->outputText(); delete options; return 1; } mReadOnly = true; // don't need write access to calendars exitCode = 1; break; default: break; } } // Make this a unique application. KDBusService* s = new KDBusService(KDBusService::Unique, this); connect(this, &KAlarmApp::aboutToQuit, s, &KDBusService::deleteLater); connect(s, &KDBusService::activateRequested, this, &KAlarmApp::activateByDBus); if (processOptions) { switch (command) { case CommandOptions::TRIGGER_EVENT: case CommandOptions::CANCEL_EVENT: { // Display or delete the event with the specified event ID const QueuedAction action = static_cast(int((command == CommandOptions::TRIGGER_EVENT) ? QueuedAction::Trigger : QueuedAction::Cancel) | int(QueuedAction::FindId) | int(QueuedAction::Exit)); // Open the calendar, don't start processing execution queue yet, // and wait for the calendar resources to be populated. if (!initCheck(true)) exitCode = 1; else { mCommandOption = options->commandName(); mActionQueue.enqueue(ActionQEntry(action, options->eventId())); startProcessQueue(); // start processing the execution queue dontRedisplay = true; } break; } case CommandOptions::LIST: // Output a list of scheduled alarms to stdout. // Open the calendar, don't start processing execution queue yet, // and wait for all calendar resources to be populated. mReadOnly = true; // don't need write access to calendars mAlarmsEnabled = false; // prevent alarms being processed if (!initCheck(true)) exitCode = 1; else { const QueuedAction action = static_cast(int(QueuedAction::List) | int(QueuedAction::Exit)); mActionQueue.enqueue(ActionQEntry(action, EventId())); startProcessQueue(); // start processing the execution queue dontRedisplay = true; } break; case CommandOptions::EDIT: // Edit a specified existing alarm. // Open the calendar and wait for the calendar resources to be populated. if (!initCheck(false)) exitCode = 1; else { mCommandOption = options->commandName(); if (firstInstance) mEditingCmdLineAlarm = 0x10; // want to redisplay alarms if successful mActionQueue.enqueue(ActionQEntry(QueuedAction::Edit, options->eventId())); startProcessQueue(); // start processing the execution queue dontRedisplay = true; } break; case CommandOptions::EDIT_NEW: { // Edit a new alarm, and optionally preset selected values if (!initCheck()) exitCode = 1; else { EditAlarmDlg* editDlg = EditAlarmDlg::create(false, options->editType(), MainWindow::mainMainWindow()); if (options->alarmTime().isValid()) editDlg->setTime(options->alarmTime()); if (options->recurrence()) editDlg->setRecurrence(*options->recurrence(), options->subRepeatInterval(), options->subRepeatCount()); else if (options->flags() & KAEvent::REPEAT_AT_LOGIN) editDlg->setRepeatAtLogin(); editDlg->setAction(options->editAction(), AlarmText(options->text())); if (options->lateCancel()) editDlg->setLateCancel(options->lateCancel()); if (options->flags() & KAEvent::COPY_KORGANIZER) editDlg->setShowInKOrganizer(true); switch (options->editType()) { case EditAlarmDlg::DISPLAY: { // EditAlarmDlg::create() always returns EditDisplayAlarmDlg for type = DISPLAY EditDisplayAlarmDlg* dlg = qobject_cast(editDlg); if (options->fgColour().isValid()) dlg->setFgColour(options->fgColour()); if (options->bgColour().isValid()) dlg->setBgColour(options->bgColour()); if (!options->audioFile().isEmpty() || options->flags() & (KAEvent::BEEP | KAEvent::SPEAK)) { const KAEvent::Flags flags = options->flags(); const Preferences::SoundType type = (flags & KAEvent::BEEP) ? Preferences::Sound_Beep : (flags & KAEvent::SPEAK) ? Preferences::Sound_Speak : Preferences::Sound_File; dlg->setAudio(type, options->audioFile(), options->audioVolume(), (flags & KAEvent::REPEAT_SOUND ? 0 : -1)); } if (options->reminderMinutes()) dlg->setReminder(options->reminderMinutes(), (options->flags() & KAEvent::REMINDER_ONCE)); if (options->flags() & KAEvent::CONFIRM_ACK) dlg->setConfirmAck(true); if (options->flags() & KAEvent::AUTO_CLOSE) dlg->setAutoClose(true); break; } case EditAlarmDlg::COMMAND: break; case EditAlarmDlg::EMAIL: { // EditAlarmDlg::create() always returns EditEmailAlarmDlg for type = EMAIL EditEmailAlarmDlg* dlg = qobject_cast(editDlg); if (options->fromID() || !options->addressees().isEmpty() || !options->subject().isEmpty() || !options->attachments().isEmpty()) dlg->setEmailFields(options->fromID(), options->addressees(), options->subject(), options->attachments()); if (options->flags() & KAEvent::EMAIL_BCC) dlg->setBcc(true); break; } case EditAlarmDlg::AUDIO: { // EditAlarmDlg::create() always returns EditAudioAlarmDlg for type = AUDIO EditAudioAlarmDlg* dlg = qobject_cast(editDlg); if (!options->audioFile().isEmpty() || options->audioVolume() >= 0) dlg->setAudio(options->audioFile(), options->audioVolume()); break; } case EditAlarmDlg::NO_TYPE: break; } // Execute the edit dialogue. Note that if no other instance of KAlarm is // running, this new instance will not exit after the dialogue is closed. // This is deliberate, since exiting would mean that KAlarm wouldn't // trigger the new alarm. KAlarm::execNewAlarmDlg(editDlg); createOnlyMainWindow(); // prevent the application from quitting } break; } case CommandOptions::EDIT_NEW_PRESET: // Edit a new alarm, preset with a template if (!initCheck()) exitCode = 1; else { // Execute the edit dialogue. Note that if no other instance of KAlarm is // running, this new instance will not exit after the dialogue is closed. // This is deliberate, since exiting would mean that KAlarm wouldn't // trigger the new alarm. KAlarm::editNewAlarm(options->templateName()); createOnlyMainWindow(); // prevent the application from quitting } break; case CommandOptions::NEW: // Display a message or file, execute a command, or send an email setResourcesTimeout(); // set timeout for resource initialisation if (!initCheck()) exitCode = 1; else { if (!scheduleEvent(options->editAction(), options->text(), options->alarmTime(), options->lateCancel(), options->flags(), options->bgColour(), options->fgColour(), QFont(), options->audioFile(), options->audioVolume(), options->reminderMinutes(), (options->recurrence() ? *options->recurrence() : KARecurrence()), options->subRepeatInterval(), options->subRepeatCount(), options->fromID(), options->addressees(), options->subject(), options->attachments())) exitCode = 1; else createOnlyMainWindow(); // prevent the application from quitting } break; case CommandOptions::TRAY: // Display only the system tray icon if (Preferences::showInSystemTray() && QSystemTrayIcon::isSystemTrayAvailable()) { if (!initCheck()) // open the calendar, start processing execution queue exitCode = 1; else { if (!displayTrayIcon(true)) exitCode = 1; } break; } Q_FALLTHROUGH(); // fall through to NONE case CommandOptions::NONE: // No arguments - run interactively & display the main window #ifndef NDEBUG if (options->simulationTime().isValid() && !firstInstance) break; // simulating time: don't open main window if already running #endif if (!initCheck()) exitCode = 1; else { if (mTrayWindow && mTrayWindow->assocMainWindow() && !mTrayWindow->assocMainWindow()->isVisible()) mTrayWindow->showAssocMainWindow(); else { MainWindow* win = MainWindow::create(); if (command == CommandOptions::TRAY) win->setWindowState(win->windowState() | Qt::WindowMinimized); win->show(); } } break; default: break; } } if (options != CommandOptions::firstInstance()) delete options; // If this is the first time through, redisplay any alarm message windows // from last time. if (firstInstance && !dontRedisplay && !exitCode) { /* First time through, so redisplay alarm message windows from last time. * But it is possible for session restoration in some circumstances to * not create any windows, in which case the alarm calendars will have * been deleted - if so, don't try to do anything. (This has been known * to happen under the Xfce desktop.) */ if (ResourcesCalendar::instance()) { if (Resources::allCreated()) { mRedisplayAlarms = false; MessageWin::redisplayAlarms(); } else mRedisplayAlarms = true; } } --mActiveCount; firstInstance = false; // Quit the application if this was the last/only running "instance" of the program. // Executing 'return' doesn't work very well since the program continues to // run if no windows were created. if (quitIf(exitCode >= 0 ? exitCode : 0)) return exitCode; // exit this application instance return -1; // continue executing the application instance } /****************************************************************************** * Create a minimised main window if none already exists. * This prevents the application from quitting. */ void KAlarmApp::createOnlyMainWindow() { if (!MainWindow::count()) { if (Preferences::showInSystemTray() && QSystemTrayIcon::isSystemTrayAvailable()) { if (displayTrayIcon(true)) return; } MainWindow* win = MainWindow::create(); win->setWindowState(Qt::WindowMinimized); win->show(); } } /****************************************************************************** * Quit the program, optionally only if there are no more "instances" running. * Reply = true if program exited. */ bool KAlarmApp::quitIf(int exitCode, bool force) { if (force) { // Quit regardless, except for message windows mQuitting = true; MainWindow::closeAll(); mQuitting = false; displayTrayIcon(false); if (MessageWin::instanceCount(true)) // ignore always-hidden windows (e.g. audio alarms) return false; } else if (mQuitting) return false; // MainWindow::closeAll() causes quitIf() to be called again else { // Quit only if there are no more "instances" running mPendingQuit = false; if (mActiveCount > 0 || MessageWin::instanceCount(true)) // ignore always-hidden windows (e.g. audio alarms) return false; const int mwcount = MainWindow::count(); MainWindow* mw = mwcount ? MainWindow::firstWindow() : nullptr; if (mwcount > 1 || (mwcount && (!mw->isHidden() || !mw->isTrayParent()))) return false; // There are no windows left except perhaps a main window which is a hidden // tray icon parent, or an always-hidden message window. if (mTrayWindow) { // There is a system tray icon. // Don't exit unless the system tray doesn't seem to exist. if (checkSystemTray()) return false; } if (!mActionQueue.isEmpty() || !mCommandProcesses.isEmpty()) { // Don't quit yet if there are outstanding actions on the execution queue mPendingQuit = true; mPendingQuitCode = exitCode; return false; } } // This was the last/only running "instance" of the program, so exit completely. // NOTE: Everything which is terminated/deleted here must where applicable // be initialised in the initialiseTimerResources() method, in case // KAlarm is started again before application exit completes! qCDebug(KALARM_LOG) << "KAlarmApp::quitIf:" << exitCode << ": quitting"; MessageWin::stopAudio(true); if (mCancelRtcWake) { KAlarm::setRtcWakeTime(0, nullptr); KAlarm::deleteRtcWakeConfig(); } delete mAlarmTimer; // prevent checking for alarms after deleting calendars mAlarmTimer = nullptr; mInitialised = false; // prevent processQueue() from running ResourcesCalendar::terminate(); DisplayCalendar::terminate(); DataModel::terminate(); exit(exitCode); return true; // sometimes we actually get to here, despite calling exit() } /****************************************************************************** * Called when the Quit menu item is selected. * Closes the system tray window and all main windows, but does not exit the * program if other windows are still open. */ void KAlarmApp::doQuit(QWidget* parent) { qCDebug(KALARM_LOG) << "KAlarmApp::doQuit"; if (KAMessageBox::warningCancelContinue(parent, i18nc("@info", "Quitting will disable alarms (once any alarm message windows are closed)."), QString(), KStandardGuiItem::quit(), KStandardGuiItem::cancel(), Preferences::QUIT_WARN ) != KMessageBox::Continue) return; if (!KAlarm::checkRtcWakeConfig(true).isEmpty()) { // A wake-on-suspend alarm is set if (KAMessageBox::warningCancelContinue(parent, i18nc("@info", "Quitting will cancel the scheduled Wake from Suspend."), QString(), KStandardGuiItem::quit() ) != KMessageBox::Continue) return; mCancelRtcWake = true; } if (!Preferences::autoStart()) { int option = KMessageBox::No; if (!Preferences::autoStartChangedByUser()) { option = KAMessageBox::questionYesNoCancel(parent, xi18nc("@info", "Do you want to start KAlarm at login?" "(Note that alarms will be disabled if KAlarm is not started.)"), QString(), KStandardGuiItem::yes(), KStandardGuiItem::no(), KStandardGuiItem::cancel(), Preferences::ASK_AUTO_START); } switch (option) { case KMessageBox::Yes: Preferences::setAutoStart(true); Preferences::setNoAutoStart(false); break; case KMessageBox::No: Preferences::setNoAutoStart(true); break; case KMessageBox::Cancel: default: return; } Preferences::self()->save(); } quitIf(0, true); } /****************************************************************************** * Display an error message for a fatal error. Prevent further actions since * the program state is unsafe. */ void KAlarmApp::displayFatalError(const QString& message) { if (!mFatalError) { mFatalError = 1; mFatalMessage = message; if (mInstance) QTimer::singleShot(0, mInstance, &KAlarmApp::quitFatal); } } /****************************************************************************** * Quit the program, once the fatal error message has been acknowledged. */ void KAlarmApp::quitFatal() { switch (mFatalError) { case 0: case 2: return; case 1: mFatalError = 2; KMessageBox::error(nullptr, mFatalMessage); // this is an application modal window mFatalError = 3; Q_FALLTHROUGH(); // fall through to '3' case 3: if (mInstance) mInstance->quitIf(1, true); break; } QTimer::singleShot(1000, this, &KAlarmApp::quitFatal); } /****************************************************************************** * Called by the alarm timer when the next alarm is due. * Also called when the execution queue has finished processing to check for the * next alarm. */ void KAlarmApp::checkNextDueAlarm() { if (!mAlarmsEnabled) return; // Find the first alarm due const KAEvent* nextEvent = ResourcesCalendar::instance()->earliestAlarm(); if (!nextEvent) return; // there are no alarms pending const KADateTime nextDt = nextEvent->nextTrigger(KAEvent::ALL_TRIGGER).effectiveKDateTime(); const KADateTime now = KADateTime::currentDateTime(Preferences::timeSpec()); qint64 interval = now.msecsTo(nextDt); qCDebug(KALARM_LOG) << "KAlarmApp::checkNextDueAlarm: now:" << qPrintable(now.toString(QStringLiteral("%Y-%m-%d %H:%M %:Z"))) << ", next:" << qPrintable(nextDt.toString(QStringLiteral("%Y-%m-%d %H:%M %:Z"))) << ", due:" << interval; if (interval <= 0) { // Queue the alarm queueAlarmId(*nextEvent); qCDebug(KALARM_LOG) << "KAlarmApp::checkNextDueAlarm:" << nextEvent->id() << ": due now"; QTimer::singleShot(0, this, &KAlarmApp::processQueue); } else { // No alarm is due yet, so set timer to wake us when it's due. // Check for integer overflow before setting timer. #pragma message("TODO: use hibernation wakeup signal") #ifndef HIBERNATION_SIGNAL /* TODO: REPLACE THIS CODE WHEN A SYSTEM NOTIFICATION SIGNAL BECOMES * AVAILABLE FOR WAKEUP FROM HIBERNATION. * Re-evaluate the next alarm time every minute, in case the * system clock jumps. The most common case when the clock jumps * is when a laptop wakes from hibernation. If timers were left to * run, they would trigger late by the length of time the system * was asleep. */ if (interval > 60000) // 1 minute interval = 60000; #endif ++interval; // ensure we don't trigger just before the minute boundary if (interval > INT_MAX) interval = INT_MAX; qCDebug(KALARM_LOG) << "KAlarmApp::checkNextDueAlarm:" << nextEvent->id() << "wait" << interval/1000 << "seconds"; mAlarmTimer->start(static_cast(interval)); } } /****************************************************************************** * Called by the alarm timer when the next alarm is due. * Also called when the execution queue has finished processing to check for the * next alarm. */ void KAlarmApp::queueAlarmId(const KAEvent& event) { const EventId id(event); for (const ActionQEntry& entry : qAsConst(mActionQueue)) { if (entry.action == QueuedAction::Handle && entry.eventId == id) return; // the alarm is already queued } mActionQueue.enqueue(ActionQEntry(QueuedAction::Handle, id)); } /****************************************************************************** * Start processing the execution queue. */ void KAlarmApp::startProcessQueue() { if (!mInitialised) { qCDebug(KALARM_LOG) << "KAlarmApp::startProcessQueue"; mInitialised = true; QTimer::singleShot(0, this, &KAlarmApp::processQueue); // process anything already queued } } /****************************************************************************** * The main processing loop for KAlarm. * All KAlarm operations involving opening or updating calendar files are called * from this loop to ensure that only one operation is active at any one time. * This precaution is necessary because KAlarm's activities are mostly * asynchronous, being in response to D-Bus calls from other programs or timer * events, any of which can be received in the middle of performing another * operation. If a calendar file is opened or updated while another calendar * operation is in progress, the program has been observed to hang, or the first * calendar call has failed with data loss - clearly unacceptable!! */ void KAlarmApp::processQueue() { if (mInitialised && !mProcessingQueue) { qCDebug(KALARM_LOG) << "KAlarmApp::processQueue"; mProcessingQueue = true; // Refresh alarms if that's been queued KAlarm::refreshAlarmsIfQueued(); // Process queued events while (!mActionQueue.isEmpty()) { ActionQEntry& entry = mActionQueue.head(); // Can't process the first action until its resource has been populated. const ResourceId id = entry.eventId.resourceId(); if ((id < 0 && !Resources::allPopulated()) || (id >= 0 && !Resources::resource(id).isPopulated())) { // If resource population has timed out, discard all queued events. if (mResourcesTimedOut) { qCCritical(KALARM_LOG) << "Error! Timeout reading calendars"; mActionQueue.clear(); } break; } // Process the first action in the queue. const bool findUniqueId = int(entry.action) & int(QueuedAction::FindId); const bool exitAfter = int(entry.action) & int(QueuedAction::Exit); const QueuedAction action = static_cast(int(entry.action) & int(QueuedAction::ActionMask)); bool ok = true; if (entry.eventId.isEmpty()) { // It's a new alarm switch (action) { case QueuedAction::Trigger: execAlarm(entry.event, entry.event.firstAlarm(), false); break; case QueuedAction::Handle: KAlarm::addEvent(entry.event, nullptr, nullptr, KAlarm::ALLOW_KORG_UPDATE | KAlarm::NO_RESOURCE_PROMPT); break; case QueuedAction::List: { const QStringList alarms = scheduledAlarmList(); for (const QString& alarm : alarms) std::cout << alarm.toUtf8().constData() << std::endl; break; } default: break; } } else { if (action == QueuedAction::Edit) { int editingCmdLineAlarm = mEditingCmdLineAlarm & 3; bool keepQueued = editingCmdLineAlarm <= 1; switch (editingCmdLineAlarm) { case 0: // Initiate editing an alarm specified on the command line. mEditingCmdLineAlarm |= 1; QTimer::singleShot(0, this, &KAlarmApp::slotEditAlarmById); break; case 1: // Currently editing the alarm. break; case 2: // The edit has completed. mEditingCmdLineAlarm = 0; break; default: break; } if (keepQueued) break; } else { ok = handleEvent(entry.eventId, action, findUniqueId); if (!ok && exitAfter) CommandOptions::printError(xi18nc("@info:shell", "%1: Event %2 not found, or not unique", mCommandOption, entry.eventId.eventId())); } } if (exitAfter) { mActionQueue.clear(); // ensure that quitIf() actually exits the program quitIf(ok ? 0 : 1); return; // quitIf() can sometimes return, despite calling exit() } mActionQueue.dequeue(); } // Purge the default archived alarms resource if it's time to do so if (mPurgeDaysQueued >= 0) { KAlarm::purgeArchive(mPurgeDaysQueued); mPurgeDaysQueued = -1; } // Now that the queue has been processed, quit if a quit was queued if (mPendingQuit) { if (quitIf(mPendingQuitCode)) return; // quitIf() can sometimes return, despite calling exit() } mProcessingQueue = false; if (!mEditingCmdLineAlarm) { // Schedule the application to be woken when the next alarm is due checkNextDueAlarm(); } } } /****************************************************************************** * Called when a repeat-at-login alarm has been added externally. * Queues the alarm for triggering. * First, cancel any scheduled reminder or deferral for it, since these will be * superseded by the new at-login trigger. */ void KAlarmApp::atLoginEventAdded(const KAEvent& event) { KAEvent ev = event; if (!cancelReminderAndDeferral(ev)) { if (mAlarmsEnabled) { mActionQueue.enqueue(ActionQEntry(QueuedAction::Handle, EventId(ev))); if (mInitialised) QTimer::singleShot(0, this, &KAlarmApp::processQueue); } } } /****************************************************************************** * Called when the system tray main window is closed. */ void KAlarmApp::removeWindow(TrayWindow*) { mTrayWindow = nullptr; } /****************************************************************************** * Display or close the system tray icon. */ bool KAlarmApp::displayTrayIcon(bool show, MainWindow* parent) { qCDebug(KALARM_LOG) << "KAlarmApp::displayTrayIcon"; static bool creating = false; if (show) { if (!mTrayWindow && !creating) { if (!QSystemTrayIcon::isSystemTrayAvailable()) return false; if (!MainWindow::count()) { // We have to have at least one main window to act // as parent to the system tray icon (even if the // window is hidden). creating = true; // prevent main window constructor from creating an additional tray icon parent = MainWindow::create(); creating = false; } mTrayWindow = new TrayWindow(parent ? parent : MainWindow::firstWindow()); connect(mTrayWindow, &TrayWindow::deleted, this, &KAlarmApp::trayIconToggled); Q_EMIT trayIconToggled(); if (!checkSystemTray()) quitIf(0); // exit the application if there are no open windows } } else { delete mTrayWindow; mTrayWindow = nullptr; } return true; } /****************************************************************************** * Check whether the system tray icon has been housed in the system tray. */ bool KAlarmApp::checkSystemTray() { if (!mTrayWindow) return true; if (QSystemTrayIcon::isSystemTrayAvailable() == mNoSystemTray) { qCDebug(KALARM_LOG) << "KAlarmApp::checkSystemTray: changed ->" << mNoSystemTray; mNoSystemTray = !mNoSystemTray; // Store the new setting in the config file, so that if KAlarm exits it will // restart with the correct default. KConfigGroup config(KSharedConfig::openConfig(), "General"); config.writeEntry("NoSystemTray", mNoSystemTray); config.sync(); // Update other settings slotShowInSystemTrayChanged(); } return !mNoSystemTray; } /****************************************************************************** * Return the main window associated with the system tray icon. */ MainWindow* KAlarmApp::trayMainWindow() const { return mTrayWindow ? mTrayWindow->assocMainWindow() : nullptr; } /****************************************************************************** * Called when the show-in-system-tray preference setting has changed, to show * or hide the system tray icon. */ void KAlarmApp::slotShowInSystemTrayChanged() { const bool newShowInSysTray = wantShowInSystemTray(); if (newShowInSysTray != mOldShowInSystemTray) { // The system tray run mode has changed ++mActiveCount; // prevent the application from quitting MainWindow* win = mTrayWindow ? mTrayWindow->assocMainWindow() : nullptr; delete mTrayWindow; // remove the system tray icon if it is currently shown mTrayWindow = nullptr; mOldShowInSystemTray = newShowInSysTray; if (newShowInSysTray) { // Show the system tray icon displayTrayIcon(true); } else { // Stop showing the system tray icon if (win && win->isHidden()) { if (MainWindow::count() > 1) delete win; else { win->setWindowState(win->windowState() | Qt::WindowMinimized); win->show(); } } } --mActiveCount; } } /****************************************************************************** * Called when the start-of-day time preference setting has changed. * Change alarm times for date-only alarms. */ void KAlarmApp::changeStartOfDay() { DateTime::setStartOfDay(Preferences::startOfDay()); KAEvent::setStartOfDay(Preferences::startOfDay()); ResourcesCalendar::instance()->adjustStartOfDay(); } /****************************************************************************** * Called when the default alarm message font preference setting has changed. * Notify KAEvent. */ void KAlarmApp::slotMessageFontChanged(const QFont& font) { KAEvent::setDefaultFont(font); } /****************************************************************************** * Called when the working time preference settings have changed. * Notify KAEvent. */ void KAlarmApp::slotWorkTimeChanged(const QTime& start, const QTime& end, const QBitArray& days) { KAEvent::setWorkTime(days, start, end); } /****************************************************************************** * Called when the holiday region preference setting has changed. * Notify KAEvent. */ void KAlarmApp::slotHolidaysChanged(const KHolidays::HolidayRegion& holidays) { KAEvent::setHolidays(holidays); } /****************************************************************************** * Called when the date for February 29th recurrences has changed in the * preferences settings. */ void KAlarmApp::slotFeb29TypeChanged(Preferences::Feb29Type type) { KARecurrence::Feb29Type rtype; switch (type) { default: case Preferences::Feb29_None: rtype = KARecurrence::Feb29_None; break; case Preferences::Feb29_Feb28: rtype = KARecurrence::Feb29_Feb28; break; case Preferences::Feb29_Mar1: rtype = KARecurrence::Feb29_Mar1; break; } KARecurrence::setDefaultFeb29Type(rtype); } /****************************************************************************** * Return whether the program is configured to be running in the system tray. */ bool KAlarmApp::wantShowInSystemTray() const { return Preferences::showInSystemTray() && QSystemTrayIcon::isSystemTrayAvailable(); } /****************************************************************************** * Set a timeout for populating resources. */ void KAlarmApp::setResourcesTimeout() { - QTimer::singleShot(AKONADI_TIMEOUT * 1000, this, &KAlarmApp::slotResourcesTimeout); + QTimer::singleShot(RESOURCES_TIMEOUT * 1000, this, &KAlarmApp::slotResourcesTimeout); } /****************************************************************************** * Called on a timeout to check whether resources have been populated. * If not, exit the program with code 1. */ void KAlarmApp::slotResourcesTimeout() { if (!Resources::allPopulated()) { // Resource population has timed out. mResourcesTimedOut = true; quitIf(1); } } /****************************************************************************** * Called when all resources have been created at startup. * Check whether there are any writable active calendars, and if not, warn the * user. * If alarms are being archived, check whether there is a default archived * calendar, and if not, warn the user. */ void KAlarmApp::slotResourcesCreated() { if (mRedisplayAlarms) { mRedisplayAlarms = false; MessageWin::redisplayAlarms(); } checkWritableCalendar(); checkArchivedCalendar(); } /****************************************************************************** * Called when all calendars have been fetched at startup, or calendar migration * has completed. * Check whether there are any writable active calendars, and if not, warn the * user. */ void KAlarmApp::checkWritableCalendar() { if (mReadOnly) return; // don't need write access to calendars if (!Resources::allCreated() || !DataModel::isMigrationComplete()) return; static bool done = false; if (done) return; done = true; qCDebug(KALARM_LOG) << "KAlarmApp::checkWritableCalendar"; // Check for, and remove, any duplicate resources, i.e. those which use the // same calendar file/directory. DataModel::removeDuplicateResources(); // Find whether there are any writable active alarm calendars const bool active = !Resources::enabledResources(CalEvent::ACTIVE, true).isEmpty(); if (!active) { qCWarning(KALARM_LOG) << "KAlarmApp::checkWritableCalendar: No writable active calendar"; KAMessageBox::information(MainWindow::mainMainWindow(), xi18nc("@info", "Alarms cannot be created or updated, because no writable active alarm calendar is enabled." "To fix this, use View | Show Calendars to check or change calendar statuses."), QString(), QStringLiteral("noWritableCal")); } } /****************************************************************************** * If alarms are being archived, check whether there is a default archived * calendar, and if not, warn the user. */ void KAlarmApp::checkArchivedCalendar() { static bool done = false; if (done) return; done = true; // If alarms are to be archived, check that the default archived alarm // calendar is writable. if (Preferences::archivedKeepDays()) { Resource standard = Resources::getStandard(CalEvent::ARCHIVED); if (!standard.isValid()) { // Schedule the display of a user prompt, without holding up // other processing. QTimer::singleShot(0, this, &KAlarmApp::promptArchivedCalendar); } } } /****************************************************************************** * Edit an alarm specified on the command line. */ void KAlarmApp::slotEditAlarmById() { qCDebug(KALARM_LOG) << "KAlarmApp::slotEditAlarmById"; ActionQEntry& entry = mActionQueue.head(); if (!KAlarm::editAlarmById(entry.eventId)) { CommandOptions::printError(xi18nc("@info:shell", "%1: Event %2 not found, or not editable", mCommandOption, entry.eventId.eventId())); mActionQueue.clear(); quitIf(1); } else { createOnlyMainWindow(); // prevent the application from quitting if (mEditingCmdLineAlarm & 0x10) { mRedisplayAlarms = false; MessageWin::redisplayAlarms(); } mEditingCmdLineAlarm = 2; // indicate edit completion QTimer::singleShot(0, this, &KAlarmApp::processQueue); } } /****************************************************************************** * If alarms are being archived, check whether there is a default archived * calendar, and if not, warn the user. */ void KAlarmApp::promptArchivedCalendar() { const bool archived = !Resources::enabledResources(CalEvent::ARCHIVED, true).isEmpty(); if (archived) { qCWarning(KALARM_LOG) << "KAlarmApp::checkArchivedCalendar: Archiving, but no writable archived calendar"; KAMessageBox::information(MainWindow::mainMainWindow(), xi18nc("@info", "Alarms are configured to be archived, but this is not possible because no writable archived alarm calendar is enabled." "To fix this, use View | Show Calendars to check or change calendar statuses."), QString(), QStringLiteral("noWritableArch")); } else { qCWarning(KALARM_LOG) << "KAlarmApp::checkArchivedCalendar: Archiving, but no standard archived calendar"; KAMessageBox::information(MainWindow::mainMainWindow(), xi18nc("@info", "Alarms are configured to be archived, but this is not possible because no archived alarm calendar is set as default." "To fix this, use View | Show Calendars, select an archived alarms calendar, and check Use as Default for Archived Alarms."), QString(), QStringLiteral("noStandardArch")); } } /****************************************************************************** * Called when a new resource has been added, to note the possible need to purge * its old alarms if it is the default archived calendar. */ void KAlarmApp::slotResourceAdded(const Resource& resource) { if (resource.alarmTypes() & CalEvent::ARCHIVED) mPendingPurges += resource.id(); } /****************************************************************************** * Called when a resource has been populated, to purge its old alarms if it is * the default archived calendar. */ void KAlarmApp::slotResourcePopulated(const Resource& resource) { if (mPendingPurges.removeAll(resource.id()) > 0) purgeNewArchivedDefault(resource); } /****************************************************************************** * Called when a new resource has been populated, or when a resource has been * set as the standard resource for its type. * If it is the default archived calendar, purge its old alarms if necessary. */ void KAlarmApp::purgeNewArchivedDefault(const Resource& resource) { if (Resources::isStandard(resource, CalEvent::ARCHIVED)) { qCDebug(KALARM_LOG) << "KAlarmApp::purgeNewArchivedDefault:" << resource.displayId() << ": standard archived..."; if (mArchivedPurgeDays >= 0) purge(mArchivedPurgeDays); else setArchivePurgeDays(); } } /****************************************************************************** * Called when the length of time to keep archived alarms changes in KAlarm's * preferences. * Set the number of days to keep archived alarms. * Alarms which are older are purged immediately, and at the start of each day. */ void KAlarmApp::setArchivePurgeDays() { const int newDays = Preferences::archivedKeepDays(); if (newDays != mArchivedPurgeDays) { const int oldDays = mArchivedPurgeDays; mArchivedPurgeDays = newDays; if (mArchivedPurgeDays <= 0) StartOfDayTimer::disconnect(this); if (mArchivedPurgeDays < 0) return; // keep indefinitely, so don't purge if (oldDays < 0 || mArchivedPurgeDays < oldDays) { // Alarms are now being kept for less long, so purge them purge(mArchivedPurgeDays); if (!mArchivedPurgeDays) return; // don't archive any alarms } // Start the purge timer to expire at the start of the next day // (using the user-defined start-of-day time). StartOfDayTimer::connect(this, SLOT(slotPurge())); } } /****************************************************************************** * Purge all archived events from the calendar whose end time is longer ago than * 'daysToKeep'. All events are deleted if 'daysToKeep' is zero. */ void KAlarmApp::purge(int daysToKeep) { if (mPurgeDaysQueued < 0 || daysToKeep < mPurgeDaysQueued) mPurgeDaysQueued = daysToKeep; // Do the purge once any other current operations are completed processQueue(); } /****************************************************************************** * Output a list of pending alarms, with their next scheduled occurrence. */ QStringList KAlarmApp::scheduledAlarmList() { QStringList alarms; const QVector events = KAlarm::getSortedActiveEvents(this); for (const KAEvent& event : events) { const KADateTime dateTime = event.nextTrigger(KAEvent::DISPLAY_TRIGGER).effectiveKDateTime().toLocalZone(); const Resource resource = Resources::resource(event.resourceId()); QString text(resource.configName() + QLatin1String(":")); text += event.id() + QLatin1Char(' ') + dateTime.toString(QStringLiteral("%Y%m%dT%H%M ")) + AlarmText::summary(event, 1); alarms << text; } return alarms; } /****************************************************************************** * Enable or disable alarm monitoring. */ void KAlarmApp::setAlarmsEnabled(bool enabled) { if (enabled != mAlarmsEnabled) { mAlarmsEnabled = enabled; Q_EMIT alarmEnabledToggled(enabled); if (!enabled) KAlarm::cancelRtcWake(nullptr); else if (!mProcessingQueue) checkNextDueAlarm(); } } /****************************************************************************** * Spread or collect alarm message and error message windows. */ void KAlarmApp::spreadWindows(bool spread) { spread = MessageWin::spread(spread); Q_EMIT spreadWindowsToggled(spread); } /****************************************************************************** * Called when the spread status of message windows changes. * Set the 'spread windows' action state. */ void KAlarmApp::setSpreadWindowsState(bool spread) { Q_EMIT spreadWindowsToggled(spread); } /****************************************************************************** * Check whether the window manager's handling of keyboard focus transfer * between application windows is broken. This is true for Ubuntu's Unity * desktop, where MessageWin windows steal keyboard focus from EditAlarmDlg * windows. */ bool KAlarmApp::windowFocusBroken() const { return mWindowFocusBroken; } /****************************************************************************** * Check whether window/keyboard focus currently needs to be fixed manually due * to the window manager not handling it correctly. This will occur if there are * both EditAlarmDlg and MessageWin windows currently active. */ bool KAlarmApp::needWindowFocusFix() const { return mWindowFocusBroken && MessageWin::instanceCount(true) && EditAlarmDlg::instanceCount(); } /****************************************************************************** * Called to schedule a new alarm, either in response to a DCOP notification or * to command line options. * Reply = true unless there was a parameter error or an error opening calendar file. */ bool KAlarmApp::scheduleEvent(KAEvent::SubAction action, const QString& text, const KADateTime& dateTime, int lateCancel, KAEvent::Flags flags, const QColor& bg, const QColor& fg, const QFont& font, const QString& audioFile, float audioVolume, int reminderMinutes, const KARecurrence& recurrence, const KCalendarCore::Duration& repeatInterval, int repeatCount, uint mailFromID, const KCalendarCore::Person::List& mailAddresses, const QString& mailSubject, const QStringList& mailAttachments) { qCDebug(KALARM_LOG) << "KAlarmApp::scheduleEvent:" << text; if (!dateTime.isValid()) return false; const KADateTime now = KADateTime::currentUtcDateTime(); if (lateCancel && dateTime < now.addSecs(-maxLateness(lateCancel))) return true; // alarm time was already archived too long ago KADateTime alarmTime = dateTime; // Round down to the nearest minute to avoid scheduling being messed up if (!dateTime.isDateOnly()) alarmTime.setTime(QTime(alarmTime.time().hour(), alarmTime.time().minute(), 0)); KAEvent event(alarmTime, text, bg, fg, font, action, lateCancel, flags, true); if (reminderMinutes) { const bool onceOnly = flags & KAEvent::REMINDER_ONCE; event.setReminder(reminderMinutes, onceOnly); } if (!audioFile.isEmpty()) event.setAudioFile(audioFile, audioVolume, -1, 0, (flags & KAEvent::REPEAT_SOUND) ? 0 : -1); if (!mailAddresses.isEmpty()) event.setEmail(mailFromID, mailAddresses, mailSubject, mailAttachments); event.setRecurrence(recurrence); event.setFirstRecurrence(); event.setRepetition(Repetition(repeatInterval, repeatCount - 1)); event.endChanges(); if (alarmTime <= now) { // Alarm is due for display already. // First execute it once without adding it to the calendar file. if (!mInitialised) mActionQueue.enqueue(ActionQEntry(event, QueuedAction::Trigger)); else execAlarm(event, event.firstAlarm(), false); // If it's a recurring alarm, reschedule it for its next occurrence if (!event.recurs() || event.setNextOccurrence(now) == KAEvent::NO_OCCURRENCE) return true; // It has recurrences in the future } // Queue the alarm for insertion into the calendar file mActionQueue.enqueue(ActionQEntry(event)); if (mInitialised) QTimer::singleShot(0, this, &KAlarmApp::processQueue); return true; } /****************************************************************************** * Called in response to a D-Bus request to trigger or cancel an event. * Optionally display the event. Delete the event from the calendar file and * from every main window instance. */ bool KAlarmApp::dbusHandleEvent(const EventId& eventID, QueuedAction action) { qCDebug(KALARM_LOG) << "KAlarmApp::dbusHandleEvent:" << eventID; mActionQueue.append(ActionQEntry(action, eventID)); if (mInitialised) QTimer::singleShot(0, this, &KAlarmApp::processQueue); return true; } /****************************************************************************** * Called in response to a D-Bus request to list all pending alarms. */ QString KAlarmApp::dbusList() { qCDebug(KALARM_LOG) << "KAlarmApp::dbusList"; return scheduledAlarmList().join(QLatin1Char('\n')) + QLatin1Char('\n'); } /****************************************************************************** * Either: * a) Display the event and then delete it if it has no outstanding repetitions. * b) Delete the event. * c) Reschedule the event for its next repetition. If none remain, delete it. * If the event is deleted, it is removed from the calendar file and from every * main window instance. * If 'findUniqueId' is true and 'id' does not specify a resource, all resources * will be searched for the event's unique ID. * Reply = false if event ID not found, or if more than one event with the same * ID is found. */ bool KAlarmApp::handleEvent(const EventId& id, QueuedAction action, bool findUniqueId) { Q_ASSERT(!(int(action) & ~int(QueuedAction::ActionMask))); // Delete any expired wake-on-suspend config data KAlarm::checkRtcWakeConfig(); const QString eventID(id.eventId()); KAEvent* event = ResourcesCalendar::instance()->event(id, findUniqueId); if (!event) { if (id.resourceId() != -1) qCWarning(KALARM_LOG) << "KAlarmApp::handleEvent: Event ID not found, or duplicated:" << eventID; else qCWarning(KALARM_LOG) << "KAlarmApp::handleEvent: Event ID not found:" << eventID; return false; } switch (action) { case QueuedAction::Cancel: qCDebug(KALARM_LOG) << "KAlarmApp::handleEvent:" << eventID << ", CANCEL"; KAlarm::deleteEvent(*event, true); break; case QueuedAction::Trigger: // handle it if it's due, else execute it regardless case QueuedAction::Handle: // handle it if it's due { const KADateTime now = KADateTime::currentUtcDateTime(); qCDebug(KALARM_LOG) << "KAlarmApp::handleEvent:" << eventID << "," << (action==QueuedAction::Trigger?"TRIGGER:":"HANDLE:") << qPrintable(now.qDateTime().toString(QStringLiteral("yyyy-MM-dd hh:mm"))) << "UTC"; bool updateCalAndDisplay = false; bool alarmToExecuteValid = false; KAAlarm alarmToExecute; bool restart = false; // Check all the alarms in turn. // Note that the main alarm is fetched before any other alarms. for (KAAlarm alarm = event->firstAlarm(); alarm.isValid(); alarm = (restart ? event->firstAlarm() : event->nextAlarm(alarm)), restart = false) { // Check if the alarm is due yet. const KADateTime nextDT = alarm.dateTime(true).effectiveKDateTime(); const int secs = nextDT.secsTo(now); if (secs < 0) { // The alarm appears to be in the future. // Check if it's an invalid local time during a daylight // saving time shift, which has actually passed. if (alarm.dateTime().timeSpec() != KADateTime::LocalZone || nextDT > now.toTimeSpec(KADateTime::LocalZone)) { // This alarm is definitely not due yet qCDebug(KALARM_LOG) << "KAlarmApp::handleEvent: Alarm" << alarm.type() << "at" << nextDT.qDateTime() << ": not due"; continue; } } bool reschedule = false; bool rescheduleWork = false; if ((event->workTimeOnly() || event->holidaysExcluded()) && !alarm.deferred()) { // The alarm is restricted to working hours and/or non-holidays // (apart from deferrals). This needs to be re-evaluated every // time it triggers, since working hours could change. if (alarm.dateTime().isDateOnly()) { KADateTime dt(nextDT); dt.setDateOnly(true); reschedule = !event->isWorkingTime(dt); } else reschedule = !event->isWorkingTime(nextDT); rescheduleWork = reschedule; if (reschedule) qCDebug(KALARM_LOG) << "KAlarmApp::handleEvent: Alarm" << alarm.type() << "at" << nextDT.qDateTime() << ": not during working hours"; } if (!reschedule && alarm.repeatAtLogin()) { // Alarm is to be displayed at every login. qCDebug(KALARM_LOG) << "KAlarmApp::handleEvent: REPEAT_AT_LOGIN"; // Check if the main alarm is already being displayed. // (We don't want to display both at the same time.) if (alarmToExecute.isValid()) continue; // Set the time to display if it's a display alarm alarm.setTime(now); } if (!reschedule && event->lateCancel()) { // Alarm is due, and it is to be cancelled if too late. qCDebug(KALARM_LOG) << "KAlarmApp::handleEvent: LATE_CANCEL"; bool cancel = false; if (alarm.dateTime().isDateOnly()) { // The alarm has no time, so cancel it if its date is too far past const int maxlate = event->lateCancel() / 1440; // maximum lateness in days KADateTime limit(DateTime(nextDT.addDays(maxlate + 1)).effectiveKDateTime()); if (now >= limit) { // It's too late to display the scheduled occurrence. // Find the last previous occurrence of the alarm. DateTime next; const KAEvent::OccurType type = event->previousOccurrence(now, next, true); switch (type & ~KAEvent::OCCURRENCE_REPEAT) { case KAEvent::FIRST_OR_ONLY_OCCURRENCE: case KAEvent::RECURRENCE_DATE: case KAEvent::RECURRENCE_DATE_TIME: case KAEvent::LAST_RECURRENCE: limit.setDate(next.date().addDays(maxlate + 1)); if (now >= limit) { if (type == KAEvent::LAST_RECURRENCE || (type == KAEvent::FIRST_OR_ONLY_OCCURRENCE && !event->recurs())) cancel = true; // last occurrence (and there are no repetitions) else reschedule = true; } break; case KAEvent::NO_OCCURRENCE: default: reschedule = true; break; } } } else { // The alarm is timed. Allow it to be the permitted amount late before cancelling it. const int maxlate = maxLateness(event->lateCancel()); if (secs > maxlate) { // It's over the maximum interval late. // Find the most recent occurrence of the alarm. DateTime next; const KAEvent::OccurType type = event->previousOccurrence(now, next, true); switch (type & ~KAEvent::OCCURRENCE_REPEAT) { case KAEvent::FIRST_OR_ONLY_OCCURRENCE: case KAEvent::RECURRENCE_DATE: case KAEvent::RECURRENCE_DATE_TIME: case KAEvent::LAST_RECURRENCE: if (next.effectiveKDateTime().secsTo(now) > maxlate) { if (type == KAEvent::LAST_RECURRENCE || (type == KAEvent::FIRST_OR_ONLY_OCCURRENCE && !event->recurs())) cancel = true; // last occurrence (and there are no repetitions) else reschedule = true; } break; case KAEvent::NO_OCCURRENCE: default: reschedule = true; break; } } } if (cancel) { // All recurrences are finished, so cancel the event event->setArchive(); if (cancelAlarm(*event, alarm.type(), false)) return true; // event has been deleted updateCalAndDisplay = true; continue; } } if (reschedule) { // The latest repetition was too long ago, so schedule the next one switch (rescheduleAlarm(*event, alarm, false, (rescheduleWork ? nextDT : KADateTime()))) { case 1: // A working-time-only alarm has been rescheduled and the // rescheduled time is already due. Start processing the // event again. alarmToExecuteValid = false; restart = true; break; case -1: return true; // event has been deleted default: break; } updateCalAndDisplay = true; continue; } if (!alarmToExecuteValid) { qCDebug(KALARM_LOG) << "KAlarmApp::handleEvent: Alarm" << alarm.type() << ": execute"; alarmToExecute = alarm; // note the alarm to be displayed alarmToExecuteValid = true; // only trigger one alarm for the event } else qCDebug(KALARM_LOG) << "KAlarmApp::handleEvent: Alarm" << alarm.type() << ": skip"; } // If there is an alarm to execute, do this last after rescheduling/cancelling // any others. This ensures that the updated event is only saved once to the calendar. if (alarmToExecute.isValid()) execAlarm(*event, alarmToExecute, true, !alarmToExecute.repeatAtLogin()); else { if (action == QueuedAction::Trigger) { // The alarm is to be executed regardless of whether it's due. // Only trigger one alarm from the event - we don't want multiple // identical messages, for example. const KAAlarm alarm = event->firstAlarm(); if (alarm.isValid()) execAlarm(*event, alarm, false); } if (updateCalAndDisplay) KAlarm::updateEvent(*event); // update the window lists and calendar file else if (action != QueuedAction::Trigger) { qCDebug(KALARM_LOG) << "KAlarmApp::handleEvent: No action"; } } break; } default: break; } return true; } /****************************************************************************** * Called when an alarm action has completed, to perform any post-alarm actions. */ void KAlarmApp::alarmCompleted(const KAEvent& event) { if (!event.postAction().isEmpty()) { // doShellCommand() will error if the user is not authorised to run // shell commands. const QString command = event.postAction(); qCDebug(KALARM_LOG) << "KAlarmApp::alarmCompleted:" << event.id() << ":" << command; doShellCommand(command, event, nullptr, ProcData::POST_ACTION); } } /****************************************************************************** * Reschedule the alarm for its next recurrence after now. If none remain, * delete it. If the alarm is deleted and it is the last alarm for its event, * the event is removed from the calendar file and from every main window * instance. * If 'nextDt' is valid, the event is rescheduled for the next non-working * time occurrence after that. * Reply = 1 if 'nextDt' is valid and the rescheduled event is already due * = -1 if the event has been deleted * = 0 otherwise. */ int KAlarmApp::rescheduleAlarm(KAEvent& event, const KAAlarm& alarm, bool updateCalAndDisplay, const KADateTime& nextDt) { qCDebug(KALARM_LOG) << "KAlarmApp::rescheduleAlarm: Alarm type:" << alarm.type(); int reply = 0; bool update = false; event.startChanges(); if (alarm.repeatAtLogin()) { // Leave an alarm which repeats at every login until its main alarm triggers if (!event.reminderActive() && event.reminderMinutes() < 0) { // Executing an at-login alarm: first schedule the reminder // which occurs AFTER the main alarm. event.activateReminderAfter(KADateTime::currentUtcDateTime()); } // Repeat-at-login alarms are usually unchanged after triggering. // Ensure that the archive flag (which was set in execAlarm()) is saved. update = true; } else if (alarm.isReminder() || alarm.deferred()) { // It's a reminder alarm or an extra deferred alarm, so delete it event.removeExpiredAlarm(alarm.type()); update = true; } else { // Reschedule the alarm for its next occurrence. bool cancelled = false; DateTime last = event.mainDateTime(false); // note this trigger time if (last != event.mainDateTime(true)) last = DateTime(); // but ignore sub-repetition triggers bool next = nextDt.isValid(); KADateTime next_dt = nextDt; const KADateTime now = KADateTime::currentUtcDateTime(); do { const KAEvent::OccurType type = event.setNextOccurrence(next ? next_dt : now); switch (type) { case KAEvent::NO_OCCURRENCE: // All repetitions are finished, so cancel the event qCDebug(KALARM_LOG) << "KAlarmApp::rescheduleAlarm: No occurrence"; if (event.reminderMinutes() < 0 && last.isValid() && alarm.type() != KAAlarm::AT_LOGIN_ALARM && !event.mainExpired()) { // Set the reminder which is now due after the last main alarm trigger. // Note that at-login reminders are scheduled in execAlarm(). event.activateReminderAfter(last); updateCalAndDisplay = true; } if (cancelAlarm(event, alarm.type(), updateCalAndDisplay)) return -1; break; default: if (!(type & KAEvent::OCCURRENCE_REPEAT)) break; // Next occurrence is a repeat, so fall through to recurrence handling Q_FALLTHROUGH(); case KAEvent::RECURRENCE_DATE: case KAEvent::RECURRENCE_DATE_TIME: case KAEvent::LAST_RECURRENCE: // The event is due by now and repetitions still remain, so rewrite the event if (updateCalAndDisplay) update = true; break; case KAEvent::FIRST_OR_ONLY_OCCURRENCE: // The first occurrence is still due?!?, so don't do anything break; } if (cancelled) break; if (event.deferred()) { // Just in case there's also a deferred alarm, ensure it's removed event.removeExpiredAlarm(KAAlarm::DEFERRED_ALARM); update = true; } if (next) { // The alarm is restricted to working hours and/or non-holidays. // Check if the calculated next time is valid. next_dt = event.mainDateTime(true).effectiveKDateTime(); if (event.mainDateTime(false).isDateOnly()) { KADateTime dt(next_dt); dt.setDateOnly(true); next = !event.isWorkingTime(dt); } else next = !event.isWorkingTime(next_dt); } } while (next && next_dt <= now); reply = (!cancelled && next_dt.isValid() && (next_dt <= now)) ? 1 : 0; if (event.reminderMinutes() < 0 && last.isValid() && alarm.type() != KAAlarm::AT_LOGIN_ALARM) { // Set the reminder which is now due after the last main alarm trigger. // Note that at-login reminders are scheduled in execAlarm(). event.activateReminderAfter(last); } } event.endChanges(); if (update) KAlarm::updateEvent(event); // update the window lists and calendar file return reply; } /****************************************************************************** * Delete the alarm. If it is the last alarm for its event, the event is removed * from the calendar file and from every main window instance. * Reply = true if event has been deleted. */ bool KAlarmApp::cancelAlarm(KAEvent& event, KAAlarm::Type alarmType, bool updateCalAndDisplay) { qCDebug(KALARM_LOG) << "KAlarmApp::cancelAlarm"; if (alarmType == KAAlarm::MAIN_ALARM && !event.displaying() && event.toBeArchived()) { // The event is being deleted. Save it in the archived resources first. KAEvent ev(event); KAlarm::addArchivedEvent(ev); } event.removeExpiredAlarm(alarmType); if (!event.alarmCount()) { // If it's a command alarm being executed, mark it as deleted ProcData* pd = findCommandProcess(event.id()); if (pd) pd->eventDeleted = true; // Delete it KAlarm::deleteEvent(event, false); return true; } if (updateCalAndDisplay) KAlarm::updateEvent(event); // update the window lists and calendar file return false; } /****************************************************************************** * Cancel any reminder or deferred alarms in an repeat-at-login event. * This should be called when the event is first loaded. * If there are no more alarms left in the event, the event is removed from the * calendar file and from every main window instance. * Reply = true if event has been deleted. */ bool KAlarmApp::cancelReminderAndDeferral(KAEvent& event) { return cancelAlarm(event, KAAlarm::REMINDER_ALARM, false) || cancelAlarm(event, KAAlarm::DEFERRED_REMINDER_ALARM, false) || cancelAlarm(event, KAAlarm::DEFERRED_ALARM, true); } /****************************************************************************** * Execute an alarm by displaying its message or file, or executing its command. * Reply = ShellProcess instance if a command alarm * = MessageWin if an audio alarm * != 0 if successful * = -1 if execution has not completed * = 0 if the alarm is disabled, or if an error message was output. */ void* KAlarmApp::execAlarm(KAEvent& event, const KAAlarm& alarm, bool reschedule, bool allowDefer, bool noPreAction) { if (!mAlarmsEnabled || !event.enabled()) { // The event (or all events) is disabled qCDebug(KALARM_LOG) << "KAlarmApp::execAlarm:" << event.id() << ": disabled"; if (reschedule) rescheduleAlarm(event, alarm, true); return nullptr; } void* result = (void*)1; event.setArchive(); switch (alarm.action()) { case KAAlarm::COMMAND: if (!event.commandDisplay()) { // execCommandAlarm() will error if the user is not authorised // to run shell commands. result = execCommandAlarm(event, alarm); if (reschedule) rescheduleAlarm(event, alarm, true); break; } Q_FALLTHROUGH(); // fall through to MESSAGE case KAAlarm::MESSAGE: case KAAlarm::FILE: { // Display a message, file or command output, provided that the same event // isn't already being displayed MessageWin* win = MessageWin::findEvent(EventId(event)); // Find if we're changing a reminder message to the real message const bool reminder = (alarm.type() & KAAlarm::REMINDER_ALARM); const bool replaceReminder = !reminder && win && (win->alarmType() & KAAlarm::REMINDER_ALARM); if (!reminder && (!event.deferred() || (event.extraActionOptions() & KAEvent::ExecPreActOnDeferral)) && (replaceReminder || !win) && !noPreAction && !event.preAction().isEmpty()) { // It's not a reminder alarm, and it's not a deferred alarm unless the // pre-alarm action applies to deferred alarms, and there is no message // window (other than a reminder window) currently displayed for this // alarm, and we need to execute a command before displaying the new window. // // NOTE: The pre-action is not executed for a recurring alarm if an // alarm message window for a previous occurrence is still visible. // Check whether the command is already being executed for this alarm. for (const ProcData* pd : qAsConst(mCommandProcesses)) { if (pd->event->id() == event.id() && (pd->flags & ProcData::PRE_ACTION)) { qCDebug(KALARM_LOG) << "KAlarmApp::execAlarm: Already executing pre-DISPLAY command"; return pd->process; // already executing - don't duplicate the action } } // doShellCommand() will error if the user is not authorised to run // shell commands. const QString command = event.preAction(); qCDebug(KALARM_LOG) << "KAlarmApp::execAlarm: Pre-DISPLAY command:" << command; const int flags = (reschedule ? ProcData::RESCHEDULE : 0) | (allowDefer ? ProcData::ALLOW_DEFER : 0); if (doShellCommand(command, event, &alarm, (flags | ProcData::PRE_ACTION))) { ResourcesCalendar::instance()->setAlarmPending(&event); return result; // display the message after the command completes } // Error executing command if (event.extraActionOptions() & KAEvent::CancelOnPreActError) { // Cancel the rest of the alarm execution qCDebug(KALARM_LOG) << "KAlarmApp::execAlarm:" << event.id() << ": pre-action failed: cancelled"; if (reschedule) rescheduleAlarm(event, alarm, true); return nullptr; } // Display the message even though it failed } if (!win) { // There isn't already a message for this event const int flags = (reschedule ? 0 : MessageWin::NO_RESCHEDULE) | (allowDefer ? 0 : MessageWin::NO_DEFER); (new MessageWin(&event, alarm, flags))->show(); } else if (replaceReminder) { // The caption needs to be changed from "Reminder" to "Message" win->cancelReminder(event, alarm); } else if (!win->hasDefer() && !alarm.repeatAtLogin()) { // It's a repeat-at-login message with no Defer button, // which has now reached its final trigger time and needs // to be replaced with a new message. win->showDefer(); win->showDateTime(event, alarm); } else { // Use the existing message window } if (win) { // Raise the existing message window and replay any sound win->repeat(alarm); // N.B. this reschedules the alarm } break; } case KAAlarm::EMAIL: { qCDebug(KALARM_LOG) << "KAlarmApp::execAlarm: EMAIL to:" << event.emailAddresses(QStringLiteral(",")); QStringList errmsgs; KAMail::JobData data(event, alarm, reschedule, (reschedule || allowDefer)); data.queued = true; int ans = KAMail::send(data, errmsgs); if (ans) { // The email has either been sent or failed - not queued if (ans < 0) result = nullptr; // failure data.queued = false; emailSent(data, errmsgs, (ans > 0)); } else { result = (void*)-1; // email has been queued } if (reschedule) rescheduleAlarm(event, alarm, true); break; } case KAAlarm::AUDIO: { // Play the sound, provided that the same event // isn't already playing MessageWin* win = MessageWin::findEvent(EventId(event)); if (!win) { // There isn't already a message for this event. const int flags = (reschedule ? 0 : MessageWin::NO_RESCHEDULE) | MessageWin::ALWAYS_HIDE; win = new MessageWin(&event, alarm, flags); } else { // There's an existing message window: replay the sound win->repeat(alarm); // N.B. this reschedules the alarm } return win; } default: return nullptr; } return result; } /****************************************************************************** * Called when sending an email has completed. */ void KAlarmApp::emailSent(KAMail::JobData& data, const QStringList& errmsgs, bool copyerr) { if (!errmsgs.isEmpty()) { // Some error occurred, although the email may have been sent successfully if (errmsgs.count() > 1) qCDebug(KALARM_LOG) << "KAlarmApp::emailSent:" << (copyerr ? "Copy error:" : "Failed:") << errmsgs[1]; MessageWin::showError(data.event, data.alarm.dateTime(), errmsgs); } else if (data.queued) Q_EMIT execAlarmSuccess(); } /****************************************************************************** * Execute the command specified in a command alarm. * To connect to the output ready signals of the process, specify a slot to be * called by supplying 'receiver' and 'slot' parameters. */ ShellProcess* KAlarmApp::execCommandAlarm(const KAEvent& event, const KAAlarm& alarm, const QObject* receiver, const char* slot) { // doShellCommand() will error if the user is not authorised to run // shell commands. const int flags = (event.commandXterm() ? ProcData::EXEC_IN_XTERM : 0) | (event.commandDisplay() ? ProcData::DISP_OUTPUT : 0); const QString command = event.cleanText(); if (event.commandScript()) { // Store the command script in a temporary file for execution qCDebug(KALARM_LOG) << "KAlarmApp::execCommandAlarm: Script"; const QString tmpfile = createTempScriptFile(command, false, event, alarm); if (tmpfile.isEmpty()) { setEventCommandError(event, KAEvent::CMD_ERROR); return nullptr; } return doShellCommand(tmpfile, event, &alarm, (flags | ProcData::TEMP_FILE), receiver, slot); } else { qCDebug(KALARM_LOG) << "KAlarmApp::execCommandAlarm:" << command; return doShellCommand(command, event, &alarm, flags, receiver, slot); } } /****************************************************************************** * Execute a shell command line specified by an alarm. * If the PRE_ACTION bit of 'flags' is set, the alarm will be executed via * execAlarm() once the command completes, the execAlarm() parameters being * derived from the remaining bits in 'flags'. * 'flags' must contain the bit PRE_ACTION or POST_ACTION if and only if it is * a pre- or post-alarm action respectively. * To connect to the output ready signals of the process, specify a slot to be * called by supplying 'receiver' and 'slot' parameters. * * Note that if shell access is not authorised, the attempt to run the command * will be errored. */ ShellProcess* KAlarmApp::doShellCommand(const QString& command, const KAEvent& event, const KAAlarm* alarm, int flags, const QObject* receiver, const char* slot) { qCDebug(KALARM_LOG) << "KAlarmApp::doShellCommand:" << command << "," << event.id(); QIODevice::OpenMode mode = QIODevice::WriteOnly; QString cmd; QString tmpXtermFile; if (flags & ProcData::EXEC_IN_XTERM) { // Execute the command in a terminal window. cmd = composeXTermCommand(command, event, alarm, flags, tmpXtermFile); if (cmd.isEmpty()) { qCWarning(KALARM_LOG) << "KAlarmApp::doShellCommand: Command failed (no terminal selected)"; const QStringList errors{i18nc("@info", "Failed to execute command\n(no terminal selected for command alarms)")}; commandErrorMsg(nullptr, event, alarm, flags, errors); return nullptr; } } else { cmd = command; mode = QIODevice::ReadWrite; } ProcData* pd = nullptr; ShellProcess* proc = nullptr; if (!cmd.isEmpty()) { // Use ShellProcess, which automatically checks whether the user is // authorised to run shell commands. proc = new ShellProcess(cmd); proc->setEnv(QStringLiteral("KALARM_UID"), event.id(), true); proc->setOutputChannelMode(KProcess::MergedChannels); // combine stdout & stderr connect(proc, &ShellProcess::shellExited, this, &KAlarmApp::slotCommandExited); if ((flags & ProcData::DISP_OUTPUT) && receiver && slot) { connect(proc, SIGNAL(receivedStdout(ShellProcess*)), receiver, slot); connect(proc, SIGNAL(receivedStderr(ShellProcess*)), receiver, slot); } if (mode == QIODevice::ReadWrite && !event.logFile().isEmpty()) { // Output is to be appended to a log file. // Set up a logging process to write the command's output to. QString heading; if (alarm && alarm->dateTime().isValid()) { const QString dateTime = alarm->dateTime().formatLocale(); heading = QStringLiteral("\n******* KAlarm %1 *******\n").arg(dateTime); } else heading = QStringLiteral("\n******* KAlarm *******\n"); QFile logfile(event.logFile()); if (logfile.open(QIODevice::Append | QIODevice::Text)) { QTextStream out(&logfile); out << heading; logfile.close(); } proc->setStandardOutputFile(event.logFile(), QIODevice::Append); } pd = new ProcData(proc, new KAEvent(event), (alarm ? new KAAlarm(*alarm) : nullptr), flags); if (flags & ProcData::TEMP_FILE) pd->tempFiles += command; if (!tmpXtermFile.isEmpty()) pd->tempFiles += tmpXtermFile; mCommandProcesses.append(pd); if (proc->start(mode)) return proc; } // Error executing command - report it qCWarning(KALARM_LOG) << "KAlarmApp::doShellCommand: Command failed to start"; commandErrorMsg(proc, event, alarm, flags); if (pd) { mCommandProcesses.removeAt(mCommandProcesses.indexOf(pd)); delete pd; } return nullptr; } /****************************************************************************** * Compose a command line to execute the given command in a terminal window. * 'tempScriptFile' receives the name of a temporary script file which is * invoked by the command line, if applicable. * Reply = command line, or empty string if error. */ QString KAlarmApp::composeXTermCommand(const QString& command, const KAEvent& event, const KAAlarm* alarm, int flags, QString& tempScriptFile) const { qCDebug(KALARM_LOG) << "KAlarmApp::composeXTermCommand:" << command << "," << event.id(); tempScriptFile.clear(); QString cmd = Preferences::cmdXTermCommand(); if (cmd.isEmpty()) return QString(); // no terminal application is configured cmd.replace(QLatin1String("%t"), KAboutData::applicationData().displayName()); // set the terminal window title if (cmd.indexOf(QLatin1String("%C")) >= 0) { // Execute the command from a temporary script file if (flags & ProcData::TEMP_FILE) cmd.replace(QLatin1String("%C"), command); // the command is already calling a temporary file else { tempScriptFile = createTempScriptFile(command, true, event, *alarm); if (tempScriptFile.isEmpty()) return QString(); cmd.replace(QLatin1String("%C"), tempScriptFile); // %C indicates where to insert the command } } else if (cmd.indexOf(QLatin1String("%W")) >= 0) { // Execute the command from a temporary script file, // with a sleep after the command is executed tempScriptFile = createTempScriptFile(command + QLatin1String("\nsleep 86400\n"), true, event, *alarm); if (tempScriptFile.isEmpty()) return QString(); cmd.replace(QLatin1String("%W"), tempScriptFile); // %w indicates where to insert the command } else if (cmd.indexOf(QLatin1String("%w")) >= 0) { // Append a sleep to the command. // Quote the command in case it contains characters such as [>|;]. const QString exec = KShell::quoteArg(command + QLatin1String("; sleep 86400")); cmd.replace(QLatin1String("%w"), exec); // %w indicates where to insert the command string } else { // Set the command to execute. // Put it in quotes in case it contains characters such as [>|;]. const QString exec = KShell::quoteArg(command); if (cmd.indexOf(QLatin1String("%c")) >= 0) cmd.replace(QLatin1String("%c"), exec); // %c indicates where to insert the command string else cmd.append(exec); // otherwise, simply append the command string } return cmd; } /****************************************************************************** * Create a temporary script file containing the specified command string. * Reply = path of temporary file, or null string if error. */ QString KAlarmApp::createTempScriptFile(const QString& command, bool insertShell, const KAEvent& event, const KAAlarm& alarm) const { QTemporaryFile tmpFile; tmpFile.setAutoRemove(false); // don't delete file when it is destructed if (!tmpFile.open()) qCCritical(KALARM_LOG) << "Unable to create a temporary script file"; else { tmpFile.setPermissions(QFile::ReadUser | QFile::WriteUser | QFile::ExeUser); QTextStream stream(&tmpFile); if (insertShell) stream << "#!" << ShellProcess::shellPath() << "\n"; stream << command; stream.flush(); if (tmpFile.error() != QFile::NoError) qCCritical(KALARM_LOG) << "Error" << tmpFile.errorString() << " writing to temporary script file"; else return tmpFile.fileName(); } const QStringList errmsgs(i18nc("@info", "Error creating temporary script file")); MessageWin::showError(event, alarm.dateTime(), errmsgs, QStringLiteral("Script")); return QString(); } /****************************************************************************** * Called when a command alarm's execution completes. */ void KAlarmApp::slotCommandExited(ShellProcess* proc) { qCDebug(KALARM_LOG) << "KAlarmApp::slotCommandExited"; // Find this command in the command list for (int i = 0, end = mCommandProcesses.count(); i < end; ++i) { ProcData* pd = mCommandProcesses.at(i); if (pd->process == proc) { // Found the command. Check its exit status. bool executeAlarm = pd->preAction(); const ShellProcess::Status status = proc->status(); if (status == ShellProcess::SUCCESS && !proc->exitCode()) { qCDebug(KALARM_LOG) << "KAlarmApp::slotCommandExited:" << pd->event->id() << ": SUCCESS"; clearEventCommandError(*pd->event, pd->preAction() ? KAEvent::CMD_ERROR_PRE : pd->postAction() ? KAEvent::CMD_ERROR_POST : KAEvent::CMD_ERROR); } else { QString errmsg = proc->errorMessage(); if (status == ShellProcess::SUCCESS || status == ShellProcess::NOT_FOUND) qCWarning(KALARM_LOG) << "KAlarmApp::slotCommandExited:" << pd->event->id() << ":" << errmsg << "exit status =" << status << ", code =" << proc->exitCode(); else qCWarning(KALARM_LOG) << "KAlarmApp::slotCommandExited:" << pd->event->id() << ":" << errmsg << "exit status =" << status; if (pd->messageBoxParent) { // Close the existing informational KMessageBox for this process const QList dialogs = pd->messageBoxParent->findChildren(); if (!dialogs.isEmpty()) delete dialogs[0]; setEventCommandError(*pd->event, pd->preAction() ? KAEvent::CMD_ERROR_PRE : pd->postAction() ? KAEvent::CMD_ERROR_POST : KAEvent::CMD_ERROR); if (!pd->tempFile()) { errmsg += QLatin1Char('\n'); errmsg += proc->command(); } KAMessageBox::error(pd->messageBoxParent, errmsg); } else commandErrorMsg(proc, *pd->event, pd->alarm, pd->flags); if (executeAlarm && (pd->event->extraActionOptions() & KAEvent::CancelOnPreActError)) { qCDebug(KALARM_LOG) << "KAlarmApp::slotCommandExited:" << pd->event->id() << ": pre-action failed: cancelled"; if (pd->reschedule()) rescheduleAlarm(*pd->event, *pd->alarm, true); executeAlarm = false; } } if (pd->preAction()) ResourcesCalendar::instance()->setAlarmPending(pd->event, false); if (executeAlarm) execAlarm(*pd->event, *pd->alarm, pd->reschedule(), pd->allowDefer(), true); mCommandProcesses.removeAt(i); delete pd; break; } } // If there are now no executing shell commands, quit if a quit was queued if (mPendingQuit && mCommandProcesses.isEmpty()) quitIf(mPendingQuitCode); } /****************************************************************************** * Output an error message for a shell command, and record the alarm's error status. */ void KAlarmApp::commandErrorMsg(const ShellProcess* proc, const KAEvent& event, const KAAlarm* alarm, int flags, const QStringList& errors) { KAEvent::CmdErrType cmderr; QString dontShowAgain; QStringList errmsgs = errors; if (flags & ProcData::PRE_ACTION) { if (event.extraActionOptions() & KAEvent::DontShowPreActError) return; // don't notify user of any errors for the alarm errmsgs += i18nc("@info", "Pre-alarm action:"); dontShowAgain = QStringLiteral("Pre"); cmderr = KAEvent::CMD_ERROR_PRE; } else if (flags & ProcData::POST_ACTION) { errmsgs += i18nc("@info", "Post-alarm action:"); dontShowAgain = QStringLiteral("Post"); cmderr = (event.commandError() == KAEvent::CMD_ERROR_PRE) ? KAEvent::CMD_ERROR_PRE_POST : KAEvent::CMD_ERROR_POST; } else { dontShowAgain = QStringLiteral("Exec"); cmderr = KAEvent::CMD_ERROR; } // Record the alarm's error status setEventCommandError(event, cmderr); // Display an error message if (proc) { errmsgs += proc->errorMessage(); if (!(flags & ProcData::TEMP_FILE)) errmsgs += proc->command(); dontShowAgain += QString::number(proc->status()); } MessageWin::showError(event, (alarm ? alarm->dateTime() : DateTime()), errmsgs, dontShowAgain); } /****************************************************************************** * Notes that an informational KMessageBox is displayed for this process. */ void KAlarmApp::commandMessage(ShellProcess* proc, QWidget* parent) { // Find this command in the command list for (ProcData* pd : qAsConst(mCommandProcesses)) { if (pd->process == proc) { pd->messageBoxParent = parent; break; } } } /****************************************************************************** * If this is the first time through, open the calendar file, and start * processing the execution queue. */ bool KAlarmApp::initCheck(bool calendarOnly) { static bool firstTime = true; if (firstTime) qCDebug(KALARM_LOG) << "KAlarmApp::initCheck: first time"; if (initialiseTimerResources() || firstTime) { /* Need to open the display calendar now, since otherwise if display * alarms are immediately due, they will often be processed while * MessageWin::redisplayAlarms() is executing open() (but before open() * completes), which causes problems!! */ DisplayCalendar::instance()->open(); } if (firstTime) { setArchivePurgeDays(); firstTime = false; } if (!calendarOnly) startProcessQueue(); // start processing the execution queue return true; } /****************************************************************************** * Called when an audio thread starts or stops. */ void KAlarmApp::notifyAudioPlaying(bool playing) { Q_EMIT audioPlaying(playing); } /****************************************************************************** * Stop audio play. */ void KAlarmApp::stopAudio() { MessageWin::stopAudio(); } /****************************************************************************** * Set the command error for the specified alarm. */ void KAlarmApp::setEventCommandError(const KAEvent& event, KAEvent::CmdErrType err) const { ProcData* pd = findCommandProcess(event.id()); if (pd && pd->eventDeleted) return; // the alarm has been deleted, so can't set error status if (err == KAEvent::CMD_ERROR_POST && event.commandError() == KAEvent::CMD_ERROR_PRE) err = KAEvent::CMD_ERROR_PRE_POST; event.setCommandError(err); KAEvent* ev = ResourcesCalendar::instance()->event(EventId(event)); if (ev && ev->commandError() != err) ev->setCommandError(err); Resource resource = Resources::resourceForEvent(event.id()); resource.handleCommandErrorChange(event); } /****************************************************************************** * Clear the command error for the specified alarm. */ void KAlarmApp::clearEventCommandError(const KAEvent& event, KAEvent::CmdErrType err) const { ProcData* pd = findCommandProcess(event.id()); if (pd && pd->eventDeleted) return; // the alarm has been deleted, so can't set error status KAEvent::CmdErrType newerr = static_cast(event.commandError() & ~err); event.setCommandError(newerr); KAEvent* ev = ResourcesCalendar::instance()->event(EventId(event)); if (ev) { newerr = static_cast(ev->commandError() & ~err); ev->setCommandError(newerr); } Resource resource = Resources::resourceForEvent(event.id()); resource.handleCommandErrorChange(event); } /****************************************************************************** * Find the currently executing command process for an event ID, if any. */ KAlarmApp::ProcData* KAlarmApp::findCommandProcess(const QString& eventId) const { for (ProcData* pd : qAsConst(mCommandProcesses)) { if (pd->event->id() == eventId) return pd; } return nullptr; } KAlarmApp::ProcData::ProcData(ShellProcess* p, KAEvent* e, KAAlarm* a, int f) : process(p) , event(e) , alarm(a) , messageBoxParent(nullptr) , flags(f) , eventDeleted(false) { } KAlarmApp::ProcData::~ProcData() { while (!tempFiles.isEmpty()) { // Delete the temporary file called by the XTerm command QFile f(tempFiles.first()); f.remove(); tempFiles.removeFirst(); } delete process; delete event; delete alarm; } // vim: et sw=4: diff --git a/src/kalarmconfig.kcfg b/src/kalarmconfig.kcfg index ccbee7a7..847525be 100644 --- a/src/kalarmconfig.kcfg +++ b/src/kalarmconfig.kcfg @@ -1,409 +1,410 @@ "lib/timeperiod.h" "lib/locale.h" QFontDatabase KColorScheme QFont QColor qnamespace.h ShowInSystemTray Base_AutoHideSystemTray Base_TimeZone Base_HolidayRegion Base_StartOfDay Base_WorkDayStart Base_WorkDayEnd Base_WorkDays MessageFont DisabledColour ArchivedColour ArchivedKeepDays DefaultFeb29Type Select the time zone which KAlarm should use as its default for displaying and entering dates and times. Select the holiday region to use. Default foreground color for alarm message windows. KColorScheme(QPalette::Active).foreground().color() Default background color for alarm message windows. KColorScheme(QPalette::Active).background(KColorScheme::NegativeBackground).color() Default font for displaying alarm messages. QFont(QFontDatabase::systemFont(QFontDatabase::GeneralFont).family(), 16, QFont::Bold) <p>Check to show KAlarm's icon in the system tray. Showing it in the system tray provides easy access and a status indication.</p> true <p>Enter -1 to auto-hide KAlarm's icon in the system tray when there are no active alarms.</p><p>To auto-hide the system tray icon when there are no alarms due within a set period of time, enter the time period in minutes.</p><p>Enter 0 to always show the system tray icon.</p> 1440 <p>Automatically start KAlarm whenever you start KDE.</p><p>This option should always be checked unless you intend to discontinue use of KAlarm.</p> false Enter the default time interval (in minutes) to defer alarms, used by the Defer Alarm dialog. 5 <p>When saving a new alarm or alarm template, prompt for which calendar to store it in, if there is more than one active calendar.</p><p>Note that archived alarms are always stored in the default archived alarm calendar.</p> true <p>Specify the characteristics of alarm message windows:<ul><li>If checked, the window is a normal window with a title bar, which grabs keyboard input when it is displayed.</li><li>If unchecked, the window does not interfere with your typing when it is displayed, but it has no title bar and cannot be moved or resized.</li></ul></p> true <p>Specify the positioning of alarm message windows, and the delay in seconds after a window shows until its buttons are enabled. <ul><li>0 to position message windows as far from the cursor as possible with no enable delay.</li> <li>> 0 to position in center of screen and specify delay before enabling buttons.</li> <li>-1 for no special positioning and no enable delay.</li></ul></p> 0 -1 10 <p>How many alarms due in the next 24 hours to show in the system tray tooltip: <ul><li>0 to display none</li> <li>-1 to display all</li> <li>>0 to set a limit.</li></ul></p> 5 Specify whether to show in the system tray tooltip, the time at which each alarm is due. true Specify whether to show in the system tray tooltip, how long until each alarm is due. true Enter the text to be displayed in front of the time until the alarm, in the system tray tooltip. QLatin1String("+") <p>How to send email when an email alarm is triggered.<ul><li>KMail: The email is sent automatically via KMail. KMail is started first if necessary.</li><li>Sendmail: The email is sent automatically. This option will only work if your system is configured to use sendmail or a sendmail compatible mail transport agent.</li></ul></p> kmail Whether after sending an email to store a copy in KMail's sent-mail folder. Only applies when sendmail is selected as the email client. false Your email address, used to identify you as the sender when sending email alarms. Enter "@SystemSettings" to use the default email address set in KMail or System Settings, "@KMail" to allow choice of KMail identities, or enter the actual email address otherwise. KAlarmCal::Identities::identitiesExist() ? QLatin1String("@KMail") : QLatin1String("@SystemSettings") Your email address, used for blind copying email alarms to yourself. If you want blind copies to be sent to your account on the computer which KAlarm runs on, you can simply enter your user login name. Enter "@SystemSettings" to use the default email address set in KMail or System Settings, or enter the actual email address otherwise. QLatin1String("@SystemSettings") Command line to execute command alarms in a terminal window, including special codes described in the KAlarm handbook. The earliest time of day at which a date-only alarm will be triggered. QDateTime(QDate(1900,1,1),QTime(0,0)) The start time of the working day. QDateTime(QDate(1900,1,1),QTime(8,0)) The end time of the working day. QDateTime(QDate(1900,1,1),QTime(17,0)) OR'ed bits indicating which days of the week are work days, 1 = Monday ... 64 = Sunday. Locale::defaultWorkDays() Choose the text color in the alarm list for disabled alarms. KColorScheme(QPalette::Disabled).foreground(KColorScheme::InactiveText).color() Choose the text color in the alarm list for archived alarms. KColorScheme(QPalette::Active).foreground(KColorScheme::VisitedText).color() <p>Specify how many days to keep alarms after they expire or are deleted (except deleted alarms which were never triggered): <ul><li>0 to not keep</li> <li>-1 to keep indefinitely</li> <li>>0 number of days to keep.</li></ul></p> 7 Enter the event duration in minutes, for alarms which are copied to KOrganizer. 0 Enter how many minutes before the alarm trigger time to wake the system from suspend. This can be used to ensure that the system is fully restored by the time the alarm triggers. 2 Default value in the alarm edit dialog for late cancellation time (in minutes). 0 for no late cancellation, >0 how many minutes before cancelling. 0 0 Default setting in the alarm edit dialog for "auto close if late". false Default setting in the alarm edit dialog for "confirm alarm acknowledgement". false Default setting in the alarm edit dialog for "show in KOrganizer". false Default sound type in the alarm edit dialog. None Default sound file path in the alarm edit dialog. Default sound volume in the alarm edit dialog. -1 to deselect volume, or 0 - 100 % -1 -1 100 Default setting in the alarm edit dialog for sound repetition. false Default setting in the alarm edit dialog for command script entry. false Default setting in the alarm edit dialog for where to send command alarm output. Discard Default log file path for command alarms in the alarm edit dialog. Default setting in the alarm edit dialog for blind copying emails to self. false The default setting for the recurrence rule in the alarm edit dialog. None For yearly recurrences, choose what date, if any, alarms due on February 29th should occur in non-leap years.<i>Note</i>: The next scheduled occurrence of existing alarms is not re-evaluated when you change this setting. Mar1 Default reminder time units in the alarm edit dialog. HoursMinutes Default command to execute before displaying alarms. Default setting for whether to execute the pre-alarm action before deferred alarms trigger. false Default setting for whether to cancel the alarm if the pre-alarm action command fails. false Default setting for whether to show no error status or error message if the pre-alarm action command fails. false Default command to execute after alarm message windows are closed. Whether to suppress a warning prompt before quitting KAlarm. true Check to be prompted for confirmation each time you delete an alarm. true Display a notification message whenever an email alarm has queued an email for sending to a remote system. This could be useful if, for example, you have a dial-up connection, so that you can then ensure that the email is actually transmitted. false diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 556da8f2..4322ba12 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -1,1638 +1,1637 @@ /* * mainwindow.cpp - main application window * Program: kalarm * Copyright © 2001-2020 David Jarvie * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "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/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 #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); + DataModel::widgetNeedsDatabase(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 = 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() && theApp()->trayWindow()) ? theApp()->trayWindow()->assocMainWindow() : nullptr; if (tray && tray->isVisible()) return tray; for (int i = 0, end = mWindowList.count(); i < end; ++i) if (mWindowList.at(i)->isVisible()) return mWindowList.at(i); if (tray) return tray; if (mWindowList.isEmpty()) return nullptr; return mWindowList.at(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); QAction* actionMenubar = KStandardAction::showMenubar(this, SLOT(slotShowMenubar()), actions); 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); const bool menuVisible = !menuBar()->isHidden(); actionMenubar->setChecked(menuVisible); 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"; 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() { Resource resource; KAlarm::importAlarms(resource, 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()) { const KAEvent::List evts = KAEvent::ptrList(events); KAlarm::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 Show Menubar menu item is selected. */ void MainWindow::slotShowMenubar() { const bool visible = menuBar()->isVisible(); menuBar()->setVisible(!visible); } /****************************************************************************** * 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; ResourcesCalendar* resources = ResourcesCalendar::instance(); 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/resources/akonadidatamodel.h b/src/resources/akonadidatamodel.h index bb3f5224..706ec363 100644 --- a/src/resources/akonadidatamodel.h +++ b/src/resources/akonadidatamodel.h @@ -1,189 +1,192 @@ /* * akonadidatamodel.h - KAlarm calendar file access using Akonadi * Program: kalarm * Copyright © 2010-2020 David Jarvie * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef 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 reload(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; + /** Return the data storage backend type used by this model. */ + Preferences::Backend dataStorageBackend() const override { return Preferences::Akonadi; } + private Q_SLOTS: /** Called when a resource notifies a message to display to the user. */ void slotResourceMessage(ResourceType::MessageType, const QString& message, const QString& details); Q_SIGNALS: /** 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/calendarfunctions.cpp b/src/resources/calendarfunctions.cpp new file mode 100644 index 00000000..ce8cd069 --- /dev/null +++ b/src/resources/calendarfunctions.cpp @@ -0,0 +1,143 @@ +/* + * calendarfunctions.cpp - miscellaneous calendar access functions + * Program: kalarm + * Copyright © 2020 David Jarvie + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "calendarfunctions.h" + +#include "preferences.h" +#include "lib/messagebox.h" +#include "kalarm_debug.h" + +#include +#include +#include +using namespace KCalendarCore; + +#include +#include +#include + +#include + + +namespace KAlarm +{ + +/****************************************************************************** +* Import alarms from a calendar file. The alarms are converted to the current +* KAlarm format and are given new unique event IDs. +* Parameters: parent: parent widget for error message boxes +* alarmList: imported alarms are appended to this list +*/ +bool importCalendarFile(const QUrl& url, CalEvent::Types alarmTypes, bool newId, QWidget* parent, QHash>& alarmList) +{ + if (!url.isValid()) + { + qCDebug(KALARM_LOG) << "KAlarm::importCalendarFile: Invalid URL"; + return false; + } + + // If the URL is remote, download it into a temporary local file. + QString filename; + bool local = url.isLocalFile(); + if (local) + { + filename = url.toLocalFile(); + if (!QFile::exists(filename)) + { + qCDebug(KALARM_LOG) << "KAlarm::importCalendarFile:" << 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, parent); + if (!getJob->exec()) + { + qCCritical(KALARM_LOG) << "KAlarm::importCalendarFile: 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) << "KAlarm::importCalendarFile: --- 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)); + bool success = calStorage->load(); + if (!local) + QFile::remove(filename); + if (!success) + { + qCDebug(KALARM_LOG) << "KAlarm::importCalendarFile: Error loading calendar '" << filename <<"'"; + KAMessageBox::error(parent, xi18nc("@info", "Could not load calendar %1.", url.toDisplayString())); + return false; + } + + QString versionString; + const bool currentFormat = (KACalendar::updateVersion(calStorage, versionString) != KACalendar::IncompatibleFormat); + 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 (!currentFormat) + type = CalEvent::ACTIVE; + } + if (!(type & alarmTypes)) + continue; + + 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, or ensure that it is in the correct format. + const QString id = newId ? CalFormat::createUniqueId() : newev->uid(); + newev->setUid(CalEvent::uid(id, type)); + + alarmList[type] += KAEvent(newev); + } + return true; +} + +} + +// vim: et sw=4: diff --git a/src/resources/calendarfunctions.h b/src/resources/calendarfunctions.h new file mode 100644 index 00000000..9e1d712e --- /dev/null +++ b/src/resources/calendarfunctions.h @@ -0,0 +1,53 @@ +/* + * calendarfunctions.h - miscellaneous calendar access functions + * Program: kalarm + * Copyright © 2020 David Jarvie + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef CALENDARFUNCTIONS_H +#define CALENDARFUNCTIONS_H + +#include + +#include +#include + +using namespace KAlarmCal; + +class QUrl; +class QWidget; + +namespace KAlarm +{ + +/** Read events from a calendar file. The events are converted to the current + * KAlarm format and are optionally given new unique event IDs. + * + * @param url URL of calendar file to read + * @param alarmTypes alarm types to read from calendar file; other types are ignored + * @param newId whether to create new IDs for the events + * @param parent parent widget for error messages + * @param events imported alarms are appended to this list + * @return true if the calendar file was read successfully. + */ +bool importCalendarFile(const QUrl& url, CalEvent::Types alarmTypes, bool newId, QWidget* parent, QHash>& events); + +} // namespace KAlarm + +#endif // CALENDARFUNCTIONS_H + +// vim: et sw=4: diff --git a/src/resources/datamodel.cpp b/src/resources/datamodel.cpp index 6d99d2e8..63931bd1 100644 --- a/src/resources/datamodel.cpp +++ b/src/resources/datamodel.cpp @@ -1,104 +1,111 @@ /* * datamodel.cpp - calendar data model dependent functions * Program: kalarm * Copyright © 2019-2020 David Jarvie * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "datamodel.h" #include "akonadidatamodel.h" #include "akonadiresource.h" #include "akonadiresourcecreator.h" #include "akonadicalendarupdater.h" #include "eventmodel.h" #include "resourcemodel.h" namespace DataModel { void initialise() { - AkonadiDataModel::instance(); + AkonadiDataModel* model = AkonadiDataModel::instance(); + Preferences::setBackend(model->dataStorageBackend()); + Preferences::self()->save(); } void terminate() { } +void widgetNeedsDatabase(QWidget* widget) +{ + Akonadi::ControlGui::widgetNeedsAkonadi(widget); +} + void reload() { AkonadiDataModel::instance()->reload(); } bool reload(Resource& resource) { return AkonadiDataModel::instance()->reload(resource); } bool isMigrationComplete() { return AkonadiDataModel::instance()->isMigrationComplete(); } void removeDuplicateResources() { AkonadiResource::removeDuplicateResources(); } ResourceListModel* createResourceListModel(QObject* parent) { return ResourceListModel::create(parent); } ResourceFilterCheckListModel* createResourceFilterCheckListModel(QObject* parent) { return ResourceFilterCheckListModel::create(parent); } AlarmListModel* createAlarmListModel(QObject* parent) { return AlarmListModel::create(parent); } AlarmListModel* allAlarmListModel() { return AlarmListModel::all(); } TemplateListModel* createTemplateListModel(QObject* parent) { return TemplateListModel::create(parent); } TemplateListModel* allTemplateListModel() { return TemplateListModel::all(); } ResourceCreator* createResourceCreator(KAlarmCal::CalEvent::Type defaultType, QWidget* parent) { return new AkonadiResourceCreator(defaultType, parent); } void updateCalendarToCurrentFormat(Resource& resource, bool ignoreKeepFormat, QObject* parent) { AkonadiCalendarUpdater::updateToCurrentFormat(resource, ignoreKeepFormat, parent); } } // namespace DataModel // vim: et sw=4: diff --git a/src/resources/datamodel.h b/src/resources/datamodel.h index d66f9528..27b4ac68 100644 --- a/src/resources/datamodel.h +++ b/src/resources/datamodel.h @@ -1,74 +1,79 @@ /* * datamodel.h - calendar data model dependent functions * Program: kalarm * Copyright © 2019-2020 David Jarvie * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef DATAMODEL_H #define DATAMODEL_H #include class Resource; class ResourceListModel; class ResourceFilterCheckListModel; class AlarmListModel; class TemplateListModel; class ResourceCreator; class QObject; class QWidget; /** Class to create objects dependent on data model. */ namespace DataModel { void initialise(); void terminate(); +/** Disable the widget if the database engine is not available, and display an + * error overlay. + */ +void widgetNeedsDatabase(QWidget*); + /** Reload all resources' data from storage. * @note In the case of Akonadi, this does not reload from the backend storage. */ void reload(); /** Reload a resource's data from storage. * @note In the case of Akonadi, this does not reload from the backend storage. */ bool reload(Resource&); bool isMigrationComplete(); /** Check for, and remove, any duplicate Akonadi resources, i.e. those which * use the same calendar file/directory. */ void removeDuplicateResources(); ResourceListModel* createResourceListModel(QObject* parent); ResourceFilterCheckListModel* createResourceFilterCheckListModel(QObject* parent); AlarmListModel* createAlarmListModel(QObject* parent); AlarmListModel* allAlarmListModel(); TemplateListModel* createTemplateListModel(QObject* parent); TemplateListModel* allTemplateListModel(); ResourceCreator* createResourceCreator(KAlarmCal::CalEvent::Type defaultType, QWidget* parent); void updateCalendarToCurrentFormat(Resource&, bool ignoreKeepFormat, QObject* parent); } // namespace DataModel #endif // DATAMODEL_H // vim: et sw=4: diff --git a/src/resources/resourcedatamodelbase.h b/src/resources/resourcedatamodelbase.h index 00c3820d..7ee8e1f2 100644 --- a/src/resources/resourcedatamodelbase.h +++ b/src/resources/resourcedatamodelbase.h @@ -1,149 +1,154 @@ /* * resourcedatamodelbase.h - base for models containing calendars and events * Program: kalarm * Copyright © 2007-2020 David Jarvie * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef RESOURCEDATAMODELBASE_H #define RESOURCEDATAMODELBASE_H #include "resourcetype.h" +#include "preferences.h" + #include #include class Resource; class QModelIndex; class QPixmap; namespace KAlarmCal { class KAEvent; } using namespace KAlarmCal; /*============================================================================= = Class: ResourceDataModelBase = Base class for models containing all calendars and events. =============================================================================*/ class ResourceDataModelBase { public: /** Data column numbers. */ enum { // Item columns TimeColumn = 0, TimeToColumn, RepeatColumn, ColourColumn, TypeColumn, TextColumn, TemplateNameColumn, ColumnCount }; /** Additional model data roles. */ enum { UserRole = Qt::UserRole + 500, // copied from Akonadi::EntityTreeModel ItemTypeRole = UserRole, // item's type: calendar or event // Calendar roles ResourceIdRole, // the resource ID BaseColourRole, // background colour ignoring collection colour // Event roles EventIdRole, // the event ID ParentResourceIdRole, // the parent resource ID of the event EnabledRole, // true for enabled alarm, false for disabled StatusRole, // KAEvent::ACTIVE/ARCHIVED/TEMPLATE AlarmActionsRole, // KAEvent::Actions AlarmSubActionRole, // KAEvent::Action ValueRole, // numeric value SortRole, // the value to use for sorting TimeDisplayRole, // time column value with '~' representing omitted leading zeroes ColumnTitleRole, // column titles (whether displayed or not) CommandErrorRole // last command execution error for alarm (per user) }; /** The type of a model row. */ enum class Type { Error = 0, Event, Resource }; virtual ~ResourceDataModelBase(); public: + /** Return the data storage backend type used by this model. */ + virtual Preferences::Backend dataStorageBackend() const = 0; + static QSize iconSize() { return mIconSize; } /** Return a bulleted list of alarm types for inclusion in an i18n message. */ static QString typeListForDisplay(CalEvent::Types); /** Get the tooltip for a resource. The resource's enabled status is * evaluated for specified alarm types. */ QString tooltip(const Resource&, CalEvent::Types) const; /** Return the read-only status tooltip for a resource. * A null string is returned if the resource is fully writable. */ static QString readOnlyTooltip(const Resource&); /** Return offset to add to headerData() role, for item models. */ virtual int headerDataEventRoleOffset() const { return 0; } /** Return whether calendar migration/creation at initialisation has completed. */ bool isMigrationComplete() const; protected: ResourceDataModelBase(); static QVariant headerData(int section, Qt::Orientation, int role, bool eventHeaders, bool& handled); /** Return whether resourceData() and/or eventData() handle a role. */ bool roleHandled(int role) const; /** Return the model data for a resource. * @param role may be updated for calling the base model. * @param handled updated to true if the reply is valid, else set to false. */ QVariant resourceData(int& role, const Resource&, bool& handled) const; /** Return the model data for an event. * @param handled updated to true if the reply is valid, else set to false. */ QVariant eventData(int role, int column, const KAEvent& event, const Resource&, bool& handled) const; /** Called when a resource notifies a message to display to the user. */ void handleResourceMessage(ResourceType::MessageType, const QString& message, const QString& details); /** Return whether calendar migration is currently in progress. */ bool isMigrating() const; /** To be called when calendar migration has been initiated (or reset). */ void setMigrationInitiated(bool started = true); /** To be called when calendar migration has been initiated (or reset). */ void setMigrationComplete(); static QString repeatText(const KAEvent&); static QString repeatOrder(const KAEvent&); static QString whatsThisText(int column); static QPixmap* eventIcon(const KAEvent&); private: static QPixmap* mTextIcon; static QPixmap* mFileIcon; static QPixmap* mCommandIcon; static QPixmap* mEmailIcon; static QPixmap* mAudioIcon; static QSize mIconSize; // maximum size of any icon int mMigrationStatus {-1}; // migration status, -1 = no, 0 = initiated, 1 = complete }; #endif // RESOURCEDATAMODELBASE_H // vim: et sw=4: