diff --git a/src/find.cpp b/src/find.cpp index 91f38fda..72345005 100644 --- a/src/find.cpp +++ b/src/find.cpp @@ -1,443 +1,443 @@ /* * find.cpp - search facility * Program: kalarm * Copyright © 2005-2019 David Jarvie * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kalarm.h" #include "find.h" #include "alarmlistview.h" #include "eventlistview.h" #include "messagebox.h" #include "preferences.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include "kalarm_debug.h" using namespace KAlarmCal; // KAlarm-specific options for Find dialog enum { FIND_LIVE = KFind::MinimumUserOption, FIND_ARCHIVED = KFind::MinimumUserOption << 1, FIND_MESSAGE = KFind::MinimumUserOption << 2, FIND_FILE = KFind::MinimumUserOption << 3, FIND_COMMAND = KFind::MinimumUserOption << 4, FIND_EMAIL = KFind::MinimumUserOption << 5, FIND_AUDIO = KFind::MinimumUserOption << 6 }; static long FIND_KALARM_OPTIONS = FIND_LIVE | FIND_ARCHIVED | FIND_MESSAGE | FIND_FILE | FIND_COMMAND | FIND_EMAIL | FIND_AUDIO; Find::Find(EventListView* parent) : QObject(parent), mListView(parent), mDialog(nullptr), mFind(nullptr), mOptions(0), mFound(false) { connect(mListView->selectionModel(), &QItemSelectionModel::currentChanged, this, &Find::slotSelectionChanged); } Find::~Find() { delete mDialog; // automatically set to null delete mFind; mFind = nullptr; } void Find::slotSelectionChanged() { if (mDialog) mDialog->setHasCursor(mListView->selectionModel()->currentIndex().isValid()); } /****************************************************************************** * Display the Find dialog. */ void Find::display() { if (!mOptions) { // Set defaults the first time the Find dialog is activated mOptions = FIND_LIVE | FIND_ARCHIVED | FIND_MESSAGE | FIND_FILE | FIND_COMMAND | FIND_EMAIL | FIND_AUDIO; } bool noArchived = !Preferences::archivedKeepDays(); bool showArchived = qobject_cast(mListView) && (static_cast(mListView->model())->eventTypeFilter() & CalEvent::ARCHIVED); if (noArchived || !showArchived) // these settings could change between activations mOptions &= ~FIND_ARCHIVED; if (mDialog) KWindowSystem::activateWindow(mDialog->winId()); else { mDialog = new KFindDialog(mListView, mOptions, mHistory, (mListView->selectionModel()->selectedRows().count() > 1)); mDialog->setModal(false); mDialog->setObjectName(QStringLiteral("FindDlg")); mDialog->setHasSelection(false); QWidget* kalarmWidgets = mDialog->findExtension(); // Alarm types QVBoxLayout* layout = new QVBoxLayout(kalarmWidgets); layout->setContentsMargins(0, 0, 0, 0); - layout->setSpacing(qApp->style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); + layout->setSpacing(QApplication::style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); QGroupBox* group = new QGroupBox(i18nc("@title:group", "Alarm Type"), kalarmWidgets); layout->addWidget(group); QGridLayout* grid = new QGridLayout(group); - int dcm = qApp->style()->pixelMetric(QStyle::PM_DefaultChildMargin); + int dcm = QApplication::style()->pixelMetric(QStyle::PM_DefaultChildMargin); grid->setContentsMargins(dcm, dcm, dcm, dcm); - grid->setSpacing(qApp->style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); + grid->setSpacing(QApplication::style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); grid->setColumnStretch(1, 1); // Live & archived alarm selection mLive = new QCheckBox(i18nc("@option:check Alarm type", "Active"), group); mLive->setFixedSize(mLive->sizeHint()); mLive->setWhatsThis(i18nc("@info:whatsthis", "Check to include active alarms in the search.")); grid->addWidget(mLive, 1, 0, Qt::AlignLeft); mArchived = new QCheckBox(i18nc("@option:check Alarm type", "Archived"), group); mArchived->setFixedSize(mArchived->sizeHint()); mArchived->setWhatsThis(i18nc("@info:whatsthis", "Check to include archived alarms in the search. " "This option is only available if archived alarms are currently being displayed.")); grid->addWidget(mArchived, 1, 2, Qt::AlignLeft); mActiveArchivedSep = new KSeparator(Qt::Horizontal, kalarmWidgets); grid->addWidget(mActiveArchivedSep, 2, 0, 1, 3); // Alarm actions mMessageType = new QCheckBox(i18nc("@option:check Alarm action = text display", "Text"), group); mMessageType->setFixedSize(mMessageType->sizeHint()); mMessageType->setWhatsThis(i18nc("@info:whatsthis", "Check to include text message alarms in the search.")); grid->addWidget(mMessageType, 3, 0); mFileType = new QCheckBox(i18nc("@option:check Alarm action = file display", "File"), group); mFileType->setFixedSize(mFileType->sizeHint()); mFileType->setWhatsThis(i18nc("@info:whatsthis", "Check to include file alarms in the search.")); grid->addWidget(mFileType, 3, 2); mCommandType = new QCheckBox(i18nc("@option:check Alarm action", "Command"), group); mCommandType->setFixedSize(mCommandType->sizeHint()); mCommandType->setWhatsThis(i18nc("@info:whatsthis", "Check to include command alarms in the search.")); grid->addWidget(mCommandType, 4, 0); mEmailType = new QCheckBox(i18nc("@option:check Alarm action", "Email"), group); mEmailType->setFixedSize(mEmailType->sizeHint()); mEmailType->setWhatsThis(i18nc("@info:whatsthis", "Check to include email alarms in the search.")); grid->addWidget(mEmailType, 4, 2); mAudioType = new QCheckBox(i18nc("@option:check Alarm action", "Audio"), group); mAudioType->setFixedSize(mAudioType->sizeHint()); mAudioType->setWhatsThis(i18nc("@info:whatsthis", "Check to include audio alarms in the search.")); grid->addWidget(mAudioType, 5, 0); // Set defaults mLive->setChecked(mOptions & FIND_LIVE); mArchived->setChecked(mOptions & FIND_ARCHIVED); mMessageType->setChecked(mOptions & FIND_MESSAGE); mFileType->setChecked(mOptions & FIND_FILE); mCommandType->setChecked(mOptions & FIND_COMMAND); mEmailType->setChecked(mOptions & FIND_EMAIL); mAudioType->setChecked(mOptions & FIND_AUDIO); connect(mDialog.data(), &KFindDialog::okClicked, this, &Find::slotFind); } // Only display active/archived options if archived alarms are being kept if (noArchived) { mLive->hide(); mArchived->hide(); mActiveArchivedSep->hide(); } else { mLive->show(); mArchived->show(); mActiveArchivedSep->show(); } // Disable options where no displayed alarms match them bool live = false; bool archived = false; bool text = false; bool file = false; bool command = false; bool email = false; bool audio = false; int rowCount = mListView->model()->rowCount(); for (int row = 0; row < rowCount; ++row) { KAEvent viewEvent = mListView->event(row); const KAEvent* event = &viewEvent; if (event->expired()) archived = true; else live = true; switch (event->actionTypes()) { case KAEvent::ACT_EMAIL: email = true; break; case KAEvent::ACT_AUDIO: audio = true; break; case KAEvent::ACT_COMMAND: command = true; break; case KAEvent::ACT_DISPLAY: if (event->actionSubType() == KAEvent::FILE) { file = true; break; } // fall through to ACT_DISPLAY_COMMAND Q_FALLTHROUGH(); case KAEvent::ACT_DISPLAY_COMMAND: default: text = true; break; } } mLive->setEnabled(live); mArchived->setEnabled(archived); mMessageType->setEnabled(text); mFileType->setEnabled(file); mCommandType->setEnabled(command); mEmailType->setEnabled(email); mAudioType->setEnabled(audio); mDialog->setHasCursor(mListView->selectionModel()->currentIndex().isValid()); mDialog->show(); } /****************************************************************************** * Called when the user requests a search by clicking the dialog OK button. */ void Find::slotFind() { if (!mDialog) return; mHistory = mDialog->findHistory(); // save search history so that it can be displayed again mOptions = mDialog->options() & ~FIND_KALARM_OPTIONS; if ((mOptions & KFind::RegularExpression) && !QRegExp(mDialog->pattern()).isValid()) return; mOptions |= (mLive->isEnabled() && mLive->isChecked() ? FIND_LIVE : 0) | (mArchived->isEnabled() && mArchived->isChecked() ? FIND_ARCHIVED : 0) | (mMessageType->isEnabled() && mMessageType->isChecked() ? FIND_MESSAGE : 0) | (mFileType->isEnabled() && mFileType->isChecked() ? FIND_FILE : 0) | (mCommandType->isEnabled() && mCommandType->isChecked() ? FIND_COMMAND : 0) | (mEmailType->isEnabled() && mEmailType->isChecked() ? FIND_EMAIL : 0) | (mAudioType->isEnabled() && mAudioType->isChecked() ? FIND_AUDIO : 0); if (!(mOptions & (FIND_LIVE | FIND_ARCHIVED)) || !(mOptions & (FIND_MESSAGE | FIND_FILE | FIND_COMMAND | FIND_EMAIL | FIND_AUDIO))) { KAMessageBox::sorry(mDialog, i18nc("@info", "No alarm types are selected to search")); return; } // Supply KFind with only those options which relate to the text within alarms long options = mOptions & (KFind::WholeWordsOnly | KFind::CaseSensitive | KFind::RegularExpression); bool newFind = !mFind; bool newPattern = (mDialog->pattern() != mLastPattern); mLastPattern = mDialog->pattern(); if (mFind) { mFind->resetCounts(); mFind->setPattern(mLastPattern); mFind->setOptions(options); } else { mFind = new KFind(mLastPattern, options, mListView, mDialog); connect(mFind, &KFind::destroyed, this, &Find::slotKFindDestroyed); mFind->closeFindNextDialog(); // prevent 'Find Next' dialog appearing } // Set the starting point for the search mStartID.clear(); mNoCurrentItem = newPattern; bool checkEnd = false; if (newPattern) { mFound = false; if (mOptions & KFind::FromCursor) { QModelIndex index = mListView->selectionModel()->currentIndex(); if (index.isValid()) { mStartID = mListView->event(index).id(); mNoCurrentItem = false; checkEnd = true; } } } // Execute the search findNext(true, checkEnd, false); if (mFind && newFind) Q_EMIT active(true); } /****************************************************************************** * Perform the search. * If 'fromCurrent' is true, the search starts with the current search item; * otherwise, it starts from the next item. */ void Find::findNext(bool forward, bool checkEnd, bool fromCurrent) { QModelIndex index; if (!mNoCurrentItem) index = mListView->selectionModel()->currentIndex(); if (!fromCurrent) index = nextItem(index, forward); // Search successive alarms until a match is found or the end is reached bool found = false; bool last = false; for ( ; index.isValid() && !last; index = nextItem(index, forward)) { KAEvent viewEvent = mListView->event(index); const KAEvent* event = &viewEvent; if (!fromCurrent && !mStartID.isNull() && mStartID == event->id()) last = true; // we've wrapped round and reached the starting alarm again fromCurrent = false; bool live = !event->expired(); if ((live && !(mOptions & FIND_LIVE)) || (!live && !(mOptions & FIND_ARCHIVED))) continue; // we're not searching this type of alarm switch (event->actionTypes()) { case KAEvent::ACT_EMAIL: if (!(mOptions & FIND_EMAIL)) break; mFind->setData(event->emailAddresses(QStringLiteral(", "))); found = (mFind->find() == KFind::Match); if (found) break; mFind->setData(event->emailSubject()); found = (mFind->find() == KFind::Match); if (found) break; mFind->setData(event->emailAttachments().join(QStringLiteral(", "))); found = (mFind->find() == KFind::Match); if (found) break; mFind->setData(event->cleanText()); found = (mFind->find() == KFind::Match); break; case KAEvent::ACT_AUDIO: if (!(mOptions & FIND_AUDIO)) break; mFind->setData(event->audioFile()); found = (mFind->find() == KFind::Match); break; case KAEvent::ACT_COMMAND: if (!(mOptions & FIND_COMMAND)) break; mFind->setData(event->cleanText()); found = (mFind->find() == KFind::Match); break; case KAEvent::ACT_DISPLAY: if (event->actionSubType() == KAEvent::FILE) { if (!(mOptions & FIND_FILE)) break; mFind->setData(event->cleanText()); found = (mFind->find() == KFind::Match); break; } // fall through to ACT_DISPLAY_COMMAND Q_FALLTHROUGH(); case KAEvent::ACT_DISPLAY_COMMAND: if (!(mOptions & FIND_MESSAGE)) break; mFind->setData(event->cleanText()); found = (mFind->find() == KFind::Match); break; default: break; } if (found) break; } // Process the search result mNoCurrentItem = !index.isValid(); if (found) { // A matching alarm was found - highlight it and make it current mFound = true; QItemSelectionModel* sel = mListView->selectionModel(); sel->select(index, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows); sel->setCurrentIndex(index, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows); mListView->scrollTo(index); } else { // No match was found if (mFound || checkEnd) { QString msg = forward ? xi18nc("@info", "End of alarm list reached.Continue from the beginning?") : xi18nc("@info", "Beginning of alarm list reached.Continue from the end?"); if (KAMessageBox::questionYesNo(mListView, msg, QString(), KStandardGuiItem::cont(), KStandardGuiItem::cancel()) == KMessageBox::Yes) { mNoCurrentItem = true; findNext(forward, false, false); return; } } else mFind->displayFinalDialog(); // display "no match was found" mNoCurrentItem = false; // restart from the currently highlighted alarm if Find Next etc selected } } /****************************************************************************** * Get the next alarm item to search. */ QModelIndex Find::nextItem(const QModelIndex& index, bool forward) const { if (mOptions & KFind::FindBackwards) forward = !forward; if (!index.isValid()) { QAbstractItemModel* model = mListView->model(); if (forward) return model->index(0, 0); else return model->index(model->rowCount() - 1, 0); } if (forward) return mListView->indexBelow(index); else return mListView->indexAbove(index); } // vim: et sw=4: diff --git a/src/functions.cpp b/src/functions.cpp index ae356b8c..e86b876e 100644 --- a/src/functions.cpp +++ b/src/functions.cpp @@ -1,2011 +1,2011 @@ /* * functions.cpp - miscellaneous functions * Program: kalarm - * Copyright © 2001-2019 by David Jarvie + * Copyright © 2001-2019 David Jarvie * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kalarm.h" //krazy:exclude=includes (kalarm.h must be first) #include "functions.h" #include "functions_p.h" #include "collectionmodel.h" #include "collectionsearch.h" #include "alarmcalendar.h" #include "alarmtime.h" #include "autoqpointer.h" #include "alarmlistview.h" #include "editdlg.h" #include "kalarmapp.h" #include "kamail.h" #include "mainwindow.h" #include "messagebox.h" #include "messagewin.h" #include "preferences.h" #include "shellprocess.h" #include "templatelistview.h" #include "templatemenuaction.h" #include "config-kalarm.h" #include #include #include #include #include #include using namespace KCalCore; #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if KDEPIM_HAVE_X11 #include #include #include #include #include #include #endif #include #include #include #include #include #include #include #include #include #include "kalarm_debug.h" #if KDEPIM_HAVE_X11 #include #include #endif using namespace Akonadi; namespace { bool refreshAlarmsQueued = false; QDBusInterface* korgInterface = nullptr; struct UpdateStatusData { KAlarm::UpdateResult status; // status code and KOrganizer error message if any int warnErr; int warnKOrg; explicit UpdateStatusData(KAlarm::UpdateStatus s = KAlarm::UPDATE_OK) : status(s), warnErr(0), warnKOrg(0) {} // Set an error status and increment to number of errors to warn about void setError(KAlarm::UpdateStatus st, int errorCount = -1) { status.set(st); if (errorCount < 0) ++warnErr; else warnErr = errorCount; } // Update the error status with a KOrganizer related status void korgUpdate(const KAlarm::UpdateResult& result) { if (result.status != KAlarm::UPDATE_OK) { ++warnKOrg; if (result.status > status.status) status = result; } } }; const QLatin1String KMAIL_DBUS_SERVICE("org.kde.kmail"); //const QLatin1String KMAIL_DBUS_IFACE("org.kde.kmail.kmail"); //const QLatin1String KMAIL_DBUS_WINDOW_PATH("/kmail/kmail_mainwindow_1"); const QLatin1String KORG_DBUS_SERVICE("org.kde.korganizer"); const QLatin1String KORG_DBUS_IFACE("org.kde.korganizer.Korganizer"); // D-Bus object path of KOrganizer's notification interface #define KORG_DBUS_PATH "/Korganizer" #define KORG_DBUS_LOAD_PATH "/korganizer_PimApplication" //const QLatin1String KORG_DBUS_WINDOW_PATH("/korganizer/MainWindow_1"); const QLatin1String KORG_MIME_TYPE("application/x-vnd.akonadi.calendar.event"); const QLatin1String KORGANIZER_UID("-korg"); const QLatin1String ALARM_OPTS_FILE("alarmopts"); const char* DONT_SHOW_ERRORS_GROUP = "DontShowErrors"; void editNewTemplate(EditAlarmDlg::Type, const KAEvent* preset, QWidget* parent); void displayUpdateError(QWidget* parent, KAlarm::UpdateError, const UpdateStatusData&, bool showKOrgError = true); KAlarm::UpdateResult sendToKOrganizer(const KAEvent&); KAlarm::UpdateResult deleteFromKOrganizer(const QString& eventID); KAlarm::UpdateResult runKOrganizer(); QString uidKOrganizer(const QString& eventID); } namespace KAlarm { Private* Private::mInstance = nullptr; /****************************************************************************** * Display a main window with the specified event selected. */ MainWindow* displayMainWindowSelected(Akonadi::Item::Id eventId) { MainWindow* win = MainWindow::firstWindow(); if (!win) { if (theApp()->checkCalendar()) // ensure calendar is open { win = MainWindow::create(); win->show(); } } else { // There is already a main window, so make it the active window win->hide(); // in case it's on a different desktop win->setWindowState(win->windowState() & ~Qt::WindowMinimized); win->show(); win->raise(); win->activateWindow(); } if (win && eventId >= 0) win->selectEvent(eventId); return win; } /****************************************************************************** * Create an "Alarms Enabled/Enable Alarms" action. */ KToggleAction* createAlarmEnableAction(QObject* parent) { KToggleAction* action = new KToggleAction(i18nc("@action", "Enable &Alarms"), parent); action->setChecked(theApp()->alarmsEnabled()); QObject::connect(action, &QAction::toggled, theApp(), &KAlarmApp::setAlarmsEnabled); // The following line ensures that all instances are kept in the same state QObject::connect(theApp(), &KAlarmApp::alarmEnabledToggled, action, &QAction::setChecked); return action; } /****************************************************************************** * Create a "Stop Play" action. */ QAction* createStopPlayAction(QObject* parent) { QAction* action = new QAction(QIcon::fromTheme(QStringLiteral("media-playback-stop")), i18nc("@action", "Stop Play"), parent); action->setEnabled(MessageWin::isAudioPlaying()); QObject::connect(action, &QAction::triggered, theApp(), &KAlarmApp::stopAudio); // The following line ensures that all instances are kept in the same state QObject::connect(theApp(), &KAlarmApp::audioPlaying, action, &QAction::setEnabled); return action; } /****************************************************************************** * Create a "Spread Windows" action. */ KToggleAction* createSpreadWindowsAction(QObject* parent) { KToggleAction* action = new KToggleAction(i18nc("@action", "Spread Windows"), parent); QObject::connect(action, &QAction::triggered, theApp(), &KAlarmApp::spreadWindows); // The following line ensures that all instances are kept in the same state QObject::connect(theApp(), &KAlarmApp::spreadWindowsToggled, action, &QAction::setChecked); return action; } /****************************************************************************** * Add a new active (non-archived) alarm. * Save it in the calendar file and add it to every main window instance. * Parameters: msgParent = parent widget for any calendar selection prompt or * error message. * event - is updated with the actual event ID. */ UpdateResult addEvent(KAEvent& event, Collection* calendar, QWidget* msgParent, int options, bool showKOrgErr) { qCDebug(KALARM_LOG) << "addEvent:" << event.id(); bool cancelled = false; UpdateStatusData status; if (!theApp()->checkCalendar()) // ensure calendar is open status.status = UPDATE_FAILED; else { // Save the event details in the calendar file, and get the new event ID AlarmCalendar* cal = AlarmCalendar::resources(); // Note that AlarmCalendar::addEvent() updates 'event'. if (!cal->addEvent(event, msgParent, (options & USE_EVENT_ID), calendar, (options & NO_RESOURCE_PROMPT), &cancelled)) { status.status = UPDATE_FAILED; } else { if (!cal->save()) status.status = SAVE_FAILED; } if (status.status == UPDATE_OK) { if ((options & ALLOW_KORG_UPDATE) && event.copyToKOrganizer()) { UpdateResult st = sendToKOrganizer(event); // tell KOrganizer to show the event status.korgUpdate(st); } } } if (status.status != UPDATE_OK && !cancelled && msgParent) displayUpdateError(msgParent, ERR_ADD, status, showKOrgErr); return status.status; } /****************************************************************************** * Add a list of new active (non-archived) alarms. * Save them in the calendar file and add them to every main window instance. * The events are updated with their actual event IDs. */ UpdateResult addEvents(QVector& events, QWidget* msgParent, bool allowKOrgUpdate, bool showKOrgErr) { qCDebug(KALARM_LOG) << "addEvents:" << events.count(); if (events.isEmpty()) return UpdateResult(UPDATE_OK); UpdateStatusData status; Collection collection; if (!theApp()->checkCalendar()) // ensure calendar is open status.status = UPDATE_FAILED; else { collection = CollectionControlModel::instance()->destination(CalEvent::ACTIVE, msgParent); if (!collection.isValid()) { qCDebug(KALARM_LOG) << "addEvents: No calendar"; status.status = UPDATE_FAILED; } } if (status.status == UPDATE_OK) { AlarmCalendar* cal = AlarmCalendar::resources(); for (int i = 0, end = events.count(); i < end; ++i) { // Save the event details in the calendar file, and get the new event ID if (!cal->addEvent(events[i], msgParent, false, &collection)) { status.setError(UPDATE_ERROR); continue; } if (allowKOrgUpdate && events[i].copyToKOrganizer()) { UpdateResult st = sendToKOrganizer(events[i]); // tell KOrganizer to show the event status.korgUpdate(st); } } if (status.warnErr == events.count()) status.status = UPDATE_FAILED; else if (!cal->save()) status.setError(SAVE_FAILED, events.count()); // everything failed } if (status.status != UPDATE_OK && msgParent) displayUpdateError(msgParent, ERR_ADD, status, showKOrgErr); return status.status; } /****************************************************************************** * Save the event in the archived calendar and adjust every main window instance. * The event's ID is changed to an archived ID if necessary. */ bool addArchivedEvent(KAEvent& event, Collection* collection) { qCDebug(KALARM_LOG) << "addArchivedEvent:" << event.id(); bool archiving = (event.category() == CalEvent::ACTIVE); if (archiving && !Preferences::archivedKeepDays()) return false; // expired alarms aren't being kept AlarmCalendar* cal = AlarmCalendar::resources(); KAEvent newevent(event); newevent.setItemId(-1); // invalidate the Akonadi item ID since it's a new item KAEvent* const newev = &newevent; if (archiving) { newev->setCategory(CalEvent::ARCHIVED); // this changes the event ID newev->setCreatedDateTime(KADateTime::currentUtcDateTime()); // time stamp to control purging } // Note that archived resources are automatically saved after changes are made if (!cal->addEvent(newevent, nullptr, false, collection)) return false; event = *newev; // update event ID etc. return true; } /****************************************************************************** * Add a new template. * Save it in the calendar file and add it to every template list view. * 'event' is updated with the actual event ID. * Parameters: promptParent = parent widget for any calendar selection prompt. */ UpdateResult addTemplate(KAEvent& event, Collection* collection, QWidget* msgParent) { qCDebug(KALARM_LOG) << "addTemplate:" << event.id(); UpdateStatusData status; // Add the template to the calendar file AlarmCalendar* cal = AlarmCalendar::resources(); KAEvent newev(event); if (!cal->addEvent(newev, msgParent, false, collection)) status.status = UPDATE_FAILED; else { event = newev; // update event ID etc. if (!cal->save()) status.status = SAVE_FAILED; else { return UpdateResult(UPDATE_OK); } } if (msgParent) displayUpdateError(msgParent, ERR_TEMPLATE, status); return status.status; } /****************************************************************************** * Modify an active (non-archived) alarm in the calendar file and in every main * window instance. * The new event must have a different event ID from the old one. */ UpdateResult modifyEvent(KAEvent& oldEvent, KAEvent& newEvent, QWidget* msgParent, bool showKOrgErr) { qCDebug(KALARM_LOG) << "modifyEvent:" << oldEvent.id(); UpdateStatusData status; if (!newEvent.isValid()) { deleteEvent(oldEvent, true); status.status = UPDATE_FAILED; } else { EventId oldId(oldEvent); if (oldEvent.copyToKOrganizer()) { // Tell KOrganizer to delete its old event. // But ignore errors, because the user could have manually // deleted it since KAlarm asked KOrganizer to set it up. deleteFromKOrganizer(oldId.eventId()); } // Update the event in the calendar file, and get the new event ID AlarmCalendar* cal = AlarmCalendar::resources(); if (!cal->modifyEvent(oldId, newEvent)) status.status = UPDATE_FAILED; else { if (!cal->save()) status.status = SAVE_FAILED; if (status.status == UPDATE_OK) { if (newEvent.copyToKOrganizer()) { UpdateResult st = sendToKOrganizer(newEvent); // tell KOrganizer to show the new event status.korgUpdate(st); } // Remove "Don't show error messages again" for the old alarm setDontShowErrors(oldId); } } } if (status.status != UPDATE_OK && msgParent) displayUpdateError(msgParent, ERR_MODIFY, status, showKOrgErr); return status.status; } /****************************************************************************** * Update an active (non-archived) alarm from the calendar file and from every * main window instance. * The new event will have the same event ID as the old one. * The event is not updated in KOrganizer, since this function is called when an * existing alarm is rescheduled (due to recurrence or deferral). */ UpdateResult updateEvent(KAEvent& event, QWidget* msgParent, bool archiveOnDelete) { qCDebug(KALARM_LOG) << "updateEvent:" << event.id(); if (!event.isValid()) deleteEvent(event, archiveOnDelete); else { // Update the event in the calendar file. AlarmCalendar* cal = AlarmCalendar::resources(); cal->updateEvent(event); if (!cal->save()) { if (msgParent) displayUpdateError(msgParent, ERR_ADD, UpdateStatusData(SAVE_FAILED)); return UpdateResult(SAVE_FAILED); } } return UpdateResult(UPDATE_OK); } /****************************************************************************** * Update a template in the calendar file and in every template list view. * If 'selectionView' is non-null, the selection highlight is moved to the * updated event in that listView instance. */ UpdateResult updateTemplate(KAEvent& event, QWidget* msgParent) { AlarmCalendar* cal = AlarmCalendar::resources(); KAEvent* newEvent = cal->updateEvent(event); UpdateStatus status = UPDATE_OK; if (!newEvent) status = UPDATE_FAILED; else if (!cal->save()) status = SAVE_FAILED; if (status != UPDATE_OK) { if (msgParent) displayUpdateError(msgParent, ERR_TEMPLATE, UpdateStatusData(SAVE_FAILED)); return UpdateResult(status); } return UpdateResult(UPDATE_OK); } /****************************************************************************** * Delete alarms from the calendar file and from every main window instance. * If the events are archived, the events' IDs are changed to archived IDs if necessary. */ UpdateResult deleteEvent(KAEvent& event, bool archive, QWidget* msgParent, bool showKOrgErr) { QVector events(1, event); return deleteEvents(events, archive, msgParent, showKOrgErr); } UpdateResult deleteEvents(QVector& events, bool archive, QWidget* msgParent, bool showKOrgErr) { qCDebug(KALARM_LOG) << "deleteEvents:" << events.count(); if (events.isEmpty()) return UpdateResult(UPDATE_OK); UpdateStatusData status; AlarmCalendar* cal = AlarmCalendar::resources(); bool deleteWakeFromSuspendAlarm = false; QString wakeFromSuspendId = checkRtcWakeConfig().value(0); for (int i = 0, end = events.count(); i < end; ++i) { // Save the event details in the calendar file, and get the new event ID KAEvent* event = &events[i]; QString id = event->id(); // Delete the event from the calendar file if (event->category() != CalEvent::ARCHIVED) { if (event->copyToKOrganizer()) { // The event was shown in KOrganizer, so tell KOrganizer to // delete it. But ignore errors, because the user could have // manually deleted it from KOrganizer since it was set up. UpdateResult st = deleteFromKOrganizer(id); status.korgUpdate(st); } if (archive && event->toBeArchived()) { KAEvent ev(*event); addArchivedEvent(ev); // this changes the event ID to an archived ID } } if (!cal->deleteEvent(*event, false)) // don't save calendar after deleting status.setError(UPDATE_ERROR); if (id == wakeFromSuspendId) deleteWakeFromSuspendAlarm = true; // Remove "Don't show error messages again" for this alarm setDontShowErrors(EventId(*event)); } if (status.warnErr == events.count()) status.status = UPDATE_FAILED; else if (!cal->save()) // save the calendars now status.setError(SAVE_FAILED, events.count()); if (status.status != UPDATE_OK && msgParent) displayUpdateError(msgParent, ERR_DELETE, status, showKOrgErr); // Remove any wake-from-suspend scheduled for a deleted alarm if (deleteWakeFromSuspendAlarm && !wakeFromSuspendId.isEmpty()) cancelRtcWake(msgParent, wakeFromSuspendId); return status.status; } /****************************************************************************** * Delete templates from the calendar file and from every template list view. */ UpdateResult deleteTemplates(const KAEvent::List& events, QWidget* msgParent) { int count = events.count(); qCDebug(KALARM_LOG) << "deleteTemplates:" << count; if (!count) return UpdateResult(UPDATE_OK); UpdateStatusData status; AlarmCalendar* cal = AlarmCalendar::resources(); for (int i = 0, end = count; i < end; ++i) { // Update the window lists // Delete the template from the calendar file AlarmCalendar* cal = AlarmCalendar::resources(); if (!cal->deleteEvent(*events[i], false)) // don't save calendar after deleting status.setError(UPDATE_ERROR); } if (status.warnErr == count) status.status = UPDATE_FAILED; else if (!cal->save()) // save the calendars now status.setError(SAVE_FAILED, count); if (status.status != UPDATE_OK && msgParent) displayUpdateError(msgParent, ERR_TEMPLATE, status); return status.status; } /****************************************************************************** * Delete an alarm from the display calendar. */ void deleteDisplayEvent(const QString& eventID) { qCDebug(KALARM_LOG) << "deleteDisplayEvent:" << eventID; AlarmCalendar* cal = AlarmCalendar::displayCalendarOpen(); if (cal) cal->deleteDisplayEvent(eventID, true); // save calendar after deleting } /****************************************************************************** * Undelete archived alarms, and update every main window instance. * The archive bit is set to ensure that they get re-archived if deleted again. * 'ineligibleIDs' is filled in with the IDs of any ineligible events. */ UpdateResult reactivateEvent(KAEvent& event, Collection* calendar, QWidget* msgParent, bool showKOrgErr) { QVector ids; QVector events(1, event); return reactivateEvents(events, ids, calendar, msgParent, showKOrgErr); } UpdateResult reactivateEvents(QVector& events, QVector& ineligibleIDs, Collection* col, QWidget* msgParent, bool showKOrgErr) { qCDebug(KALARM_LOG) << "reactivateEvents:" << events.count(); ineligibleIDs.clear(); if (events.isEmpty()) return UpdateResult(UPDATE_OK); UpdateStatusData status; Collection collection; if (col) collection = *col; if (!collection.isValid()) collection = CollectionControlModel::instance()->destination(CalEvent::ACTIVE, msgParent); if (!collection.isValid()) { qCDebug(KALARM_LOG) << "reactivateEvents: No calendar"; status.setError(UPDATE_FAILED, events.count()); } else { int count = 0; AlarmCalendar* cal = AlarmCalendar::resources(); const KADateTime now = KADateTime::currentUtcDateTime(); for (int i = 0, end = events.count(); i < end; ++i) { // Delete the event from the archived resource KAEvent* event = &events[i]; if (event->category() != CalEvent::ARCHIVED || !event->occursAfter(now, true)) { ineligibleIDs += EventId(*event); continue; } ++count; KAEvent newevent(*event); KAEvent* const newev = &newevent; newev->setCategory(CalEvent::ACTIVE); // this changes the event ID if (newev->recurs() || newev->repetition()) newev->setNextOccurrence(now); // skip any recurrences in the past newev->setArchive(); // ensure that it gets re-archived if it is deleted // Save the event details in the calendar file. // This converts the event ID. if (!cal->addEvent(newevent, msgParent, true, &collection)) { status.setError(UPDATE_ERROR); continue; } if (newev->copyToKOrganizer()) { UpdateResult st = sendToKOrganizer(*newev); // tell KOrganizer to show the event status.korgUpdate(st); } if (cal->event(EventId(*event)) // no error if event doesn't exist in archived resource && !cal->deleteEvent(*event, false)) // don't save calendar after deleting status.setError(UPDATE_ERROR); events[i] = newevent; } if (status.warnErr == count) status.status = UPDATE_FAILED; // Save the calendars, even if all events failed, since more than one calendar was updated if (!cal->save() && status.status != UPDATE_FAILED) status.setError(SAVE_FAILED, count); } if (status.status != UPDATE_OK && msgParent) displayUpdateError(msgParent, ERR_REACTIVATE, status, showKOrgErr); return status.status; } /****************************************************************************** * Enable or disable alarms in the calendar file and in every main window instance. * The new events will have the same event IDs as the old ones. */ UpdateResult enableEvents(QVector& events, bool enable, QWidget* msgParent) { qCDebug(KALARM_LOG) << "enableEvents:" << events.count(); if (events.isEmpty()) return UpdateResult(UPDATE_OK); UpdateStatusData status; AlarmCalendar* cal = AlarmCalendar::resources(); bool deleteWakeFromSuspendAlarm = false; QString wakeFromSuspendId = checkRtcWakeConfig().value(0); for (int i = 0, end = events.count(); i < end; ++i) { KAEvent* event = &events[i]; if (event->category() == CalEvent::ACTIVE && enable != event->enabled()) { event->setEnabled(enable); if (!enable && event->id() == wakeFromSuspendId) deleteWakeFromSuspendAlarm = true; // Update the event in the calendar file KAEvent* newev = cal->updateEvent(event); if (!newev) qCCritical(KALARM_LOG) << "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) << "purgeArchive:" << purgeDays; const QDate cutoff = KADateTime::currentLocalDate().addDays(-purgeDays); Collection collection = CollectionControlModel::getStandard(CalEvent::ARCHIVED); if (!collection.isValid()) return; KAEvent::List events = AlarmCalendar::resources()->events(collection); for (int i = 0; i < events.count(); ) { if (purgeDays && events.at(i)->createdDateTime().date() >= cutoff) events.remove(i); else ++i; } if (!events.isEmpty()) AlarmCalendar::resources()->purgeEvents(events); // delete the events and save the calendar } /****************************************************************************** * Display an error message about an error when saving an event. * If 'model' is non-null, the AlarmListModel* which it points to is used; if * that is null, it is created. */ QVector getSortedActiveEvents(QObject* parent, AlarmListModel** model) { AlarmListModel* mdl = nullptr; if (!model) model = &mdl; if (!*model) { *model = new AlarmListModel(parent); (*model)->setEventTypeFilter(CalEvent::ACTIVE); (*model)->sort(AlarmListModel::TimeColumn); } QVector result; for (int i = 0, count = (*model)->rowCount(); i < count; ++i) { KAEvent event = (*model)->event(i); if (event.enabled() && !event.expired()) result += event; } return result; } /****************************************************************************** * Display an error message corresponding to a specified alarm update error code. */ void displayKOrgUpdateError(QWidget* parent, UpdateError code, const UpdateResult& korgError, int nAlarms) { QString errmsg; switch (code) { case ERR_ADD: case ERR_REACTIVATE: errmsg = (nAlarms > 1) ? i18nc("@info", "Unable to show alarms in KOrganizer") : i18nc("@info", "Unable to show alarm in KOrganizer"); break; case ERR_MODIFY: errmsg = i18nc("@info", "Unable to update alarm in KOrganizer"); break; case ERR_DELETE: errmsg = (nAlarms > 1) ? i18nc("@info", "Unable to delete alarms from KOrganizer") : i18nc("@info", "Unable to delete alarm from KOrganizer"); break; case ERR_TEMPLATE: return; } bool showDetail = !korgError.message.isEmpty(); QString msg; switch (korgError.status) { case UPDATE_KORG_ERRINIT: msg = xi18nc("@info", "%1(Could not start KOrganizer)", errmsg); break; case UPDATE_KORG_ERRSTART: msg = xi18nc("@info", "%1(KOrganizer not fully started)", errmsg); break; case UPDATE_KORG_ERR: msg = xi18nc("@info", "%1(Error communicating with KOrganizer)", errmsg); break; default: msg = errmsg; showDetail = false; break; } if (showDetail) KAMessageBox::detailedError(parent, msg, korgError.message); else KAMessageBox::error(parent, msg); } /****************************************************************************** * Execute a New Alarm dialog for the specified alarm type. */ void editNewAlarm(EditAlarmDlg::Type type, QWidget* parent) { execNewAlarmDlg(EditAlarmDlg::create(false, type, parent)); } /****************************************************************************** * Execute a New Alarm dialog for the specified alarm type. */ void editNewAlarm(KAEvent::SubAction action, QWidget* parent, const AlarmText* text) { bool setAction = false; EditAlarmDlg::Type type; switch (action) { case KAEvent::MESSAGE: case KAEvent::FILE: type = EditAlarmDlg::DISPLAY; setAction = true; break; case KAEvent::COMMAND: type = EditAlarmDlg::COMMAND; break; case KAEvent::EMAIL: type = EditAlarmDlg::EMAIL; break; case KAEvent::AUDIO: type = EditAlarmDlg::AUDIO; break; default: return; } EditAlarmDlg* editDlg = EditAlarmDlg::create(false, type, parent); if (setAction || text) editDlg->setAction(action, *text); execNewAlarmDlg(editDlg); } /****************************************************************************** * Execute a New Alarm dialog, optionally either presetting it to the supplied * event, or setting the action and text. */ void editNewAlarm(const KAEvent* preset, QWidget* parent) { execNewAlarmDlg(EditAlarmDlg::create(false, preset, true, parent)); } /****************************************************************************** * Common code for editNewAlarm() variants. */ void execNewAlarmDlg(EditAlarmDlg* editDlg) { // Create a PrivateNewAlarmDlg parented by editDlg. // It will be deleted when editDlg is closed. new PrivateNewAlarmDlg(editDlg); editDlg->show(); editDlg->raise(); editDlg->activateWindow(); } PrivateNewAlarmDlg::PrivateNewAlarmDlg(EditAlarmDlg* dlg) : QObject(dlg) { connect(dlg, &QDialog::accepted, this, &PrivateNewAlarmDlg::okClicked); connect(dlg, &QDialog::rejected, this, &PrivateNewAlarmDlg::cancelClicked); } /****************************************************************************** * Called when the dialogue is accepted (e.g. by clicking the OK button). * Creates the event specified in the instance's dialogue. */ void PrivateNewAlarmDlg::okClicked() { accept(static_cast(parent())); } /****************************************************************************** * Creates the event specified in a given dialogue. */ void PrivateNewAlarmDlg::accept(EditAlarmDlg* editDlg) { KAEvent event; Collection calendar; editDlg->getEvent(event, calendar); // Add the alarm to the displayed lists and to the calendar file UpdateResult status = addEvent(event, &calendar, editDlg); switch (status.status) { case UPDATE_FAILED: return; case UPDATE_KORG_ERR: case UPDATE_KORG_ERRINIT: case UPDATE_KORG_ERRSTART: case UPDATE_KORG_FUNCERR: displayKOrgUpdateError(editDlg, ERR_ADD, status); break; default: break; } Undo::saveAdd(event, calendar); outputAlarmWarnings(editDlg, &event); editDlg->deleteLater(); } /****************************************************************************** * Called when the dialogue is rejected (e.g. by clicking the Cancel button). */ void PrivateNewAlarmDlg::cancelClicked() { static_cast(parent())->deleteLater(); } /****************************************************************************** * Display the alarm edit dialog to edit a new alarm, preset with a template. */ bool editNewAlarm(const QString& templateName, QWidget* parent) { if (!templateName.isEmpty()) { KAEvent* templateEvent = AlarmCalendar::resources()->templateEvent(templateName); if (templateEvent->isValid()) { editNewAlarm(templateEvent, parent); return true; } qCWarning(KALARM_LOG) << "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); } /****************************************************************************** * Find the identity of the desktop we are running on. */ QString currentDesktopIdentityName() { return QProcessEnvironment::systemEnvironment().value(QStringLiteral("XDG_CURRENT_DESKTOP")); } /****************************************************************************** * Find the identity of the desktop we are running on. */ Desktop currentDesktopIdentity() { const QString desktop = currentDesktopIdentityName(); if (desktop == QStringLiteral("KDE")) return Desktop::Kde; if (desktop == QStringLiteral("Unity")) return Desktop::Unity; return Desktop::Other; } /****************************************************************************** * Check the config as to whether there is a wake-on-suspend alarm pending, and * if so, delete it from the config if it has expired. * If 'checkExists' is true, the config entry will only be returned if the * event exists. * Reply = config entry: [0] = event's collection ID (Akonadi only), * [1] = event ID, * [2] = trigger time (time_t). * = empty list if none or expired. */ QStringList checkRtcWakeConfig(bool checkEventExists) { KConfigGroup config(KSharedConfig::openConfig(), "General"); const QStringList params = config.readEntry("RtcWake", QStringList()); if (params.count() == 3 && params[2].toUInt() > KADateTime::currentUtcDateTime().toTime_t()) { if (checkEventExists && !AlarmCalendar::getEvent(EventId(params[0].toLongLong(), params[1]))) return QStringList(); return params; // config entry is valid } if (!params.isEmpty()) { config.deleteEntry("RtcWake"); // delete the expired config entry config.sync(); } return QStringList(); } /****************************************************************************** * Delete any wake-on-suspend alarm from the config. */ void deleteRtcWakeConfig() { KConfigGroup config(KSharedConfig::openConfig(), "General"); config.deleteEntry("RtcWake"); config.sync(); } /****************************************************************************** * Delete any wake-on-suspend alarm, optionally only for a specified event. */ void cancelRtcWake(QWidget* msgParent, const QString& eventId) { const QStringList wakeup = checkRtcWakeConfig(); if (!wakeup.isEmpty() && (eventId.isEmpty() || wakeup[0] == eventId)) { Private::instance()->mMsgParent = msgParent ? msgParent : MainWindow::mainMainWindow(); QTimer::singleShot(0, Private::instance(), &Private::cancelRtcWake); } } /****************************************************************************** * Delete any wake-on-suspend alarm. */ void Private::cancelRtcWake() { // setRtcWakeTime will only work with a parent window specified setRtcWakeTime(0, mMsgParent); deleteRtcWakeConfig(); KAMessageBox::information(mMsgParent, i18nc("info", "The scheduled Wake from Suspend has been cancelled.")); } /****************************************************************************** * Set the wakeup time for the system. * Set 'triggerTime' to zero to cancel the wakeup. * Reply = true if successful. */ bool setRtcWakeTime(unsigned triggerTime, QWidget* parent) { QVariantMap args; args[QStringLiteral("time")] = triggerTime; KAuth::Action action(QStringLiteral("org.kde.kalarm.rtcwake.settimer")); action.setHelperId(QStringLiteral("org.kde.kalarm.rtcwake")); action.setParentWidget(parent); action.setArguments(args); KAuth::ExecuteJob* job = action.execute(); if (!job->exec()) { QString errmsg = job->errorString(); qCDebug(KALARM_LOG) << "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) << "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 (CollectionControlModel::enabledCollections(CalEvent::TEMPLATE, true).isEmpty()) { KAMessageBox::sorry(parent, i18nc("@info", "You must enable a template calendar to save the template in")); return; } // Use AutoQPointer to guard against crash on application exit while // the dialogue is still open. It prevents double deletion (both on // deletion of parent, and on return from this function). AutoQPointer editDlg; if (preset) editDlg = EditAlarmDlg::create(true, preset, true, parent); else editDlg = EditAlarmDlg::create(true, type, parent); if (editDlg->exec() == QDialog::Accepted) { KAEvent event; Akonadi::Collection calendar; editDlg->getEvent(event, calendar); // Add the template to the displayed lists and to the calendar file KAlarm::addTemplate(event, &calendar, editDlg); Undo::saveAdd(event, calendar); } } } // namespace namespace KAlarm { /****************************************************************************** * Open the Edit Alarm dialog to edit the specified alarm. * If the alarm is read-only or archived, the dialog is opened read-only. */ void editAlarm(KAEvent* event, QWidget* parent) { if (event->expired() || AlarmCalendar::resources()->eventReadOnly(event->itemId())) { viewAlarm(event, parent); return; } EventId id(*event); // Use AutoQPointer to guard against crash on application exit while // the dialogue is still open. It prevents double deletion (both on // deletion of parent, and on return from this function). AutoQPointer editDlg = EditAlarmDlg::create(false, event, false, parent, EditAlarmDlg::RES_USE_EVENT_ID); if (editDlg->exec() == QDialog::Accepted) { if (!AlarmCalendar::resources()->event(id)) { // Event has been deleted while the user was editing the alarm, // so treat it as a new alarm. PrivateNewAlarmDlg().accept(editDlg); return; } KAEvent newEvent; Collection calendar; bool changeDeferral = !editDlg->getEvent(newEvent, calendar); // Update the event in the displays and in the calendar file Undo::Event undo(*event, calendar); if (changeDeferral) { // The only change has been to an existing deferral if (updateEvent(newEvent, editDlg, true) != UPDATE_OK) // keep the same event ID return; // failed to save event } else { UpdateResult status = modifyEvent(*event, newEvent, editDlg); if (status.status != UPDATE_OK && status.status <= UPDATE_KORG_ERR) displayKOrgUpdateError(editDlg, ERR_MODIFY, status); } Undo::saveEdit(undo, newEvent); outputAlarmWarnings(editDlg, &newEvent); } } /****************************************************************************** * Display the alarm edit dialog to edit the alarm with the specified ID. * An error occurs if the alarm is not found, if there is more than one alarm * with the same ID, or if it is read-only or expired. */ bool editAlarmById(const EventId& id, QWidget* parent) { const QString eventID(id.eventId()); KAEvent* event = AlarmCalendar::resources()->event(id, true); if (!event) { if (id.collectionId() != -1) qCWarning(KALARM_LOG) << "editAlarmById: Event ID not found, or duplicated:" << eventID; else qCWarning(KALARM_LOG) << "editAlarmById: Event ID not found:" << eventID; return false; } if (AlarmCalendar::resources()->eventReadOnly(event->itemId())) { qCCritical(KALARM_LOG) << "editAlarmById:" << eventID << ": read-only"; return false; } switch (event->category()) { case CalEvent::ACTIVE: case CalEvent::TEMPLATE: break; default: qCCritical(KALARM_LOG) << "editAlarmById:" << eventID << ": event not active or template"; return false; } editAlarm(event, parent); return true; } /****************************************************************************** * Open the Edit Alarm dialog to edit the specified template. * If the template is read-only, the dialog is opened read-only. */ void editTemplate(KAEvent* event, QWidget* parent) { if (AlarmCalendar::resources()->eventReadOnly(event->itemId())) { // The template is read-only, so make the dialogue read-only. // Use AutoQPointer to guard against crash on application exit while // the dialogue is still open. It prevents double deletion (both on // deletion of parent, and on return from this function). AutoQPointer editDlg = EditAlarmDlg::create(true, event, false, parent, EditAlarmDlg::RES_PROMPT, true); editDlg->exec(); return; } // Use AutoQPointer to guard against crash on application exit while // the dialogue is still open. It prevents double deletion (both on // deletion of parent, and on return from this function). AutoQPointer editDlg = EditAlarmDlg::create(true, event, false, parent, EditAlarmDlg::RES_USE_EVENT_ID); if (editDlg->exec() == QDialog::Accepted) { KAEvent newEvent; Akonadi::Collection calendar; editDlg->getEvent(newEvent, calendar); QString id = event->id(); newEvent.setEventId(id); newEvent.setCollectionId(event->collectionId()); newEvent.setItemId(event->itemId()); // Update the event in the displays and in the calendar file Undo::Event undo(*event, calendar); updateTemplate(newEvent, editDlg); Undo::saveEdit(undo, newEvent); } } /****************************************************************************** * Open the Edit Alarm dialog to view the specified alarm (read-only). */ void viewAlarm(const KAEvent* event, QWidget* parent) { // Use AutoQPointer to guard against crash on application exit while // the dialogue is still open. It prevents double deletion (both on // deletion of parent, and on return from this function). AutoQPointer editDlg = EditAlarmDlg::create(false, event, false, parent, EditAlarmDlg::RES_PROMPT, true); editDlg->exec(); } /****************************************************************************** * Called when OK is clicked in the alarm edit dialog invoked by the Edit button * in an alarm message window. * Updates the alarm calendar and closes the dialog. */ void updateEditedAlarm(EditAlarmDlg* editDlg, KAEvent& event, Collection& calendar) { qCDebug(KALARM_LOG) << "updateEditedAlarm"; KAEvent newEvent; Akonadi::Collection cal; editDlg->getEvent(newEvent, cal); // Update the displayed lists and the calendar file UpdateResult status; if (AlarmCalendar::resources()->event(EventId(event))) { // The old alarm hasn't expired yet, so replace it Undo::Event undo(event, calendar); status = modifyEvent(event, newEvent, editDlg); Undo::saveEdit(undo, newEvent); } else { // The old event has expired, so simply create a new one status = addEvent(newEvent, &calendar, editDlg); Undo::saveAdd(newEvent, calendar); } if (status.status != UPDATE_OK && status.status <= UPDATE_KORG_ERR) displayKOrgUpdateError(editDlg, ERR_MODIFY, status); outputAlarmWarnings(editDlg, &newEvent); editDlg->close(); } /****************************************************************************** * Returns a list of all alarm templates. * If shell commands are disabled, command alarm templates are omitted. */ KAEvent::List templateList() { KAEvent::List templates; bool includeCmdAlarms = ShellProcess::authorised(); KAEvent::List events = AlarmCalendar::resources()->events(CalEvent::TEMPLATE); for (int i = 0, end = events.count(); i < end; ++i) { KAEvent* event = events[i]; if (includeCmdAlarms || !(event->actionTypes() & KAEvent::ACT_COMMAND)) templates.append(event); } return templates; } /****************************************************************************** * To be called after an alarm has been edited. * Prompt the user to re-enable alarms if they are currently disabled, and if * it's an email alarm, warn if no 'From' email address is configured. */ void outputAlarmWarnings(QWidget* parent, const KAEvent* event) { if (event && event->actionTypes() == KAEvent::ACT_EMAIL && Preferences::emailAddress().isEmpty()) KAMessageBox::information(parent, xi18nc("@info Please set the 'From' email address...", "%1Please set it in the Configuration dialog.", KAMail::i18n_NeedFromEmailAddress())); if (!theApp()->alarmsEnabled()) { if (KAMessageBox::warningYesNo(parent, xi18nc("@info", "Alarms are currently disabled.Do you want to enable alarms now?"), QString(), KGuiItem(i18nc("@action:button", "Enable")), KGuiItem(i18nc("@action:button", "Keep Disabled")), QStringLiteral("EditEnableAlarms")) == KMessageBox::Yes) theApp()->setAlarmsEnabled(true); } } /****************************************************************************** * Reload the calendar. */ void refreshAlarms() { qCDebug(KALARM_LOG) << "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) << "refreshAlarmsIfQueued"; AlarmCalendar::resources()->reload(); // Close any message windows for alarms which are now disabled KAEvent::List events = AlarmCalendar::resources()->events(CalEvent::ACTIVE); for (int i = 0, end = events.count(); i < end; ++i) { KAEvent* event = events[i]; if (!event->enabled() && (event->actionTypes() & KAEvent::ACT_DISPLAY)) { MessageWin* win = MessageWin::findEvent(EventId(*event)); delete win; } } MainWindow::refresh(); refreshAlarmsQueued = false; } } /****************************************************************************** * Start KMail if it isn't already running, optionally minimised. * Reply = reason for failure to run KMail (which may be the empty string) * = null string if success. */ QString runKMail(bool minimise) { QDBusReply reply = QDBusConnection::sessionBus().interface()->isServiceRegistered(KMAIL_DBUS_SERVICE); if (!reply.isValid() || !reply.value()) { // Program is not already running, so start it QString errmsg; if (minimise && Private::startKMailMinimised()) return QString(); if (KToolInvocation::startServiceByDesktopName(QStringLiteral("org.kde.kmail2"), QString(), &errmsg)) { qCCritical(KALARM_LOG) << "Couldn't start KMail (" << errmsg << ")"; return xi18nc("@info", "Unable to start KMail(%1)", errmsg); } } return QString(); } /****************************************************************************** * Start KMail, minimised. * This code is taken from kstart in kdebase. */ bool Private::startKMailMinimised() { #if KDEPIM_HAVE_X11 NETRootInfo i(QX11Info::connection(), NET::Supported, NET::Properties2()); if (i.isSupported(NET::WM2KDETemporaryRules)) { qCDebug(KALARM_LOG) << "startKMailMinimised: using rules"; KXMessages msg; QString message = QLatin1String("wmclass=kmail\nwmclassmatch=1\n" // 1 = exact match "wmclasscomplete=false\n" "minimize=true\nminimizerule=3\n" "type=") + QString().setNum(NET::Normal) + QLatin1String("\ntyperule=2"); msg.broadcastMessage("_KDE_NET_WM_TEMPORARY_RULES", message, -1); - qApp->flush(); + QCoreApplication::processEvents(); } else { // Connect to window add to get the NEW windows qCDebug(KALARM_LOG) << "startKMailMinimised: connecting to window add"; connect(KWindowSystem::self(), &KWindowSystem::windowAdded, instance(), &Private::windowAdded); } // Propagate the app startup notification info to the started app. // We are not using KApplication, so the env remained set. KStartupInfoId id = KStartupInfo::currentStartupIdEnv(); KProcess* proc = new KProcess; (*proc) << QStringLiteral("kmail"); int pid = proc->startDetached(); if (!pid) { KStartupInfo::sendFinish(id); // failed to start return false; } KStartupInfoData data; data.addPid(pid); data.setName(QLatin1String("kmail")); data.setBin(QLatin1String("kmail")); KStartupInfo::sendChange(id, data); return true; #else return false; #endif } /****************************************************************************** * Called when a window is created, to minimise it. * This code is taken from kstart in kdebase. */ void Private::windowAdded(WId w) { #if KDEPIM_HAVE_X11 static const NET::WindowTypes SUPPORTED_TYPES = NET::NormalMask | NET::DesktopMask | NET::DockMask | NET::ToolbarMask | NET::MenuMask | NET::DialogMask | NET::OverrideMask | NET::TopMenuMask | NET::UtilityMask | NET::SplashMask; KWindowInfo kwinfo(w, NET::WMWindowType | NET::WMName); if (kwinfo.windowType(SUPPORTED_TYPES) == NET::TopMenu || kwinfo.windowType(SUPPORTED_TYPES) == NET::Toolbar || kwinfo.windowType(SUPPORTED_TYPES) == NET::Desktop) return; // always ignore these window types Display* display = QX11Info::display(); XWithdrawWindow(display, w, QX11Info::appScreen()); - QApplication::flush(); + QCoreApplication::processEvents(); NETWinInfo info(QX11Info::connection(), w, QX11Info::appRootWindow(), NET::WMState, NET::Properties2()); XWMHints* hints = XGetWMHints(display, w); if (hints) { hints->flags |= StateHint; hints->initial_state = IconicState; XSetWMHints(display, w, hints); XFree(hints); } info.setWindowType(NET::Normal); XSync(display, False); XMapWindow(display, w); XSync(display, False); - QApplication::flush(); + QCoreApplication::processEvents(); #endif } /****************************************************************************** * The "Don't show again" option for error messages is personal to the user on a * particular computer. For example, he may want to inhibit error messages only * on his laptop. So the status is not stored in the alarm calendar, but in the * user's local KAlarm data directory. ******************************************************************************/ /****************************************************************************** * Return the Don't-show-again error message tags set for a specified alarm ID. */ QStringList dontShowErrors(const EventId& eventId) { if (eventId.isEmpty()) return QStringList(); KConfig config(QStandardPaths::writableLocation(QStandardPaths::DataLocation) + QLatin1Char('/') + ALARM_OPTS_FILE); KConfigGroup group(&config, DONT_SHOW_ERRORS_GROUP); const QString id = QStringLiteral("%1:%2").arg(eventId.collectionId()).arg(eventId.eventId()); return group.readEntry(id, QStringList()); } /****************************************************************************** * Check whether the specified Don't-show-again error message tag is set for an * alarm ID. */ bool dontShowErrors(const EventId& eventId, const QString& tag) { if (tag.isEmpty()) return false; QStringList tags = dontShowErrors(eventId); return tags.indexOf(tag) >= 0; } /****************************************************************************** * Reset the Don't-show-again error message tags for an alarm ID. * If 'tags' is empty, the config entry is deleted. */ void setDontShowErrors(const EventId& eventId, const QStringList& tags) { if (eventId.isEmpty()) return; KConfig config(QStandardPaths::writableLocation(QStandardPaths::DataLocation) + QLatin1Char('/') + ALARM_OPTS_FILE); KConfigGroup group(&config, DONT_SHOW_ERRORS_GROUP); const QString id = QStringLiteral("%1:%2").arg(eventId.collectionId()).arg(eventId.eventId()); if (tags.isEmpty()) group.deleteEntry(id); else group.writeEntry(id, tags); group.sync(); } /****************************************************************************** * Set the specified Don't-show-again error message tag for an alarm ID. * Existing tags are unaffected. */ void setDontShowErrors(const EventId& eventId, const QString& tag) { if (eventId.isEmpty() || tag.isEmpty()) return; KConfig config(QStandardPaths::writableLocation(QStandardPaths::DataLocation) + QLatin1Char('/') + ALARM_OPTS_FILE); KConfigGroup group(&config, DONT_SHOW_ERRORS_GROUP); const QString id = QStringLiteral("%1:%2").arg(eventId.collectionId()).arg(eventId.eventId()); QStringList tags = group.readEntry(id, QStringList()); if (tags.indexOf(tag) < 0) { tags += tag; group.writeEntry(id, tags); group.sync(); } } /****************************************************************************** * Read the size for the specified window from the config file, for the * current screen resolution. * Reply = true if size set in the config file, in which case 'result' is set * = false if no size is set, in which case 'result' is unchanged. */ bool readConfigWindowSize(const char* window, QSize& result, int* splitterWidth) { KConfigGroup config(KSharedConfig::openConfig(), window); - QWidget* desktop = qApp->desktop(); - QSize s = QSize(config.readEntry(QStringLiteral("Width %1").arg(desktop->width()), (int)0), - config.readEntry(QStringLiteral("Height %1").arg(desktop->height()), (int)0)); + const QWidget* desktop = QApplication::desktop(); + const QSize s = QSize(config.readEntry(QStringLiteral("Width %1").arg(desktop->width()), (int)0), + config.readEntry(QStringLiteral("Height %1").arg(desktop->height()), (int)0)); if (s.isEmpty()) return false; result = s; if (splitterWidth) *splitterWidth = config.readEntry(QStringLiteral("Splitter %1").arg(desktop->width()), -1); return true; } /****************************************************************************** * Write the size for the specified window to the config file, for the * current screen resolution. */ void writeConfigWindowSize(const char* window, const QSize& size, int splitterWidth) { KConfigGroup config(KSharedConfig::openConfig(), window); - QWidget* desktop = qApp->desktop(); + const QWidget* desktop = QApplication::desktop(); config.writeEntry(QStringLiteral("Width %1").arg(desktop->width()), size.width()); config.writeEntry(QStringLiteral("Height %1").arg(desktop->height()), size.height()); if (splitterWidth >= 0) config.writeEntry(QStringLiteral("Splitter %1").arg(desktop->width()), splitterWidth); config.sync(); } /****************************************************************************** * Check from its mime type whether a file appears to be a text or image file. * If a text file, its type is distinguished. * Reply = file type. */ FileType fileType(const QMimeType& mimetype) { if (mimetype.inherits(QStringLiteral("text/html"))) return TextFormatted; if (mimetype.inherits(QStringLiteral("application/x-executable"))) return TextApplication; if (mimetype.inherits(QStringLiteral("text/plain"))) return TextPlain; if (mimetype.name().startsWith(QLatin1String("image/"))) return Image; return Unknown; } /****************************************************************************** * Check that a file exists and is a plain readable file. * Updates 'filename' and 'url' even if an error occurs, since 'filename' may * be needed subsequently by showFileErrMessage(). * 'filename' is in user input format and may be a local file path or URL. */ FileErr checkFileExists(QString& filename, QUrl& url) { // Convert any relative file path to absolute // (using home directory as the default). // This also supports absolute paths and absolute urls. FileErr err = FileErr_None; url = QUrl::fromUserInput(filename, QDir::homePath(), QUrl::AssumeLocalFile); if (filename.isEmpty()) { url = QUrl(); err = FileErr_Blank; // blank file name } else if (!url.isValid()) err = FileErr_Nonexistent; else if (url.isLocalFile()) { // It's a local file filename = url.toLocalFile(); QFileInfo info(filename); if (info.isDir()) err = FileErr_Directory; else if (!info.exists()) err = FileErr_Nonexistent; else if (!info.isReadable()) err = FileErr_Unreadable; } else { filename = url.toDisplayString(); auto statJob = KIO::stat(url, KIO::StatJob::SourceSide, 2); KJobWidgets::setWindow(statJob, MainWindow::mainMainWindow()); if (!statJob->exec()) err = FileErr_Nonexistent; else { KFileItem fi(statJob->statResult(), url); if (fi.isDir()) err = FileErr_Directory; else if (!fi.isReadable()) err = FileErr_Unreadable; } } return err; } /****************************************************************************** * Display an error message appropriate to 'err'. * Display a Continue/Cancel error message if 'errmsgParent' non-null. * Reply = true to continue, false to cancel. */ bool showFileErrMessage(const QString& filename, FileErr err, FileErr blankError, QWidget* errmsgParent) { if (err != FileErr_None) { // If file is a local file, remove "file://" from name QString file = filename; QRegExp f(QStringLiteral("^file:/+")); if (f.indexIn(file) >= 0) file = file.mid(f.matchedLength() - 1); QString errmsg; switch (err) { case FileErr_Blank: if (blankError == FileErr_BlankDisplay) errmsg = i18nc("@info", "Please select a file to display"); else if (blankError == FileErr_BlankPlay) errmsg = i18nc("@info", "Please select a file to play"); else qFatal("Program error"); KAMessageBox::sorry(errmsgParent, errmsg); return false; case FileErr_Directory: KAMessageBox::sorry(errmsgParent, xi18nc("@info", "%1 is a folder", file)); return false; case FileErr_Nonexistent: errmsg = xi18nc("@info", "%1 not found", file); break; case FileErr_Unreadable: errmsg = xi18nc("@info", "%1 is not readable", file); break; case FileErr_NotTextImage: errmsg = xi18nc("@info", "%1 appears not to be a text or image file", file); break; default: break; } if (KAMessageBox::warningContinueCancel(errmsgParent, errmsg) == KMessageBox::Cancel) return false; } return true; } /****************************************************************************** * If a url string is a local file, strip off the 'file:/' prefix. */ QString pathOrUrl(const QString& url) { static const QRegExp localfile(QStringLiteral("^file:/+")); return (localfile.indexIn(url) >= 0) ? url.mid(localfile.matchedLength() - 1) : url; } /****************************************************************************** * Display a modal dialog to choose an existing file, initially highlighting * any specified file. * @param initialFile The file to initially highlight - must be a full path name or URL. * @param defaultDir The directory to start in if @p initialFile is empty. If empty, * the user's home directory will be used. Updated to the * directory containing the selected file, if a file is chosen. * @param existing true to return only existing files, false to allow new ones. * Reply = URL selected. * = empty, non-null string if no file was selected. * = null string if dialogue was deleted while visible (indicating that * the parent widget was probably also deleted). */ QString browseFile(const QString& caption, QString& defaultDir, const QString& initialFile, const QString& filter, bool existing, QWidget* parent) { QString initialDir = !initialFile.isEmpty() ? QString(initialFile).remove(QRegExp(QLatin1String("/[^/]*$"))) : !defaultDir.isEmpty() ? defaultDir : QDir::homePath(); // Use AutoQPointer to guard against crash on application exit while // the dialogue is still open. It prevents double deletion (both on // deletion of parent, and on return from this function). AutoQPointer fileDlg = new QFileDialog(parent, caption, initialDir, filter); fileDlg->setAcceptMode(existing ? QFileDialog::AcceptOpen : QFileDialog::AcceptSave); fileDlg->setFileMode(existing ? QFileDialog::ExistingFile : QFileDialog::AnyFile); if (!initialFile.isEmpty()) fileDlg->selectFile(initialFile); if (fileDlg->exec() != QDialog::Accepted) return fileDlg ? QStringLiteral("") : QString(); // return null only if dialog was deleted const QList urls = fileDlg->selectedUrls(); if (urls.isEmpty()) return QStringLiteral(""); // return empty, non-null string const QUrl& url = urls[0]; defaultDir = url.isLocalFile() ? KIO::upUrl(url).toLocalFile() : url.adjusted(QUrl::RemoveFilename).path(); bool localOnly = true; return localOnly ? url.toDisplayString(QUrl::PreferLocalFile) : url.toDisplayString(); } /****************************************************************************** * Return a prompt string to ask the user whether to convert the calendar to the * current format. * If 'whole' is true, the whole calendar needs to be converted; else only some * alarms may need to be converted. * * Note: This method is defined here to avoid duplicating the i18n string * definition between the Akonadi and KResources code. */ QString conversionPrompt(const QString& calendarName, const QString& calendarVersion, bool whole) { QString msg = whole ? xi18nc("@info", "Calendar %1 is in an old format (KAlarm version %2), " "and will be read-only unless you choose to update it to the current format.", calendarName, calendarVersion) : xi18nc("@info", "Some or all of the alarms in calendar %1 are in an old KAlarm format, " "and will be read-only unless you choose to update them to the current format.", calendarName); return xi18nc("@info", "%1" "Do not update the calendar if it is also used with an older version of KAlarm " "(e.g. on another computer). If you do so, the calendar may become unusable there." "Do you wish to update the calendar?", msg); } #ifndef NDEBUG /****************************************************************************** * Set up KAlarm test conditions based on environment variables. * KALARM_TIME: specifies current system time (format [[[yyyy-]mm-]dd-]hh:mm [TZ]). */ void setTestModeConditions() { const QByteArray newTime = qgetenv("KALARM_TIME"); if (!newTime.isEmpty()) { KADateTime dt; if (AlarmTime::convertTimeString(newTime, dt, KADateTime::realCurrentLocalDateTime(), true)) setSimulatedSystemTime(dt); } } /****************************************************************************** * Set the simulated system time. */ void setSimulatedSystemTime(const KADateTime& dt) { KADateTime::setSimulatedSystemTime(dt); qCDebug(KALARM_LOG) << "New time =" << qPrintable(KADateTime::currentLocalDateTime().toString(QStringLiteral("%Y-%m-%d %H:%M %:Z"))); } #endif } // namespace KAlarm namespace { /****************************************************************************** * Display an error message about an error when saving an event. */ void displayUpdateError(QWidget* parent, KAlarm::UpdateError code, const UpdateStatusData& status, bool showKOrgError) { QString errmsg; if (status.status.status > KAlarm::UPDATE_KORG_ERR) { switch (code) { case KAlarm::ERR_ADD: case KAlarm::ERR_MODIFY: errmsg = (status.warnErr > 1) ? i18nc("@info", "Error saving alarms") : i18nc("@info", "Error saving alarm"); break; case KAlarm::ERR_DELETE: errmsg = (status.warnErr > 1) ? i18nc("@info", "Error deleting alarms") : i18nc("@info", "Error deleting alarm"); break; case KAlarm::ERR_REACTIVATE: errmsg = (status.warnErr > 1) ? i18nc("@info", "Error saving reactivated alarms") : i18nc("@info", "Error saving reactivated alarm"); break; case KAlarm::ERR_TEMPLATE: errmsg = (status.warnErr > 1) ? i18nc("@info", "Error saving alarm templates") : i18nc("@info", "Error saving alarm template"); break; } KAMessageBox::error(parent, errmsg); } else if (showKOrgError) displayKOrgUpdateError(parent, code, status.status, status.warnKOrg); } /****************************************************************************** * Tell KOrganizer to put an alarm in its calendar. * It will be held by KOrganizer as a simple event, without alarms - KAlarm * is still responsible for alarming. */ KAlarm::UpdateResult sendToKOrganizer(const KAEvent& event) { Event::Ptr kcalEvent(new KCalCore::Event); event.updateKCalEvent(kcalEvent, KAEvent::UID_IGNORE); // Change the event ID to avoid duplicating the same unique ID as the original event QString uid = uidKOrganizer(event.id()); kcalEvent->setUid(uid); kcalEvent->clearAlarms(); QString userEmail; switch (event.actionTypes()) { case KAEvent::ACT_DISPLAY: case KAEvent::ACT_COMMAND: case KAEvent::ACT_DISPLAY_COMMAND: kcalEvent->setSummary(event.cleanText()); userEmail = Preferences::emailAddress(); break; case KAEvent::ACT_EMAIL: { QString from = event.emailFromId() ? Identities::identityManager()->identityForUoid(event.emailFromId()).fullEmailAddr() : Preferences::emailAddress(); AlarmText atext; atext.setEmail(event.emailAddresses(QStringLiteral(", ")), from, QString(), QString(), event.emailSubject(), QString()); kcalEvent->setSummary(atext.displayText()); userEmail = from; break; } case KAEvent::ACT_AUDIO: kcalEvent->setSummary(event.audioFile()); break; default: break; } Person person(QString(), userEmail); kcalEvent->setOrganizer(person); kcalEvent->setDuration(Duration(Preferences::kOrgEventDuration() * 60, Duration::Seconds)); // Translate the event into string format ICalFormat format; format.setTimeZone(Preferences::timeSpecAsZone()); QString iCal = format.toICalString(kcalEvent); // Send the event to KOrganizer KAlarm::UpdateResult status = runKOrganizer(); // start KOrganizer if it isn't already running, and create its D-Bus interface if (status != KAlarm::UPDATE_OK) return status; QList args; args << iCal; QDBusReply reply = korgInterface->callWithArgumentList(QDBus::Block, QStringLiteral("addIncidence"), args); if (!reply.isValid()) { if (reply.error().type() == QDBusError::UnknownObject) { status = KAlarm::UPDATE_KORG_ERRSTART; qCCritical(KALARM_LOG) << "sendToKOrganizer: addIncidence() D-Bus error: still starting"; } else { status.set(KAlarm::UPDATE_KORG_ERR, reply.error().message()); qCCritical(KALARM_LOG) << "sendToKOrganizer: addIncidence(" << uid << ") D-Bus call failed:" << status.message; } } else if (!reply.value()) { status = KAlarm::UPDATE_KORG_FUNCERR; qCDebug(KALARM_LOG) << "sendToKOrganizer: addIncidence(" << uid << ") D-Bus call returned false"; } else qCDebug(KALARM_LOG) << "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 CollectionSearch(KORG_MIME_TYPE, newID, true); // this auto-deletes when complete // Ignore errors return KAlarm::UpdateResult(KAlarm::UPDATE_OK); } /****************************************************************************** * Start KOrganizer if not already running, and create its D-Bus interface. */ KAlarm::UpdateResult runKOrganizer() { KAlarm::UpdateResult status; QString error, dbusService; int result = KDBusServiceStarter::self()->findServiceFor(QStringLiteral("DBUS/Organizer"), QString(), &error, &dbusService); if (result) { status.set(KAlarm::UPDATE_KORG_ERRINIT, error); qCWarning(KALARM_LOG) << "runKOrganizer: Unable to start DBUS/Organizer:" << status.message; return status; } // If Kontact is running, there is a load() method which needs to be called to // load KOrganizer into Kontact. But if KOrganizer is running independently, // the load() method doesn't exist. QDBusInterface iface(KORG_DBUS_SERVICE, QStringLiteral(KORG_DBUS_LOAD_PATH), QStringLiteral("org.kde.PIMUniqueApplication")); if (!iface.isValid()) { status.set(KAlarm::UPDATE_KORG_ERR, iface.lastError().message()); qCWarning(KALARM_LOG) << "runKOrganizer: Unable to access " KORG_DBUS_LOAD_PATH " D-Bus interface:" << status.message; return status; } QDBusReply reply = iface.call(QStringLiteral("load")); if ((!reply.isValid() || !reply.value()) && iface.lastError().type() != QDBusError::UnknownMethod) { status.set(KAlarm::UPDATE_KORG_ERR, iface.lastError().message()); qCWarning(KALARM_LOG) << "Loading KOrganizer failed:" << status.message; return status; } // KOrganizer has been started, but it may not have the necessary // D-Bus interface available yet. if (!korgInterface || !korgInterface->isValid()) { delete korgInterface; korgInterface = new QDBusInterface(KORG_DBUS_SERVICE, QStringLiteral(KORG_DBUS_PATH), KORG_DBUS_IFACE); if (!korgInterface->isValid()) { status.set(KAlarm::UPDATE_KORG_ERRSTART, korgInterface->lastError().message()); qCWarning(KALARM_LOG) << "runKOrganizer: Unable to access " KORG_DBUS_PATH " D-Bus interface:" << status.message; delete korgInterface; korgInterface = nullptr; } } return status; } /****************************************************************************** * Insert a KOrganizer string after the hyphen in the supplied event ID. */ QString uidKOrganizer(const QString& id) { QString result = id; int i = result.lastIndexOf(QLatin1Char('-')); if (i < 0) i = result.length(); return result.insert(i, KORGANIZER_UID); } } // namespace /****************************************************************************** * Case insensitive comparison for use by qSort(). */ bool caseInsensitiveLessThan(const QString& s1, const QString& s2) { return s1.toLower() < s2.toLower(); } // vim: et sw=4: diff --git a/src/lib/desktop.cpp b/src/lib/desktop.cpp index e921ff35..dd3637ca 100644 --- a/src/lib/desktop.cpp +++ b/src/lib/desktop.cpp @@ -1,46 +1,50 @@ /* * desktop.cpp - desktop functions * Program: kalarm - * Copyright © 2008,2009 by David Jarvie + * Copyright © 2008,2009,2019 David Jarvie * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "desktop.h" #include "config-kalarm.h" + #if KDEPIM_HAVE_X11 #include #endif -#include -#include +#include +#include namespace KAlarm { /****************************************************************************** * Return the size of the usable area of the desktop, optionally for a specific * screen in a multi-head setup. */ QRect desktopWorkArea(int screen) { #if KDEPIM_HAVE_X11 if (screen < 0) return KWindowSystem::workArea(); #endif - return qApp->desktop()->availableGeometry(screen); + const QList screens = QGuiApplication::screens(); + if (screen < 0 || screen >= screens.count()) + return QRect(); + return screens[screen]->availableGeometry(); } } // namespace KAlarm // vim: et sw=4: diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 93d95143..35fd2ee4 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -1,1543 +1,1547 @@ /* * mainwindow.cpp - main application window * Program: kalarm * Copyright © 2001-2019 David Jarvie * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kalarm.h" #include "mainwindow.h" #include "alarmcalendar.h" #include "alarmlistdelegate.h" #include "autoqpointer.h" #include "alarmlistview.h" #include "birthdaydlg.h" #include "functions.h" #include "kalarmapp.h" #include "kamail.h" #include "messagebox.h" #include "newalarmaction.h" #include "prefdlg.h" #include "preferences.h" #include "resourceselector.h" #include "synchtimer.h" #include "templatedlg.h" #include "templatemenuaction.h" #include "templatepickdlg.h" #include "traywindow.h" #include "wakedlg.h" #include "kalarm_debug.h" #include #include #include #include #include #include #include using namespace KCalCore; 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 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), mResourcesWidth(-1), mHiddenTrayParent(false), mShown(false), mResizing(false) { 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(qApp->desktop()->width()), (int)0); + mResourcesWidth = wconfig.readEntry(QStringLiteral("Splitter %1").arg(QApplication::desktop()->width()), (int)0); } setAcceptDrops(true); // allow drag-and-drop onto this window mSplitter = new QSplitter(Qt::Horizontal, this); mSplitter->setChildrenCollapsible(false); mSplitter->installEventFilter(this); setCentralWidget(mSplitter); // Create the calendar resource selector widget Akonadi::ControlGui::widgetNeedsAkonadi(this); mResourceSelector = new ResourceSelector(mSplitter); mSplitter->setStretchFactor(0, 0); // don't resize resource selector when window is resized mSplitter->setStretchFactor(1, 1); // Create the alarm list widget mListFilterModel = new AlarmListModel(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(AkonadiModel::instance(), &AkonadiModel::collectionStatusChanged, this, &MainWindow::slotCalendarStatusChanged); connect(mResourceSelector, &ResourceSelector::resized, this, &MainWindow::resourcesResized); mListView->installEventFilter(this); initActions(); setAutoSaveSettings(QLatin1String(WINDOW_NAME), true); // save toolbars, window sizes etc. mWindowList.append(this); if (mWindowList.count() == 1) { // It's the first main window if (theApp()->wantShowInSystemTray()) theApp()->displayTrayIcon(true, this); // create system tray icon for run-in-system-tray mode else if (theApp()->trayWindow()) theApp()->trayWindow()->setAssocMainWindow(this); // associate this window with the system tray icon } slotCalendarStatusChanged(); // initialise action states now that window is registered } MainWindow::~MainWindow() { qCDebug(KALARM_LOG) << "~MainWindow"; bool trayParent = isTrayParent(); // must call before removing from window list mWindowList.removeAt(mWindowList.indexOf(this)); // Prevent view updates during window destruction delete mResourceSelector; mResourceSelector = nullptr; delete mListView; mListView = nullptr; if (theApp()->trayWindow()) { if (trayParent) delete theApp()->trayWindow(); else theApp()->trayWindow()->removeWindow(this); } KSharedConfig::openConfig()->sync(); // save any new window size to disc theApp()->quitIf(); } /****************************************************************************** * Called when the QApplication::saveStateRequest() signal has been emitted. * Save settings to the session managed config file, for restoration * when the program is restored. */ void MainWindow::saveProperties(KConfigGroup& config) { config.writeEntry("HiddenTrayParent", isTrayParent() && isHidden()); config.writeEntry("ShowArchived", mShowArchived); config.writeEntry("ShowColumns", mListView->columnsVisible()); config.writeEntry("ResourcesWidth", mResourceSelector->isHidden() ? 0 : mResourceSelector->width()); } /****************************************************************************** * Read settings from the session managed config file. * This function is automatically called whenever the app is being * restored. Read in whatever was saved in saveProperties(). */ void MainWindow::readProperties(const KConfigGroup& config) { mHiddenTrayParent = config.readEntry("HiddenTrayParent", true); mShowArchived = config.readEntry("ShowArchived", false); mResourcesWidth = config.readEntry("ResourcesWidth", (int)0); mShowResources = (mResourcesWidth > 0); mListView->setColumnsVisible(config.readEntry("ShowColumns", QList())); } /****************************************************************************** * Get the main main window, i.e. the parent of the system tray icon, or if * none, the first main window to be created. Visible windows take precedence * over hidden ones. */ MainWindow* MainWindow::mainMainWindow() { MainWindow* tray = theApp()->trayWindow() ? theApp()->trayWindow()->assocMainWindow() : nullptr; if (tray && tray->isVisible()) return tray; for (int i = 0, end = mWindowList.count(); i < end; ++i) if (mWindowList[i]->isVisible()) return mWindowList[i]; if (tray) return tray; if (mWindowList.isEmpty()) return nullptr; return mWindowList[0]; } /****************************************************************************** * Check whether this main window is effectively the parent of the system tray icon. */ bool MainWindow::isTrayParent() const { TrayWindow* tray = theApp()->trayWindow(); if (!tray || !QSystemTrayIcon::isSystemTrayAvailable()) return false; if (tray->assocMainWindow() == this) return true; return mWindowList.count() == 1; } /****************************************************************************** * Close all main windows. */ void MainWindow::closeAll() { while (!mWindowList.isEmpty()) delete mWindowList[0]; // N.B. the destructor removes the window from the list } /****************************************************************************** * Intercept events for the splitter widget. */ bool MainWindow::eventFilter(QObject* obj, QEvent* e) { if (obj == mSplitter) { switch (e->type()) { case QEvent::Resize: // Don't change resources size while WINDOW is being resized. // Resize event always occurs before Paint. mResizing = true; break; case QEvent::Paint: // Allow resources to be resized again mResizing = false; break; default: break; } } else if (obj == mListView) { switch (e->type()) { case QEvent::KeyPress: { QKeyEvent* ke = static_cast(e); if (ke->key() == Qt::Key_Delete && ke->modifiers() == Qt::ShiftModifier) { // Prevent Shift-Delete being processed by EventListDelegate mActionDeleteForce->trigger(); return true; } break; } default: break; } } return false; } /****************************************************************************** * Called when the window's size has changed (before it is painted). * Sets the last column in the list view to extend at least to the right hand * edge of the list view. * Records the new size in the config file. */ void MainWindow::resizeEvent(QResizeEvent* re) { // Save the window's new size only if it's the first main window MainWindowBase::resizeEvent(re); if (mResourcesWidth > 0) { QList widths; widths.append(mResourcesWidth); widths.append(width() - mResourcesWidth - mSplitter->handleWidth()); mSplitter->setSizes(widths); } } +/****************************************************************************** +* Called when the resources panel has been resized. +* Records the new size in the config file. +*/ void MainWindow::resourcesResized() { if (!mShown || mResizing) return; QList widths = mSplitter->sizes(); if (widths.count() > 1) { mResourcesWidth = widths[0]; // Width is reported as non-zero when resource selector is // actually invisible, so note a zero width in these circumstances. if (mResourcesWidth <= 5) mResourcesWidth = 0; else if (mainMainWindow() == this) { KConfigGroup config(KSharedConfig::openConfig(), WINDOW_NAME); - config.writeEntry(QStringLiteral("Splitter %1").arg(qApp->desktop()->width()), mResourcesWidth); + config.writeEntry(QStringLiteral("Splitter %1").arg(QApplication::desktop()->width()), mResourcesWidth); config.sync(); } } } /****************************************************************************** * Called when the window is first displayed. * Sets the last column in the list view to extend at least to the right hand * edge of the list view. */ void MainWindow::showEvent(QShowEvent* se) { if (mResourcesWidth > 0) { QList widths; widths.append(mResourcesWidth); widths.append(width() - mResourcesWidth - mSplitter->handleWidth()); mSplitter->setSizes(widths); } MainWindowBase::showEvent(se); mShown = true; } /****************************************************************************** * Display the window. */ void MainWindow::show() { MainWindowBase::show(); if (mMenuError) { // Show error message now that the main window has been displayed. // Waiting until now lets the user easily associate the message with // the main window which is faulty. KAMessageBox::error(this, xi18nc("@info", "Failure to create menus (perhaps %1 missing or corrupted)", UI_FILE)); mMenuError = false; } } /****************************************************************************** * Called after the window is hidden. */ void MainWindow::hideEvent(QHideEvent* he) { MainWindowBase::hideEvent(he); } /****************************************************************************** * Initialise the menu, toolbar and main window actions. */ void MainWindow::initActions() { KActionCollection* actions = actionCollection(); mActionTemplates = new QAction(i18nc("@action", "&Templates..."), this); actions->addAction(QStringLiteral("templates"), mActionTemplates); connect(mActionTemplates, &QAction::triggered, this, &MainWindow::slotTemplates); mActionNew = new NewAlarmAction(false, i18nc("@action", "&New"), this, actions); actions->addAction(QStringLiteral("new"), mActionNew); QAction* action = mActionNew->displayAlarmAction(QStringLiteral("newDisplay")); connect(action, &QAction::triggered, this, &MainWindow::slotNewDisplay); action = mActionNew->commandAlarmAction(QStringLiteral("newCommand")); connect(action, &QAction::triggered, this, &MainWindow::slotNewCommand); action = mActionNew->emailAlarmAction(QStringLiteral("newEmail")); connect(action, &QAction::triggered, this, &MainWindow::slotNewEmail); action = mActionNew->audioAlarmAction(QStringLiteral("newAudio")); connect(action, &QAction::triggered, this, &MainWindow::slotNewAudio); TemplateMenuAction* templateMenuAction = mActionNew->fromTemplateAlarmAction(QStringLiteral("newFromTemplate")); connect(templateMenuAction, &TemplateMenuAction::selected, this, &MainWindow::slotNewFromTemplate); mActionCreateTemplate = new QAction(i18nc("@action", "Create Tem&plate..."), this); actions->addAction(QStringLiteral("createTemplate"), mActionCreateTemplate); connect(mActionCreateTemplate, &QAction::triggered, this, &MainWindow::slotNewTemplate); mActionCopy = new QAction(QIcon::fromTheme(QStringLiteral("edit-copy")), i18nc("@action", "&Copy..."), this); actions->addAction(QStringLiteral("copy"), mActionCopy); actions->setDefaultShortcut(mActionCopy, QKeySequence(Qt::SHIFT + Qt::Key_Insert)); connect(mActionCopy, &QAction::triggered, this, &MainWindow::slotCopy); mActionModify = new QAction(QIcon::fromTheme(QStringLiteral("document-properties")), i18nc("@action", "&Edit..."), this); actions->addAction(QStringLiteral("modify"), mActionModify); actions->setDefaultShortcut(mActionModify, QKeySequence(Qt::CTRL + Qt::Key_E)); connect(mActionModify, &QAction::triggered, this, &MainWindow::slotModify); mActionDelete = new QAction(QIcon::fromTheme(QStringLiteral("edit-delete")), i18nc("@action", "&Delete"), this); actions->addAction(QStringLiteral("delete"), mActionDelete); actions->setDefaultShortcut(mActionDelete, QKeySequence::Delete); connect(mActionDelete, &QAction::triggered, this, &MainWindow::slotDeleteIf); // Set up Shift-Delete as a shortcut to delete without confirmation mActionDeleteForce = new QAction(i18nc("@action", "Delete Without Confirmation"), this); actions->addAction(QStringLiteral("delete-force"), mActionDeleteForce); actions->setDefaultShortcut(mActionDeleteForce, QKeySequence::Delete + Qt::SHIFT); connect(mActionDeleteForce, &QAction::triggered, this, &MainWindow::slotDeleteForce); mActionReactivate = new QAction(i18nc("@action", "Reac&tivate"), this); actions->addAction(QStringLiteral("undelete"), mActionReactivate); actions->setDefaultShortcut(mActionReactivate, QKeySequence(Qt::CTRL + Qt::Key_R)); connect(mActionReactivate, &QAction::triggered, this, &MainWindow::slotReactivate); mActionEnable = new QAction(this); actions->addAction(QStringLiteral("disable"), mActionEnable); actions->setDefaultShortcut(mActionEnable, QKeySequence(Qt::CTRL + Qt::Key_B)); connect(mActionEnable, &QAction::triggered, this, &MainWindow::slotEnable); action = new QAction(i18nc("@action", "Wake From Suspend..."), this); actions->addAction(QStringLiteral("wakeSuspend"), action); connect(action, &QAction::triggered, this, &MainWindow::slotWakeFromSuspend); action = KAlarm::createStopPlayAction(this); actions->addAction(QStringLiteral("stopAudio"), action); KGlobalAccel::setGlobalShortcut(action, QList()); // allow user to set a global shortcut mActionShowArchived = new KToggleAction(i18nc("@action", "Show Archi&ved Alarms"), this); actions->addAction(QStringLiteral("showArchivedAlarms"), mActionShowArchived); actions->setDefaultShortcut(mActionShowArchived, QKeySequence(Qt::CTRL + Qt::Key_P)); connect(mActionShowArchived, &KToggleAction::triggered, this, &MainWindow::slotShowArchived); mActionToggleTrayIcon = new KToggleAction(i18nc("@action", "Show in System &Tray"), this); actions->addAction(QStringLiteral("showInSystemTray"), mActionToggleTrayIcon); connect(mActionToggleTrayIcon, &KToggleAction::triggered, this, &MainWindow::slotToggleTrayIcon); mActionToggleResourceSel = new KToggleAction(QIcon::fromTheme(QStringLiteral("view-choose")), i18nc("@action", "Show &Calendars"), this); actions->addAction(QStringLiteral("showResources"), mActionToggleResourceSel); connect(mActionToggleResourceSel, &KToggleAction::triggered, this, &MainWindow::slotToggleResourceSelector); mActionSpreadWindows = KAlarm::createSpreadWindowsAction(this); actions->addAction(QStringLiteral("spread"), mActionSpreadWindows); KGlobalAccel::setGlobalShortcut(mActionSpreadWindows, QList()); // allow user to set a global shortcut mActionImportAlarms = new QAction(i18nc("@action", "Import &Alarms..."), this); actions->addAction(QStringLiteral("importAlarms"), mActionImportAlarms); connect(mActionImportAlarms, &QAction::triggered, this, &MainWindow::slotImportAlarms); mActionImportBirthdays = new QAction(i18nc("@action", "Import &Birthdays..."), this); actions->addAction(QStringLiteral("importBirthdays"), mActionImportBirthdays); connect(mActionImportBirthdays, &QAction::triggered, this, &MainWindow::slotBirthdays); mActionExportAlarms = new QAction(i18nc("@action", "E&xport Selected Alarms..."), this); actions->addAction(QStringLiteral("exportAlarms"), mActionExportAlarms); connect(mActionExportAlarms, &QAction::triggered, this, &MainWindow::slotExportAlarms); mActionExport = new QAction(i18nc("@action", "E&xport..."), this); actions->addAction(QStringLiteral("export"), mActionExport); connect(mActionExport, &QAction::triggered, this, &MainWindow::slotExportAlarms); action = new QAction(QIcon::fromTheme(QStringLiteral("view-refresh")), i18nc("@action", "&Refresh Alarms"), this); actions->addAction(QStringLiteral("refreshAlarms"), action); connect(action, &QAction::triggered, this, &MainWindow::slotRefreshAlarms); KToggleAction* toggleAction = KAlarm::createAlarmEnableAction(this); actions->addAction(QStringLiteral("alarmsEnable"), toggleAction); if (undoText.isNull()) { // Get standard texts, etc., for Undo and Redo actions QAction* act = KStandardAction::undo(this, nullptr, actions); undoShortcut = act->shortcuts(); undoText = act->text(); undoTextStripped = KLocalizedString::removeAcceleratorMarker(undoText); delete act; act = KStandardAction::redo(this, nullptr, actions); redoShortcut = act->shortcuts(); redoText = act->text(); redoTextStripped = KLocalizedString::removeAcceleratorMarker(redoText); delete act; } mActionUndo = new KToolBarPopupAction(QIcon::fromTheme(QStringLiteral("edit-undo")), undoText, this); actions->addAction(QStringLiteral("edit_undo"), mActionUndo); actions->setDefaultShortcuts(mActionUndo, undoShortcut); connect(mActionUndo, &KToolBarPopupAction::triggered, this, &MainWindow::slotUndo); mActionRedo = new KToolBarPopupAction(QIcon::fromTheme(QStringLiteral("edit-redo")), redoText, this); actions->addAction(QStringLiteral("edit_redo"), mActionRedo); actions->setDefaultShortcuts(mActionRedo, redoShortcut); connect(mActionRedo, &KToolBarPopupAction::triggered, this, &MainWindow::slotRedo); KStandardAction::find(mListView, SLOT(slotFind()), actions); mActionFindNext = KStandardAction::findNext(mListView, SLOT(slotFindNext()), actions); mActionFindPrev = KStandardAction::findPrev(mListView, SLOT(slotFindPrev()), actions); KStandardAction::selectAll(mListView, SLOT(selectAll()), actions); KStandardAction::deselect(mListView, SLOT(clearSelection()), actions); // Quit only once the event loop is called; otherwise, the parent window will // be deleted while still processing the action, resulting in a crash. QAction* act = KStandardAction::quit(nullptr, nullptr, actions); connect(act, &QAction::triggered, this, &MainWindow::slotQuit, Qt::QueuedConnection); KStandardAction::keyBindings(this, SLOT(slotConfigureKeys()), actions); KStandardAction::configureToolbars(this, SLOT(slotConfigureToolbar()), actions); KStandardAction::preferences(this, SLOT(slotPreferences()), actions); mResourceSelector->initActions(actions); setStandardToolBarMenuEnabled(true); createGUI(UI_FILE); // Load menu and toolbar settings applyMainWindowSettings(KSharedConfig::openConfig()->group(WINDOW_NAME)); mContextMenu = static_cast(factory()->container(QStringLiteral("listContext"), this)); mActionsMenu = static_cast(factory()->container(QStringLiteral("actions"), this)); QMenu* resourceMenu = static_cast(factory()->container(QStringLiteral("resourceContext"), this)); mResourceSelector->setContextMenu(resourceMenu); mMenuError = (!mContextMenu || !mActionsMenu || !resourceMenu); connect(mActionUndo->menu(), &QMenu::aboutToShow, this, &MainWindow::slotInitUndoMenu); connect(mActionUndo->menu(), &QMenu::triggered, this, &MainWindow::slotUndoItem); connect(mActionRedo->menu(), &QMenu::aboutToShow, this, &MainWindow::slotInitRedoMenu); connect(mActionRedo->menu(), &QMenu::triggered, this, &MainWindow::slotRedoItem); connect(Undo::instance(), &Undo::changed, this, &MainWindow::slotUndoStatus); connect(mListView, &AlarmListView::findActive, this, &MainWindow::slotFindActive); Preferences::connect(SIGNAL(archivedKeepDaysChanged(int)), this, SLOT(updateKeepArchived(int))); Preferences::connect(SIGNAL(showInSystemTrayChanged(bool)), this, SLOT(updateTrayIconAction())); connect(theApp(), &KAlarmApp::trayIconToggled, this, &MainWindow::updateTrayIconAction); // Set menu item states setEnableText(true); mActionShowArchived->setChecked(mShowArchived); if (!Preferences::archivedKeepDays()) mActionShowArchived->setEnabled(false); mActionToggleResourceSel->setChecked(mShowResources); slotToggleResourceSelector(); updateTrayIconAction(); // set the correct text for this action mActionUndo->setEnabled(Undo::haveUndo()); mActionRedo->setEnabled(Undo::haveRedo()); mActionFindNext->setEnabled(false); mActionFindPrev->setEnabled(false); mActionCopy->setEnabled(false); mActionModify->setEnabled(false); mActionDelete->setEnabled(false); mActionReactivate->setEnabled(false); mActionEnable->setEnabled(false); mActionCreateTemplate->setEnabled(false); mActionExport->setEnabled(false); Undo::emitChanged(); // set the Undo/Redo menu texts // Daemon::monitoringAlarms(); } /****************************************************************************** * Enable or disable the Templates menu item in every main window instance. */ void MainWindow::enableTemplateMenuItem(bool enable) { for (int i = 0, end = mWindowList.count(); i < end; ++i) mWindowList[i]->mActionTemplates->setEnabled(enable); } /****************************************************************************** * Refresh the alarm list in every main window instance. */ void MainWindow::refresh() { qCDebug(KALARM_LOG) << "MainWindow::refresh"; AkonadiModel::instance()->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(Akonadi::Item::Id eventId) { mListView->clearSelection(); 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; AlarmCalendar* resources = AlarmCalendar::resources(); for (int i = 0; i < events.count(); ) { Akonadi::Collection c = resources->collectionForEvent(events[i].itemId()); if (!c.isValid()) events.remove(i); else undos.append(events[i++], c); } 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 AlarmCalendar* resources = AlarmCalendar::resources(); for (int i = 0, end = events.count(); i < end; ++i) { if (!ineligibleIDs.contains(EventId(events[i]))) { undos.append(events[i], resources->collectionForEvent(events[i].itemId())); } } Undo::saveReactivates(undos); } /****************************************************************************** * Called when the Enable/Disable button is clicked to enable or disable the * currently highlighted alarms in the list. */ void MainWindow::slotEnable() { bool enable = mActionEnableEnable; // save since changed in response to KAlarm::enableEvent() QVector events = mListView->selectedEvents(); QVector eventCopies; for (int i = 0, end = events.count(); i < end; ++i) eventCopies += events[i]; KAlarm::enableEvents(eventCopies, enable, this); slotSelection(); // update Enable/Disable action text } /****************************************************************************** * Called when the columns visible in the alarm list view have changed. */ void MainWindow::slotAlarmListColumnsChanged() { KConfigGroup config(KSharedConfig::openConfig(), VIEW_GROUP); config.writeEntry(SHOW_COLUMNS, mListView->columnsVisible()); config.sync(); } /****************************************************************************** * Called when the Show Archived Alarms menu item is selected or deselected. */ void MainWindow::slotShowArchived() { mShowArchived = !mShowArchived; mActionShowArchived->setChecked(mShowArchived); mActionShowArchived->setToolTip(mShowArchived ? i18nc("@info:tooltip", "Hide Archived Alarms") : i18nc("@info:tooltip", "Show Archived Alarms")); mListFilterModel->setEventTypeFilter(mShowArchived ? CalEvent::ACTIVE | CalEvent::ARCHIVED : CalEvent::ACTIVE); mListView->reset(); KConfigGroup config(KSharedConfig::openConfig(), VIEW_GROUP); config.writeEntry(SHOW_ARCHIVED_KEY, mShowArchived); config.sync(); } /****************************************************************************** * Called when the Spread Windows global shortcut is selected, to spread alarm * windows so that they are all visible. */ void MainWindow::slotSpreadWindowsShortcut() { mActionSpreadWindows->trigger(); } /****************************************************************************** * Called when the Wake From Suspend menu option is selected. */ void MainWindow::slotWakeFromSuspend() { (WakeFromSuspendDlg::create(this))->show(); } /****************************************************************************** * Called when the Import Alarms menu item is selected, to merge alarms from an * external calendar into the current calendars. */ void MainWindow::slotImportAlarms() { AlarmCalendar::importAlarms(this); } /****************************************************************************** * Called when the Export Alarms menu item is selected, to export the selected * alarms to an external calendar. */ void MainWindow::slotExportAlarms() { QVector events = mListView->selectedEvents(); if (!events.isEmpty()) { KAEvent::List evts = KAEvent::ptrList(events); AlarmCalendar::exportAlarms(evts, this); } } /****************************************************************************** * Called when the Import Birthdays menu item is selected, to display birthdays * from the address book for selection as alarms. */ void MainWindow::slotBirthdays() { // Use AutoQPointer to guard against crash on application exit while // the dialogue is still open. It prevents double deletion (both on // deletion of MainWindow, and on return from this function). AutoQPointer dlg = new BirthdayDlg(this); if (dlg->exec() == QDialog::Accepted) { QVector events = dlg->events(); if (!events.isEmpty()) { mListView->clearSelection(); // Add alarm to the displayed lists and to the calendar file KAlarm::UpdateResult status = KAlarm::addEvents(events, dlg, true, true); Undo::EventList undos; AlarmCalendar* resources = AlarmCalendar::resources(); for (int i = 0, end = events.count(); i < end; ++i) { Akonadi::Collection c = resources->collectionForEvent(events[i].itemId()); undos.append(events[i], c); } Undo::saveAdds(undos, i18nc("@info", "Import birthdays")); if (status != KAlarm::UPDATE_FAILED) KAlarm::outputAlarmWarnings(dlg); } } } /****************************************************************************** * Called when the Templates menu item is selected, to display the alarm * template editing dialog. */ void MainWindow::slotTemplates() { if (!mTemplateDlg) { mTemplateDlg = TemplateDlg::create(this); enableTemplateMenuItem(false); // disable menu item in all windows connect(mTemplateDlg, &QDialog::finished, this, &MainWindow::slotTemplatesEnd); mTemplateDlg->show(); } } /****************************************************************************** * Called when the alarm template editing dialog has exited. */ void MainWindow::slotTemplatesEnd() { if (mTemplateDlg) { mTemplateDlg->deleteLater(); // this deletes the dialog once it is safe to do so mTemplateDlg = nullptr; enableTemplateMenuItem(true); // re-enable menu item in all windows } } /****************************************************************************** * Called when the Display System Tray Icon menu item is selected. */ void MainWindow::slotToggleTrayIcon() { theApp()->displayTrayIcon(!theApp()->trayIconDisplayed(), this); } /****************************************************************************** * Called when the Show Resource Selector menu item is selected. */ void MainWindow::slotToggleResourceSelector() { mShowResources = mActionToggleResourceSel->isChecked(); if (mShowResources) { if (mResourcesWidth <= 0) { mResourcesWidth = mResourceSelector->sizeHint().width(); mResourceSelector->resize(mResourcesWidth, mResourceSelector->height()); QList widths = mSplitter->sizes(); if (widths.count() == 1) { int listwidth = widths[0] - mSplitter->handleWidth() - mResourcesWidth; mListView->resize(listwidth, mListView->height()); widths.append(listwidth); widths[0] = mResourcesWidth; } mSplitter->setSizes(widths); } mResourceSelector->show(); } else mResourceSelector->hide(); KConfigGroup config(KSharedConfig::openConfig(), VIEW_GROUP); config.writeEntry(SHOW_RESOURCES_KEY, mShowResources); config.sync(); } /****************************************************************************** * Called when an error occurs in the resource calendar, to display a message. */ void MainWindow::showErrorMessage(const QString& msg) { KAMessageBox::error(this, msg); } /****************************************************************************** * Called when the system tray icon is created or destroyed. * Set the system tray icon menu text according to whether or not the system * tray icon is currently visible. */ void MainWindow::updateTrayIconAction() { mActionToggleTrayIcon->setEnabled(QSystemTrayIcon::isSystemTrayAvailable()); mActionToggleTrayIcon->setChecked(theApp()->trayIconDisplayed()); } /****************************************************************************** * Called when the active status of Find changes. */ void MainWindow::slotFindActive(bool active) { mActionFindNext->setEnabled(active); mActionFindPrev->setEnabled(active); } /****************************************************************************** * Called when the Undo action is selected. */ void MainWindow::slotUndo() { Undo::undo(this, KLocalizedString::removeAcceleratorMarker(mActionUndo->text())); } /****************************************************************************** * Called when the Redo action is selected. */ void MainWindow::slotRedo() { Undo::redo(this, KLocalizedString::removeAcceleratorMarker(mActionRedo->text())); } /****************************************************************************** * Called when an Undo item is selected. */ void MainWindow::slotUndoItem(QAction* action) { int id = mUndoMenuIds[action]; Undo::undo(id, this, Undo::actionText(Undo::UNDO, id)); } /****************************************************************************** * Called when a Redo item is selected. */ void MainWindow::slotRedoItem(QAction* action) { int id = mUndoMenuIds[action]; Undo::redo(id, this, Undo::actionText(Undo::REDO, id)); } /****************************************************************************** * Called when the Undo menu is about to show. * Populates the menu. */ void MainWindow::slotInitUndoMenu() { initUndoMenu(mActionUndo->menu(), Undo::UNDO); } /****************************************************************************** * Called when the Redo menu is about to show. * Populates the menu. */ void MainWindow::slotInitRedoMenu() { initUndoMenu(mActionRedo->menu(), Undo::REDO); } /****************************************************************************** * Populate the undo or redo menu. */ void MainWindow::initUndoMenu(QMenu* menu, Undo::Type type) { menu->clear(); mUndoMenuIds.clear(); const QString& action = (type == Undo::UNDO) ? undoTextStripped : redoTextStripped; QList ids = Undo::ids(type); for (int i = 0, end = ids.count(); i < end; ++i) { int id = ids[i]; QString actText = Undo::actionText(type, id); QString descrip = Undo::description(type, id); QString text = descrip.isEmpty() ? i18nc("@action Undo/Redo [action]", "%1 %2", action, actText) : i18nc("@action Undo [action]: message", "%1 %2: %3", action, actText, descrip); QAction* act = menu->addAction(text); mUndoMenuIds[act] = id; } } /****************************************************************************** * Called when the status of the Undo or Redo list changes. * Change the Undo or Redo text to include the action which would be undone/redone. */ void MainWindow::slotUndoStatus(const QString& undo, const QString& redo) { if (undo.isNull()) { mActionUndo->setEnabled(false); mActionUndo->setText(undoText); } else { mActionUndo->setEnabled(true); mActionUndo->setText(QStringLiteral("%1 %2").arg(undoText, undo)); } if (redo.isNull()) { mActionRedo->setEnabled(false); mActionRedo->setText(redoText); } else { mActionRedo->setEnabled(true); mActionRedo->setText(QStringLiteral("%1 %2").arg(redoText, redo)); } } /****************************************************************************** * Called when the Refresh Alarms menu item is selected. */ void MainWindow::slotRefreshAlarms() { KAlarm::refreshAlarms(); } /****************************************************************************** * Called when the "Configure KAlarm" menu item is selected. */ void MainWindow::slotPreferences() { KAlarmPrefDlg::display(); } /****************************************************************************** * Called when the Configure Keys menu item is selected. */ void MainWindow::slotConfigureKeys() { KShortcutsDialog::configure(actionCollection(), KShortcutsEditor::LetterShortcutsAllowed, this); } /****************************************************************************** * Called when the Configure Toolbars menu item is selected. */ void MainWindow::slotConfigureToolbar() { KConfigGroup grp(KSharedConfig::openConfig()->group(WINDOW_NAME)); saveMainWindowSettings(grp); KEditToolBar dlg(factory()); connect(&dlg, &KEditToolBar::newToolBarConfig, this, &MainWindow::slotNewToolbarConfig); dlg.exec(); } /****************************************************************************** * Called when OK or Apply is clicked in the Configure Toolbars dialog, to save * the new configuration. */ void MainWindow::slotNewToolbarConfig() { createGUI(UI_FILE); applyMainWindowSettings(KSharedConfig::openConfig()->group(WINDOW_NAME)); } /****************************************************************************** * Called when the Quit menu item is selected. * Note that this must be called by the event loop, not directly from the menu * item, since otherwise the window will be deleted while still processing the * menu, resulting in a crash. */ void MainWindow::slotQuit() { theApp()->doQuit(this); } /****************************************************************************** * Called when the user or the session manager attempts to close the window. */ void MainWindow::closeEvent(QCloseEvent* ce) { if (!qApp->isSavingSession()) { // The user (not the session manager) wants to close the window. if (isTrayParent()) { // It's the parent window of the system tray icon, so just hide // it to prevent the system tray icon closing. hide(); theApp()->quitIf(); ce->ignore(); return; } } ce->accept(); } /****************************************************************************** * Called when the drag cursor enters a main or system tray window, to accept * or reject the dragged object. */ void MainWindow::executeDragEnterEvent(QDragEnterEvent* e) { const QMimeData* data = e->mimeData(); bool accept = ICalDrag::canDecode(data) ? !e->source() // don't accept "text/calendar" objects from this application : data->hasText() || data->hasUrls() || KPIM::MailList::canDecode(data); if (accept) e->acceptProposedAction(); } /****************************************************************************** * Called when an object is dropped on the window. * If the object is recognised, the edit alarm dialog is opened appropriately. */ void MainWindow::dropEvent(QDropEvent* e) { executeDropEvent(this, e); } static QString getMailHeader(const char* header, KMime::Content& content) { KMime::Headers::Base* hd = content.headerByType(header); return hd ? hd->asUnicodeString() : QString(); } /****************************************************************************** * Called when an object is dropped on a main or system tray window, to * evaluate the action required and extract the text. */ void MainWindow::executeDropEvent(MainWindow* win, QDropEvent* e) { qCDebug(KALARM_LOG) << "MainWindow::executeDropEvent: Formats:" << e->mimeData()->formats(); const QMimeData* data = e->mimeData(); KAEvent::SubAction action = KAEvent::MESSAGE; QByteArray bytes; AlarmText alarmText; KPIM::MailList mailList; QList files; MemoryCalendar::Ptr calendar(new MemoryCalendar(Preferences::timeSpecAsZone())); #ifndef NDEBUG QString fmts = data->formats().join(QStringLiteral(", ")); 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[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; KPIM::MailSummary& summary = mailList[0]; QDateTime dt; dt.setTime_t(summary.date()); 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"; Event::List events = calendar->rawEvents(); if (!events.isEmpty()) { KAEvent ev(events[0]); KAlarm::editNewAlarm(&ev, win); return; } // If todos are included, use the first todo Todo::List todos = calendar->rawTodos(); if (todos.isEmpty()) return; Todo::Ptr todo = todos[0]; alarmText.setTodo(todo); KADateTime start(todo->dtStart(true)); if (!start.isValid() && todo->hasDueDate()) start = KADateTime(todo->dtDue(true)); 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); if (todo->recurs()) { ev.setRecurrence(*todo->recurrence()); ev.setNextOccurrence(KADateTime::currentUtcDateTime()); } ev.endChanges(); KAlarm::editNewAlarm(&ev, win); return; } else if (!(files = data->urls()).isEmpty()) { 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(files[0]).name(); action = mimeTypeName.startsWith(QStringLiteral("audio/")) ? KAEvent::AUDIO : KAEvent::FILE; alarmText.setText(files[0].toDisplayString()); } else if (data->hasText()) { 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 = !CollectionControlModel::enabledCollections(CalEvent::ACTIVE, true).isEmpty(); bool templat = !CollectionControlModel::enabledCollections(CalEvent::TEMPLATE, true).isEmpty(); for (int i = 0, end = mWindowList.count(); i < end; ++i) { MainWindow* w = mWindowList[i]; w->mActionImportAlarms->setEnabled(active || templat); w->mActionImportBirthdays->setEnabled(active); w->mActionCreateTemplate->setEnabled(templat); // Note: w->mActionNew enabled status is set in the NewAlarmAction class. w->slotSelection(); } } /****************************************************************************** * Called when the selected items in the ListView change. * Enables the actions appropriately. */ void MainWindow::slotSelection() { // Find which events have been selected QVector events = mListView->selectedEvents(); int count = events.count(); if (!count) { selectionCleared(); // disable actions Q_EMIT selectionChanged(); return; } // Find whether there are any writable resources bool active = mActionNew->isEnabled(); bool readOnly = false; bool allArchived = true; bool enableReactivate = true; bool enableEnableDisable = true; bool enableEnable = false; bool enableDisable = false; AlarmCalendar* resources = AlarmCalendar::resources(); const KADateTime now = KADateTime::currentUtcDateTime(); for (int i = 0; i < count; ++i) { KAEvent* ev = resources->event(EventId(events.at(i))); // get up-to-date status KAEvent* event = ev ? ev : &events[i]; bool expired = event->expired(); if (!expired) allArchived = false; if (resources->eventReadOnly(event->itemId())) 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) && !CollectionControlModel::enabledCollections(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; Akonadi::Collection c = AkonadiModel::instance()->collection(event); KAlarm::updateEditedAlarm(dlg, event, c); } /****************************************************************************** * 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/messagewin.cpp b/src/messagewin.cpp index 371da0f8..d5c21d7d 100644 --- a/src/messagewin.cpp +++ b/src/messagewin.cpp @@ -1,2438 +1,2432 @@ /* * messagewin.cpp - displays an alarm message * Program: kalarm * Copyright © 2001-2019 David Jarvie * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "config-kalarm.h" #include "kalarm.h" #include "messagewin_p.h" #include "messagewin.h" #include "alarmcalendar.h" #include "autoqpointer.h" #include "collectionmodel.h" #include "deferdlg.h" #include "desktop.h" #include "editdlg.h" #include "functions.h" #include "kalarmapp.h" #include "mainwindow.h" #include "messagebox.h" #include "preferences.h" #include "pushbutton.h" #include "shellprocess.h" #include "synchtimer.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if KDEPIM_HAVE_X11 #include #include #include +#include #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "kalarm_debug.h" #include #include using namespace KCalCore; using namespace KAlarmCal; #if KDEPIM_HAVE_X11 enum FullScreenType { NoFullScreen = 0, FullScreen = 1, FullScreenActive = 2 }; static FullScreenType haveFullScreenWindow(int screen); static FullScreenType findFullScreenWindows(const QVector& screenRects, QVector& screenTypes); #endif #include "kmailinterface.h" static const QLatin1String KMAIL_DBUS_SERVICE("org.kde.kmail"); static const QLatin1String KMAIL_DBUS_PATH("/KMail"); // The delay for enabling message window buttons if a zero delay is // configured, i.e. the windows are placed far from the cursor. static const int proximityButtonDelay = 1000; // (milliseconds) static const int proximityMultiple = 10; // multiple of button height distance from cursor for proximity // A text label widget which can be scrolled and copied with the mouse class MessageText : public KTextEdit { public: MessageText(QWidget* parent = nullptr) : KTextEdit(parent), mNewLine(false) { setReadOnly(true); setFrameStyle(NoFrame); setLineWrapMode(NoWrap); } int scrollBarHeight() const { return horizontalScrollBar()->height(); } int scrollBarWidth() const { return verticalScrollBar()->width(); } void setBackgroundColour(const QColor& c) { QPalette pal = viewport()->palette(); pal.setColor(viewport()->backgroundRole(), c); viewport()->setPalette(pal); } QSize sizeHint() const override { const QSizeF docsize = document()->size(); return QSize(static_cast(docsize.width() + 0.99) + verticalScrollBar()->width(), static_cast(docsize.height() + 0.99) + horizontalScrollBar()->height()); } bool newLine() const { return mNewLine; } void setNewLine(bool nl) { mNewLine = nl; } private: bool mNewLine; }; // Basic flags for the window static const Qt::WindowFlags WFLAGS = Qt::WindowStaysOnTopHint; static const Qt::WindowFlags WFLAGS2 = Qt::WindowContextHelpButtonHint; static const Qt::WidgetAttribute WidgetFlags = Qt::WA_DeleteOnClose; // Error message bit masks enum { ErrMsg_Speak = 0x01, ErrMsg_AudioFile = 0x02 }; QList MessageWin::mWindowList; QMap MessageWin::mErrorMessages; bool MessageWin::mRedisplayed = false; // There can only be one audio thread at a time: trying to play multiple // sound files simultaneously would result in a cacophony, and besides // that, Phonon currently crashes... QPointer MessageWin::mAudioThread; MessageWin* AudioThread::mAudioOwner = nullptr; /****************************************************************************** * Construct the message window for the specified alarm. * Other alarms in the supplied event may have been updated by the caller, so * the whole event needs to be stored for updating the calendar file when it is * displayed. */ MessageWin::MessageWin(const KAEvent* event, const KAAlarm& alarm, int flags) : MainWindowBase(nullptr, static_cast(WFLAGS | WFLAGS2 | ((flags & ALWAYS_HIDE) || getWorkAreaAndModal() ? Qt::WindowType(0) : Qt::X11BypassWindowManagerHint))), mMessage(event->cleanText()), mFont(event->font()), mBgColour(event->bgColour()), mFgColour(event->fgColour()), mEventItemId(event->itemId()), mEventId(*event), mAudioFile(event->audioFile()), mVolume(event->soundVolume()), mFadeVolume(event->fadeVolume()), mFadeSeconds(qMin(event->fadeSeconds(), 86400)), mDefaultDeferMinutes(event->deferDefaultMinutes()), mAlarmType(alarm.type()), mAction(event->actionSubType()), mKMailSerialNumber(event->kmailSerialNumber()), mCommandError(event->commandError()), mRestoreHeight(0), mAudioRepeatPause(event->repeatSoundPause()), mConfirmAck(event->confirmAck()), mNoDefer(true), mInvalid(false), mEvent(*event), mOriginalEvent(*event), mCollection(AlarmCalendar::resources()->collectionForEvent(mEventItemId)), mTimeLabel(nullptr), mRemainingText(nullptr), mEditButton(nullptr), mDeferButton(nullptr), mSilenceButton(nullptr), mKMailButton(nullptr), mCommandText(nullptr), mDontShowAgainCheck(nullptr), mEditDlg(nullptr), mDeferDlg(nullptr), mAlwaysHide(flags & ALWAYS_HIDE), mErrorWindow(false), mInitialised(false), mNoPostAction(alarm.type() & KAAlarm::REMINDER_ALARM), mRecreating(false), mBeep(event->beep()), mSpeak(event->speak()), mRescheduleEvent(!(flags & NO_RESCHEDULE)), mShown(false), mPositioning(false), mNoCloseConfirm(false), mDisableDeferral(false) { qCDebug(KALARM_LOG) << "MessageWin:" << (void*)this << "event" << mEventId; setAttribute(static_cast(WidgetFlags)); setWindowModality(Qt::WindowModal); setObjectName(QStringLiteral("MessageWin")); // used by LikeBack if (alarm.type() & KAAlarm::REMINDER_ALARM) { if (event->reminderMinutes() < 0) { event->previousOccurrence(alarm.dateTime(false).effectiveKDateTime(), mDateTime, false); if (!mDateTime.isValid() && event->repeatAtLogin()) mDateTime = alarm.dateTime().addSecs(event->reminderMinutes() * 60); } else mDateTime = event->mainDateTime(true); } else mDateTime = alarm.dateTime(true); if (!(flags & (NO_INIT_VIEW | ALWAYS_HIDE))) { const bool readonly = AlarmCalendar::resources()->eventReadOnly(mEventItemId); mShowEdit = !mEventId.isEmpty() && !readonly; mNoDefer = readonly || (flags & NO_DEFER) || alarm.repeatAtLogin(); initView(); } // Set to save settings automatically, but don't save window size. // File alarm window size is saved elsewhere. setAutoSaveSettings(QStringLiteral("MessageWin"), false); mWindowList.append(this); if (event->autoClose()) mCloseTime = alarm.dateTime().effectiveKDateTime().toUtc().qDateTime().addSecs(event->lateCancel() * 60); if (mAlwaysHide) { hide(); displayComplete(); // play audio, etc. } } /****************************************************************************** * Display an error message window. * If 'dontShowAgain' is non-null, a "Don't show again" option is displayed. Note * that the option is specific to 'event'. */ void MessageWin::showError(const KAEvent& event, const DateTime& alarmDateTime, const QStringList& errmsgs, const QString& dontShowAgain) { if (!dontShowAgain.isEmpty() && KAlarm::dontShowErrors(EventId(event), dontShowAgain)) return; // Don't pile up duplicate error messages for the same alarm for (int i = 0, end = mWindowList.count(); i < end; ++i) { const MessageWin* w = mWindowList[i]; if (w->mErrorWindow && w->mEventId == EventId(event) && w->mErrorMsgs == errmsgs && w->mDontShowAgain == dontShowAgain) return; } (new MessageWin(&event, alarmDateTime, errmsgs, dontShowAgain))->show(); } /****************************************************************************** * Construct the message window for a specified error message. * If 'dontShowAgain' is non-null, a "Don't show again" option is displayed. Note * that the option is specific to 'event'. */ MessageWin::MessageWin(const KAEvent* event, const DateTime& alarmDateTime, const QStringList& errmsgs, const QString& dontShowAgain) : MainWindowBase(nullptr, WFLAGS | WFLAGS2), mMessage(event->cleanText()), mDateTime(alarmDateTime), mEventItemId(event->itemId()), mEventId(*event), mAlarmType(KAAlarm::MAIN_ALARM), mAction(event->actionSubType()), mKMailSerialNumber(0), mCommandError(KAEvent::CMD_NO_ERROR), mErrorMsgs(errmsgs), mDontShowAgain(dontShowAgain), mRestoreHeight(0), mConfirmAck(false), mShowEdit(false), mNoDefer(true), mInvalid(false), mEvent(*event), mOriginalEvent(*event), mTimeLabel(nullptr), mRemainingText(nullptr), mEditButton(nullptr), mDeferButton(nullptr), mSilenceButton(nullptr), mKMailButton(nullptr), mCommandText(nullptr), mDontShowAgainCheck(nullptr), mEditDlg(nullptr), mDeferDlg(nullptr), mAlwaysHide(false), mErrorWindow(true), mInitialised(false), mNoPostAction(true), mRecreating(false), mRescheduleEvent(false), mShown(false), mPositioning(false), mNoCloseConfirm(false), mDisableDeferral(false) { qCDebug(KALARM_LOG) << "MessageWin: errmsg"; setAttribute(static_cast(WidgetFlags)); setWindowModality(Qt::WindowModal); setObjectName(QStringLiteral("ErrorWin")); // used by LikeBack getWorkAreaAndModal(); initView(); mWindowList.append(this); } /****************************************************************************** * Construct the message window for restoration by session management. * The window is initialised by readProperties(). */ MessageWin::MessageWin() : MainWindowBase(nullptr, WFLAGS), mTimeLabel(nullptr), mRemainingText(nullptr), mEditButton(nullptr), mDeferButton(nullptr), mSilenceButton(nullptr), mKMailButton(nullptr), mCommandText(nullptr), mDontShowAgainCheck(nullptr), mEditDlg(nullptr), mDeferDlg(nullptr), mAlwaysHide(false), mErrorWindow(false), mInitialised(false), mRecreating(false), mRescheduleEvent(false), mShown(false), mPositioning(false), mNoCloseConfirm(false), mDisableDeferral(false) { qCDebug(KALARM_LOG) << "MessageWin:" << (void*)this << "restore"; setAttribute(WidgetFlags); setWindowModality(Qt::WindowModal); setObjectName(QStringLiteral("RestoredMsgWin")); // used by LikeBack getWorkAreaAndModal(); mWindowList.append(this); } /****************************************************************************** * Destructor. Perform any post-alarm actions before tidying up. */ MessageWin::~MessageWin() { qCDebug(KALARM_LOG) << "~MessageWin" << (void*)this << mEventId; if (AudioThread::mAudioOwner == this && !mAudioThread.isNull()) mAudioThread->quit(); mErrorMessages.remove(mEventId); mWindowList.removeAll(this); if (!mRecreating) { if (!mNoPostAction && !mEvent.postAction().isEmpty()) theApp()->alarmCompleted(mEvent); if (!instanceCount(true)) theApp()->quitIf(); // no visible windows remain - check whether to quit } } /****************************************************************************** * Construct the message window. */ void MessageWin::initView() { const bool reminder = (!mErrorWindow && (mAlarmType & KAAlarm::REMINDER_ALARM)); const int leading = fontMetrics().leading(); setCaption((mAlarmType & KAAlarm::REMINDER_ALARM) ? i18nc("@title:window", "Reminder") : i18nc("@title:window", "Message")); QWidget* topWidget = new QWidget(this); setCentralWidget(topWidget); QVBoxLayout* topLayout = new QVBoxLayout(topWidget); int dcm = style()->pixelMetric(QStyle::PM_DefaultChildMargin); topLayout->setContentsMargins(dcm, dcm, dcm, dcm); topLayout->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); QPalette labelPalette = palette(); labelPalette.setColor(backgroundRole(), labelPalette.color(QPalette::Window)); // Show the alarm date/time, together with a reminder text where appropriate. // Alarm date/time: display time zone if not local time zone. mTimeLabel = new QLabel(topWidget); mTimeLabel->setText(dateTimeToDisplay()); mTimeLabel->setFrameStyle(QFrame::StyledPanel); mTimeLabel->setPalette(labelPalette); mTimeLabel->setAutoFillBackground(true); topLayout->addWidget(mTimeLabel, 0, Qt::AlignHCenter); mTimeLabel->setWhatsThis(i18nc("@info:whatsthis", "The scheduled date/time for the message (as opposed to the actual time of display).")); if (mDateTime.isValid()) { // Reminder if (reminder) { // Create a label "time\nReminder" by inserting the time at the // start of the translated string, allowing for possible HTML tags // enclosing "Reminder". QString s = i18nc("@info", "Reminder"); QRegExp re(QStringLiteral("^(<[^>]+>)*")); re.indexIn(s); s.insert(re.matchedLength(), mTimeLabel->text() + QLatin1String("
")); mTimeLabel->setText(s); mTimeLabel->setAlignment(Qt::AlignHCenter); } } else mTimeLabel->hide(); if (!mErrorWindow) { // It's a normal alarm message window switch (mAction) { case KAEvent::FILE: { // Display the file name KSqueezedTextLabel* label = new KSqueezedTextLabel(mMessage, topWidget); label->setFrameStyle(QFrame::StyledPanel); label->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard); label->setPalette(labelPalette); label->setAutoFillBackground(true); label->setWhatsThis(i18nc("@info:whatsthis", "The file whose contents are displayed below")); topLayout->addWidget(label, 0, Qt::AlignHCenter); // Display contents of file const QUrl url = QUrl::fromUserInput(mMessage, QString(), QUrl::AssumeLocalFile); auto statJob = KIO::stat(url, KIO::StatJob::SourceSide, 0, KIO::HideProgressInfo); const bool exists = statJob->exec(); const bool isDir = statJob->statResult().isDir(); bool opened = false; if (exists && !isDir) { auto job = KIO::storedGet(url); KJobWidgets::setWindow(job, MainWindow::mainMainWindow()); if (job->exec()) { opened = true; const QByteArray data = job->data(); QTemporaryFile tmpFile; tmpFile.write(data); tmpFile.seek(0); QTextBrowser* view = new QTextBrowser(topWidget); view->setFrameStyle(QFrame::NoFrame); view->setWordWrapMode(QTextOption::NoWrap); QPalette pal = view->viewport()->palette(); pal.setColor(view->viewport()->backgroundRole(), mBgColour); view->viewport()->setPalette(pal); view->setTextColor(mFgColour); view->setCurrentFont(mFont); QMimeDatabase db; QMimeType mime = db.mimeTypeForUrl(url); if (mime.name() == QLatin1String("application/octet-stream")) mime = db.mimeTypeForData(&tmpFile); switch (KAlarm::fileType(mime)) { case KAlarm::Image: view->setHtml(QLatin1String("")); break; case KAlarm::TextFormatted: view->QTextBrowser::setSource(QUrl::fromLocalFile(tmpFile.fileName())); //krazy:exclude=qclasses break; default: { view->setPlainText(QString::fromUtf8(data)); break; } } view->setMinimumSize(view->sizeHint()); topLayout->addWidget(view); // Set the default size to 20 lines square. // Note that after the first file has been displayed, this size // is overridden by the user-set default stored in the config file. // So there is no need to calculate an accurate size. int h = 20*view->fontMetrics().lineSpacing() + 2*view->frameWidth(); view->resize(QSize(h, h).expandedTo(view->sizeHint())); view->setWhatsThis(i18nc("@info:whatsthis", "The contents of the file to be displayed")); } } if (!exists || isDir || !opened) { mErrorMsgs += isDir ? i18nc("@info", "File is a folder") : exists ? i18nc("@info", "Failed to open file") : i18nc("@info", "File not found"); } break; } case KAEvent::MESSAGE: { // Message label // Using MessageText instead of QLabel allows scrolling and mouse copying MessageText* text = new MessageText(topWidget); text->setAutoFillBackground(true); text->setBackgroundColour(mBgColour); text->setTextColor(mFgColour); text->setCurrentFont(mFont); text->insertPlainText(mMessage); const int lineSpacing = text->fontMetrics().lineSpacing(); const QSize s = text->sizeHint(); const int h = s.height(); text->setMaximumHeight(h + text->scrollBarHeight()); text->setMinimumHeight(qMin(h, lineSpacing*4)); text->setMaximumWidth(s.width() + text->scrollBarWidth()); text->setWhatsThis(i18nc("@info:whatsthis", "The alarm message")); const int vspace = lineSpacing/2; const int hspace = lineSpacing - style()->pixelMetric(QStyle::PM_DefaultChildMargin); topLayout->addSpacing(vspace); topLayout->addStretch(); // Don't include any horizontal margins if message is 2/3 screen width if (text->sizeHint().width() >= KAlarm::desktopWorkArea(mScreenNumber).width()*2/3) topLayout->addWidget(text, 1, Qt::AlignHCenter); else { QHBoxLayout* layout = new QHBoxLayout(); layout->addSpacing(hspace); layout->addWidget(text, 1, Qt::AlignHCenter); layout->addSpacing(hspace); topLayout->addLayout(layout); } if (!reminder) topLayout->addStretch(); break; } case KAEvent::COMMAND: { mCommandText = new MessageText(topWidget); mCommandText->setBackgroundColour(mBgColour); mCommandText->setTextColor(mFgColour); mCommandText->setCurrentFont(mFont); topLayout->addWidget(mCommandText); mCommandText->setWhatsThis(i18nc("@info:whatsthis", "The output of the alarm's command")); theApp()->execCommandAlarm(mEvent, mEvent.alarm(mAlarmType), this, SLOT(readProcessOutput(ShellProcess*))); break; } case KAEvent::EMAIL: default: break; } if (reminder && mEvent.reminderMinutes() > 0) { // Advance reminder: show remaining time until the actual alarm mRemainingText = new QLabel(topWidget); mRemainingText->setFrameStyle(QFrame::Box | QFrame::Raised); mRemainingText->setContentsMargins(leading, leading, leading, leading); mRemainingText->setPalette(labelPalette); mRemainingText->setAutoFillBackground(true); if (mDateTime.isDateOnly() || KADateTime::currentLocalDate().daysTo(mDateTime.date()) > 0) { setRemainingTextDay(); MidnightTimer::connect(this, SLOT(setRemainingTextDay())); // update every day } else { setRemainingTextMinute(); MinuteTimer::connect(this, SLOT(setRemainingTextMinute())); // update every minute } topLayout->addWidget(mRemainingText, 0, Qt::AlignHCenter); topLayout->addSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); topLayout->addStretch(); } } else { // It's an error message switch (mAction) { case KAEvent::EMAIL: { // Display the email addresses and subject. QFrame* frame = new QFrame(topWidget); frame->setFrameStyle(QFrame::Box | QFrame::Raised); frame->setWhatsThis(i18nc("@info:whatsthis", "The email to send")); topLayout->addWidget(frame, 0, Qt::AlignHCenter); QGridLayout* grid = new QGridLayout(frame); grid->setContentsMargins(dcm, dcm, dcm, dcm); grid->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); QLabel* label = new QLabel(i18nc("@info Email addressee", "To:"), frame); label->setFixedSize(label->sizeHint()); grid->addWidget(label, 0, 0, Qt::AlignLeft); label = new QLabel(mEvent.emailAddresses(QStringLiteral("\n")), frame); label->setFixedSize(label->sizeHint()); grid->addWidget(label, 0, 1, Qt::AlignLeft); label = new QLabel(i18nc("@info Email subject", "Subject:"), frame); label->setFixedSize(label->sizeHint()); grid->addWidget(label, 1, 0, Qt::AlignLeft); label = new QLabel(mEvent.emailSubject(), frame); label->setFixedSize(label->sizeHint()); grid->addWidget(label, 1, 1, Qt::AlignLeft); break; } case KAEvent::COMMAND: case KAEvent::FILE: case KAEvent::MESSAGE: default: // Just display the error message strings break; } } if (!mErrorMsgs.count()) { topWidget->setAutoFillBackground(true); QPalette palette = topWidget->palette(); palette.setColor(topWidget->backgroundRole(), mBgColour); topWidget->setPalette(palette); } else { setCaption(i18nc("@title:window", "Error")); QHBoxLayout* layout = new QHBoxLayout(); int m = 2 * dcm; layout->setContentsMargins(m, m, m, m); layout->addStretch(); topLayout->addLayout(layout); QLabel* label = new QLabel(topWidget); label->setPixmap(QIcon::fromTheme(QStringLiteral("dialog-error")).pixmap(IconSize(KIconLoader::Desktop), IconSize(KIconLoader::Desktop))); label->setFixedSize(label->sizeHint()); layout->addWidget(label, 0, Qt::AlignRight); QVBoxLayout* vlayout = new QVBoxLayout(); layout->addLayout(vlayout); for (QStringList::ConstIterator it = mErrorMsgs.constBegin(); it != mErrorMsgs.constEnd(); ++it) { label = new QLabel(*it, topWidget); label->setFixedSize(label->sizeHint()); vlayout->addWidget(label, 0, Qt::AlignLeft); } layout->addStretch(); if (!mDontShowAgain.isEmpty()) { mDontShowAgainCheck = new QCheckBox(i18nc("@option:check", "Do not display this error message again for this alarm"), topWidget); mDontShowAgainCheck->setFixedSize(mDontShowAgainCheck->sizeHint()); topLayout->addWidget(mDontShowAgainCheck, 0, Qt::AlignLeft); } } QGridLayout* grid = new QGridLayout(); grid->setColumnStretch(0, 1); // keep the buttons right-adjusted in the window topLayout->addLayout(grid); int gridIndex = 1; // Close button mOkButton = new PushButton(KStandardGuiItem::close(), topWidget); // Prevent accidental acknowledgement of the message if the user is typing when the window appears mOkButton->clearFocus(); mOkButton->setFocusPolicy(Qt::ClickFocus); // don't allow keyboard selection mOkButton->setFixedSize(mOkButton->sizeHint()); connect(mOkButton, &QAbstractButton::clicked, this, &MessageWin::slotOk); grid->addWidget(mOkButton, 0, gridIndex++, Qt::AlignHCenter); mOkButton->setWhatsThis(i18nc("@info:whatsthis", "Acknowledge the alarm")); if (mShowEdit) { // Edit button mEditButton = new PushButton(i18nc("@action:button", "&Edit..."), topWidget); mEditButton->setFocusPolicy(Qt::ClickFocus); // don't allow keyboard selection mEditButton->setFixedSize(mEditButton->sizeHint()); connect(mEditButton, &QAbstractButton::clicked, this, &MessageWin::slotEdit); grid->addWidget(mEditButton, 0, gridIndex++, Qt::AlignHCenter); mEditButton->setWhatsThis(i18nc("@info:whatsthis", "Edit the alarm.")); } // Defer button mDeferButton = new PushButton(i18nc("@action:button", "&Defer..."), topWidget); mDeferButton->setFocusPolicy(Qt::ClickFocus); // don't allow keyboard selection mDeferButton->setFixedSize(mDeferButton->sizeHint()); connect(mDeferButton, &QAbstractButton::clicked, this, &MessageWin::slotDefer); grid->addWidget(mDeferButton, 0, gridIndex++, Qt::AlignHCenter); mDeferButton->setWhatsThis(xi18nc("@info:whatsthis", "Defer the alarm until later." "You will be prompted to specify when the alarm should be redisplayed.")); if (mNoDefer) mDeferButton->hide(); else setDeferralLimit(mEvent); // ensure that button is disabled when alarm can't be deferred any more if (!mAudioFile.isEmpty() && (mVolume || mFadeVolume > 0)) { // Silence button to stop sound repetition const QPixmap pixmap = MainBarIcon(QStringLiteral("media-playback-stop")); mSilenceButton = new PushButton(topWidget); mSilenceButton->setIcon(pixmap); grid->addWidget(mSilenceButton, 0, gridIndex++, Qt::AlignHCenter); mSilenceButton->setToolTip(i18nc("@info:tooltip", "Stop sound")); mSilenceButton->setWhatsThis(i18nc("@info:whatsthis", "Stop playing the sound")); // To avoid getting in a mess, disable the button until sound playing has been set up mSilenceButton->setEnabled(false); } KIconLoader iconLoader; if (mKMailSerialNumber) { // KMail button const QPixmap pixmap = iconLoader.loadIcon(QStringLiteral("internet-mail"), KIconLoader::MainToolbar); mKMailButton = new PushButton(topWidget); mKMailButton->setIcon(pixmap); connect(mKMailButton, &QAbstractButton::clicked, this, &MessageWin::slotShowKMailMessage); grid->addWidget(mKMailButton, 0, gridIndex++, Qt::AlignHCenter); mKMailButton->setToolTip(xi18nc("@info:tooltip Locate this email in KMail", "Locate in KMail")); mKMailButton->setWhatsThis(xi18nc("@info:whatsthis", "Locate and highlight this email in KMail")); } // KAlarm button const QPixmap pixmap = iconLoader.loadIcon(KAboutData::applicationData().componentName(), KIconLoader::MainToolbar); mKAlarmButton = new PushButton(topWidget); mKAlarmButton->setIcon(pixmap); connect(mKAlarmButton, &QAbstractButton::clicked, this, &MessageWin::displayMainWindow); grid->addWidget(mKAlarmButton, 0, gridIndex++, Qt::AlignHCenter); mKAlarmButton->setToolTip(xi18nc("@info:tooltip", "Activate KAlarm")); mKAlarmButton->setWhatsThis(xi18nc("@info:whatsthis", "Activate KAlarm")); int butsize = mKAlarmButton->sizeHint().height(); if (mSilenceButton) butsize = qMax(butsize, mSilenceButton->sizeHint().height()); if (mKMailButton) butsize = qMax(butsize, mKMailButton->sizeHint().height()); mKAlarmButton->setFixedSize(butsize, butsize); if (mSilenceButton) mSilenceButton->setFixedSize(butsize, butsize); if (mKMailButton) mKMailButton->setFixedSize(butsize, butsize); // Disable all buttons initially, to prevent accidental clicking on if they happen to be // under the mouse just as the window appears. mOkButton->setEnabled(false); if (mDeferButton->isVisible()) mDeferButton->setEnabled(false); if (mEditButton) mEditButton->setEnabled(false); if (mKMailButton) mKMailButton->setEnabled(false); mKAlarmButton->setEnabled(false); topLayout->activate(); setMinimumSize(QSize(grid->sizeHint().width() + 2 * style()->pixelMetric(QStyle::PM_DefaultChildMargin), sizeHint().height())); const bool modal = !(windowFlags() & Qt::X11BypassWindowManagerHint); NET::States wstate = NET::Sticky | NET::StaysOnTop; if (modal) wstate |= NET::Modal; WId winid = winId(); KWindowSystem::setState(winid, wstate); KWindowSystem::setOnAllDesktops(winid, true); mInitialised = true; // the window's widgets have been created } /****************************************************************************** * Return the number of message windows, optionally excluding always-hidden ones. */ int MessageWin::instanceCount(bool excludeAlwaysHidden) { int count = mWindowList.count(); if (excludeAlwaysHidden) { for (MessageWin* win : qAsConst(mWindowList)) { if (win->mAlwaysHide) --count; } } return count; } bool MessageWin::hasDefer() const { return mDeferButton && mDeferButton->isVisible(); } /****************************************************************************** * Show the Defer button when it was previously hidden. */ void MessageWin::showDefer() { if (mDeferButton) { mNoDefer = false; mDeferButton->show(); setDeferralLimit(mEvent); // ensure that button is disabled when alarm can't be deferred any more resize(sizeHint()); } } /****************************************************************************** * Convert a reminder window into a normal alarm window. */ void MessageWin::cancelReminder(const KAEvent& event, const KAAlarm& alarm) { if (!mInitialised) return; mDateTime = alarm.dateTime(true); mNoPostAction = false; mAlarmType = alarm.type(); if (event.autoClose()) mCloseTime = alarm.dateTime().effectiveKDateTime().toUtc().qDateTime().addSecs(event.lateCancel() * 60); setCaption(i18nc("@title:window", "Message")); mTimeLabel->setText(dateTimeToDisplay()); if (mRemainingText) mRemainingText->hide(); MidnightTimer::disconnect(this, SLOT(setRemainingTextDay())); MinuteTimer::disconnect(this, SLOT(setRemainingTextMinute())); setMinimumHeight(0); centralWidget()->layout()->activate(); setMinimumHeight(sizeHint().height()); resize(sizeHint()); } /****************************************************************************** * Show the alarm's trigger time. * This is assumed to have previously been hidden. */ void MessageWin::showDateTime(const KAEvent& event, const KAAlarm& alarm) { if (!mTimeLabel) return; mDateTime = (alarm.type() & KAAlarm::REMINDER_ALARM) ? event.mainDateTime(true) : alarm.dateTime(true); if (mDateTime.isValid()) { mTimeLabel->setText(dateTimeToDisplay()); mTimeLabel->show(); } } /****************************************************************************** * Get the trigger time to display. */ QString MessageWin::dateTimeToDisplay() { QString tm; if (mDateTime.isValid()) { if (mDateTime.isDateOnly()) tm = QLocale().toString(mDateTime.date(), QLocale::ShortFormat); else { bool showZone = false; if (mDateTime.timeType() == KADateTime::UTC || (mDateTime.timeType() == KADateTime::TimeZone && !mDateTime.isLocalZone())) { // Display time zone abbreviation if it's different from the local // zone. Note that the iCalendar time zone might represent the local // time zone in a slightly different way from the system time zone, // so the zone comparison above might not produce the desired result. const QString tz = mDateTime.kDateTime().toString(QStringLiteral("%Z")); KADateTime local = mDateTime.kDateTime(); local.setTimeSpec(KADateTime::Spec::LocalZone()); showZone = (local.toString(QStringLiteral("%Z")) != tz); } const QDateTime dt = mDateTime.qDateTime(); tm = QLocale().toString(dt, QLocale::ShortFormat); if (showZone) tm += QLatin1Char(' ') + mDateTime.timeZone().displayName(dt, QTimeZone::ShortName, QLocale()); } } return tm; } /****************************************************************************** * Set the remaining time text in a reminder window. * Called at the start of every day (at the user-defined start-of-day time). */ void MessageWin::setRemainingTextDay() { QString text; const int days = KADateTime::currentLocalDate().daysTo(mDateTime.date()); if (days <= 0 && !mDateTime.isDateOnly()) { // The alarm is due today, so start refreshing every minute MidnightTimer::disconnect(this, SLOT(setRemainingTextDay())); setRemainingTextMinute(); MinuteTimer::connect(this, SLOT(setRemainingTextMinute())); // update every minute } else { if (days <= 0) text = i18nc("@info", "Today"); else if (days % 7) text = i18ncp("@info", "Tomorrow", "in %1 days' time", days); else text = i18ncp("@info", "in 1 week's time", "in %1 weeks' time", days/7); } mRemainingText->setText(text); } /****************************************************************************** * Set the remaining time text in a reminder window. * Called on every minute boundary. */ void MessageWin::setRemainingTextMinute() { QString text; const int mins = (KADateTime::currentUtcDateTime().secsTo(mDateTime.effectiveKDateTime()) + 59) / 60; if (mins < 60) text = i18ncp("@info", "in 1 minute's time", "in %1 minutes' time", (mins > 0 ? mins : 0)); else if (mins % 60 == 0) text = i18ncp("@info", "in 1 hour's time", "in %1 hours' time", mins/60); else { QString hourText = i18ncp("@item:intext inserted into 'in ... %1 minute's time' below", "1 hour", "%1 hours", mins/60); text = i18ncp("@info '%2' is the previous message '1 hour'/'%1 hours'", "in %2 1 minute's time", "in %2 %1 minutes' time", mins%60, hourText); } mRemainingText->setText(text); } /****************************************************************************** * Called when output is available from the command which is providing the text * for this window. Add the output and resize the window to show it. */ void MessageWin::readProcessOutput(ShellProcess* proc) { const QByteArray data = proc->readAll(); if (!data.isEmpty()) { // Strip any trailing newline, to avoid showing trailing blank line // in message window. if (mCommandText->newLine()) mCommandText->append(QStringLiteral("\n")); const int nl = data.endsWith('\n') ? 1 : 0; mCommandText->setNewLine(nl); mCommandText->insertPlainText(QString::fromLocal8Bit(data.data(), data.length() - nl)); resize(sizeHint()); } } /****************************************************************************** * Save settings to the session managed config file, for restoration * when the program is restored. */ void MessageWin::saveProperties(KConfigGroup& config) { if (mShown && !mErrorWindow && !mAlwaysHide) { config.writeEntry("EventID", mEventId.eventId()); config.writeEntry("EventItemID", mEventItemId); config.writeEntry("AlarmType", static_cast(mAlarmType)); if (mAlarmType == KAAlarm::INVALID_ALARM) qCCritical(KALARM_LOG) << "MessageWin::saveProperties: Invalid alarm: id=" << mEventId << ", alarm count=" << mEvent.alarmCount(); config.writeEntry("Message", mMessage); config.writeEntry("Type", static_cast(mAction)); config.writeEntry("Font", mFont); config.writeEntry("BgColour", mBgColour); config.writeEntry("FgColour", mFgColour); config.writeEntry("ConfirmAck", mConfirmAck); if (mDateTime.isValid()) { //TODO: Write KADateTime when it becomes possible config.writeEntry("Time", mDateTime.effectiveDateTime()); config.writeEntry("DateOnly", mDateTime.isDateOnly()); QByteArray zone; if (mDateTime.isUtc()) zone = "UTC"; else if (mDateTime.timeType() == KADateTime::TimeZone) { const QTimeZone tz = mDateTime.timeZone(); if (tz.isValid()) zone = tz.id(); } config.writeEntry("TimeZone", zone); } if (mCloseTime.isValid()) config.writeEntry("Expiry", mCloseTime); if (mAudioRepeatPause >= 0 && mSilenceButton && mSilenceButton->isEnabled()) { // Only need to restart sound file playing if it's being repeated config.writePathEntry("AudioFile", mAudioFile); config.writeEntry("Volume", static_cast(mVolume * 100)); config.writeEntry("AudioPause", mAudioRepeatPause); } config.writeEntry("Speak", mSpeak); config.writeEntry("Height", height()); config.writeEntry("DeferMins", mDefaultDeferMinutes); config.writeEntry("NoDefer", mNoDefer); config.writeEntry("NoPostAction", mNoPostAction); config.writeEntry("KMailSerial", static_cast(mKMailSerialNumber)); config.writeEntry("CmdErr", static_cast(mCommandError)); config.writeEntry("DontShowAgain", mDontShowAgain); } else config.writeEntry("Invalid", true); } /****************************************************************************** * 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 MessageWin::readProperties(const KConfigGroup& config) { mInvalid = config.readEntry("Invalid", false); QString eventId = config.readEntry("EventID"); mEventItemId = config.readEntry("EventItemID", Akonadi::Item::Id(-1)); mAlarmType = static_cast(config.readEntry("AlarmType", 0)); if (mAlarmType == KAAlarm::INVALID_ALARM) { mInvalid = true; qCCritical(KALARM_LOG) << "MessageWin::readProperties: Invalid alarm: id=" << eventId; } mMessage = config.readEntry("Message"); mAction = static_cast(config.readEntry("Type", 0)); mFont = config.readEntry("Font", QFont()); mBgColour = config.readEntry("BgColour", QColor(Qt::white)); mFgColour = config.readEntry("FgColour", QColor(Qt::black)); mConfirmAck = config.readEntry("ConfirmAck", false); QDateTime invalidDateTime; QDateTime dt = config.readEntry("Time", invalidDateTime); const QByteArray zoneId = config.readEntry("TimeZone").toLatin1(); KADateTime::Spec timeSpec; if (zoneId.isEmpty()) timeSpec = KADateTime::LocalZone; else if (zoneId == "UTC") timeSpec = KADateTime::UTC; else timeSpec = QTimeZone(zoneId); mDateTime = KADateTime(dt.date(), dt.time(), timeSpec); const bool dateOnly = config.readEntry("DateOnly", false); if (dateOnly) mDateTime.setDateOnly(true); mCloseTime = config.readEntry("Expiry", invalidDateTime); mCloseTime.setTimeSpec(Qt::UTC); mAudioFile = config.readPathEntry("AudioFile", QString()); mVolume = static_cast(config.readEntry("Volume", 0)) / 100; mFadeVolume = -1; mFadeSeconds = 0; if (!mAudioFile.isEmpty()) // audio file URL was only saved if it repeats mAudioRepeatPause = config.readEntry("AudioPause", 0); mBeep = false; // don't beep after restart (similar to not playing non-repeated sound file) mSpeak = config.readEntry("Speak", false); mRestoreHeight = config.readEntry("Height", 0); mDefaultDeferMinutes = config.readEntry("DeferMins", 0); mNoDefer = config.readEntry("NoDefer", false); mNoPostAction = config.readEntry("NoPostAction", true); mKMailSerialNumber = static_cast(config.readEntry("KMailSerial", QVariant(QVariant::ULongLong)).toULongLong()); mCommandError = KAEvent::CmdErrType(config.readEntry("CmdErr", static_cast(KAEvent::CMD_NO_ERROR))); mDontShowAgain = config.readEntry("DontShowAgain", QString()); mShowEdit = false; // Temporarily initialise mCollection and mEventId - they will be set by redisplayAlarm() mCollection = Akonadi::Collection(); mEventId = EventId(mCollection.id(), eventId); qCDebug(KALARM_LOG) << "MessageWin::readProperties:" << eventId; if (mAlarmType != KAAlarm::INVALID_ALARM) { // Recreate the event from the calendar file (if possible) if (eventId.isEmpty()) initView(); else { // Close any other window for this alarm which has already been restored by redisplayAlarms() if (!AkonadiModel::instance()->isCollectionTreeFetched()) { connect(AkonadiModel::instance(), &Akonadi::EntityTreeModel::collectionTreeFetched, this, &MessageWin::showRestoredAlarm); return; } redisplayAlarm(); } } } /****************************************************************************** * Fetch the restored alarm from the calendar and redisplay it in this window. */ void MessageWin::showRestoredAlarm() { qCDebug(KALARM_LOG) << "MessageWin::showRestoredAlarm:" << mEventId; redisplayAlarm(); show(); } /****************************************************************************** * Fetch the restored alarm from the calendar and redisplay it in this window. */ void MessageWin::redisplayAlarm() { mCollection = AkonadiModel::instance()->collectionForItem(mEventItemId); mEventId.setCollectionId(mCollection.id()); qCDebug(KALARM_LOG) << "MessageWin::redisplayAlarm:" << mEventId; // Delete any already existing window for the same event MessageWin* duplicate = findEvent(mEventId, this); if (duplicate) qCDebug(KALARM_LOG) << "MessageWin::redisplayAlarm: Deleting duplicate window:" << mEventId; delete duplicate; KAEvent* event = AlarmCalendar::resources()->event(mEventId); if (event) { mEvent = *event; mShowEdit = true; } else { // It's not in the active calendar, so try the displaying or archive calendars retrieveEvent(mEvent, mCollection, mShowEdit, mNoDefer); mNoDefer = !mNoDefer; } initView(); } /****************************************************************************** * Redisplay alarms which were being shown when the program last exited. * Normally, these alarms will have been displayed by session restoration, but * if the program crashed or was killed, we can redisplay them here so that * they won't be lost. */ void MessageWin::redisplayAlarms() { if (mRedisplayed) return; qCDebug(KALARM_LOG) << "MessageWin::redisplayAlarms"; mRedisplayed = true; AlarmCalendar* cal = AlarmCalendar::displayCalendar(); if (cal && cal->isOpen()) { KAEvent event; Akonadi::Collection collection; const Event::List events = cal->kcalEvents(); for (int i = 0, end = events.count(); i < end; ++i) { bool showDefer, showEdit; reinstateFromDisplaying(events[i], event, collection, showEdit, showDefer); Akonadi::Item::Id id = AkonadiModel::instance()->findItemId(event); if (id >= 0) event.setItemId(id); const EventId eventId(event); if (findEvent(eventId)) qCDebug(KALARM_LOG) << "MessageWin::redisplayAlarms: Message window already exists:" << eventId; else { // This event should be displayed, but currently isn't being const KAAlarm alarm = event.convertDisplayingAlarm(); if (alarm.type() == KAAlarm::INVALID_ALARM) { qCCritical(KALARM_LOG) << "MessageWin::redisplayAlarms: Invalid alarm: id=" << eventId; continue; } qCDebug(KALARM_LOG) << "MessageWin::redisplayAlarms:" << eventId; const bool login = alarm.repeatAtLogin(); const int flags = NO_RESCHEDULE | (login ? NO_DEFER : 0) | NO_INIT_VIEW; MessageWin* win = new MessageWin(&event, alarm, flags); win->mCollection = collection; const bool rw = CollectionControlModel::isWritableEnabled(collection, event.category()) > 0; win->mShowEdit = rw ? showEdit : false; win->mNoDefer = (rw && !login) ? !showDefer : true; win->initView(); win->show(); } } } } /****************************************************************************** * Retrieves the event with the current ID from the displaying calendar file, * or if not found there, from the archive calendar. */ bool MessageWin::retrieveEvent(KAEvent& event, Akonadi::Collection& resource, bool& showEdit, bool& showDefer) { const Event::Ptr kcalEvent = AlarmCalendar::displayCalendar()->kcalEvent(CalEvent::uid(mEventId.eventId(), CalEvent::DISPLAYING)); if (!reinstateFromDisplaying(kcalEvent, event, resource, showEdit, showDefer)) { // The event isn't in the displaying calendar. // Try to retrieve it from the archive calendar. KAEvent* ev = nullptr; Akonadi::Collection archiveCol = CollectionControlModel::getStandard(CalEvent::ARCHIVED); if (archiveCol.isValid()) ev = AlarmCalendar::resources()->event(EventId(archiveCol.id(), CalEvent::uid(mEventId.eventId(), CalEvent::ARCHIVED))); if (!ev) return false; event = *ev; event.setArchive(); // ensure that it gets re-archived if it's saved event.setCategory(CalEvent::ACTIVE); if (mEventId.eventId() != event.id()) qCCritical(KALARM_LOG) << "MessageWin::retrieveEvent: Wrong event ID"; event.setEventId(mEventId.eventId()); resource = Akonadi::Collection(); showEdit = true; showDefer = true; qCDebug(KALARM_LOG) << "MessageWin::retrieveEvent:" << event.id() << ": success"; } return true; } /****************************************************************************** * Retrieves the displayed event from the calendar file, or if not found there, * from the displaying calendar. */ bool MessageWin::reinstateFromDisplaying(const Event::Ptr& kcalEvent, KAEvent& event, Akonadi::Collection& collection, bool& showEdit, bool& showDefer) { if (!kcalEvent) return false; Akonadi::Collection::Id collectionId; event.reinstateFromDisplaying(kcalEvent, collectionId, showEdit, showDefer); event.setCollectionId(collectionId); collection = AkonadiModel::instance()->collectionById(collectionId); qCDebug(KALARM_LOG) << "MessageWin::reinstateFromDisplaying:" << EventId(event) << ": success"; return true; } /****************************************************************************** * Called when an alarm is currently being displayed, to store a copy of the * alarm in the displaying calendar, and to reschedule it for its next repetition. * If no repetitions remain, cancel it. */ void MessageWin::alarmShowing(KAEvent& event) { qCDebug(KALARM_LOG) << "MessageWin::alarmShowing:" << event.id() << "," << KAAlarm::debugType(mAlarmType); const KAAlarm alarm = event.alarm(mAlarmType); if (!alarm.isValid()) { qCCritical(KALARM_LOG) << "MessageWin::alarmShowing: Alarm type not found:" << event.id() << ":" << mAlarmType; return; } if (!mAlwaysHide) { // Copy the alarm to the displaying calendar in case of a crash, etc. KAEvent dispEvent; const Akonadi::Collection collection = AkonadiModel::instance()->collectionForItem(event.itemId()); dispEvent.setDisplaying(event, mAlarmType, collection.id(), mDateTime.effectiveKDateTime(), mShowEdit, !mNoDefer); AlarmCalendar* cal = AlarmCalendar::displayCalendarOpen(); if (cal) { cal->deleteDisplayEvent(dispEvent.id()); // in case it already exists cal->addEvent(dispEvent); cal->save(); } } theApp()->rescheduleAlarm(event, alarm); } /****************************************************************************** * Spread alarm windows over the screen so that they are all visible, or pile * them on top of each other again. * Reply = true if windows are now scattered, false if piled up. */ bool MessageWin::spread(bool scatter) { if (instanceCount(true) <= 1) // ignore always-hidden windows return false; const QRect desk = KAlarm::desktopWorkArea(); // get the usable area of the desktop if (scatter == isSpread(desk.topLeft())) return scatter; if (scatter) { // Usually there won't be many windows, so a crude // scattering algorithm should suffice. int x = desk.left(); int y = desk.top(); int ynext = y; for (int errmsgs = 0; errmsgs < 2; ++errmsgs) { // Display alarm messages first, then error messages, since most // error messages tend to be the same height. for (int i = 0, end = mWindowList.count(); i < end; ++i) { MessageWin* w = mWindowList[i]; if ((!errmsgs && w->mErrorWindow) || (errmsgs && !w->mErrorWindow)) continue; const QSize sz = w->frameGeometry().size(); if (x + sz.width() > desk.right()) { x = desk.left(); y = ynext; } int ytmp = y; if (y + sz.height() > desk.bottom()) { ytmp = desk.bottom() - sz.height(); if (ytmp < desk.top()) ytmp = desk.top(); } w->move(x, ytmp); x += sz.width(); if (ytmp + sz.height() > ynext) ynext = ytmp + sz.height(); } } } else { // Move all windows to the top left corner for (int i = 0, end = mWindowList.count(); i < end; ++i) mWindowList[i]->move(desk.topLeft()); } return scatter; } /****************************************************************************** * Check whether message windows are all piled up, or are spread out. * Reply = true if windows are currently spread, false if piled up. */ bool MessageWin::isSpread(const QPoint& topLeft) { for (int i = 0, end = mWindowList.count(); i < end; ++i) { if (mWindowList[i]->pos() != topLeft) return true; } return false; } /****************************************************************************** * Returns the existing message window (if any) which is displaying the event * with the specified ID. */ MessageWin* MessageWin::findEvent(const EventId& eventId, MessageWin* exclude) { if (!eventId.isEmpty()) { for (int i = 0, end = mWindowList.count(); i < end; ++i) { MessageWin* w = mWindowList[i]; if (w != exclude && w->mEventId == eventId && !w->mErrorWindow) return w; } } return nullptr; } /****************************************************************************** * Beep and play the audio file, as appropriate. */ void MessageWin::playAudio() { if (mBeep) { // Beep using two methods, in case the sound card/speakers are switched off or not working QApplication::beep(); // beep through the internal speaker KNotification::beep(); // beep through the sound card & speakers } if (!mAudioFile.isEmpty()) { if (!mVolume && mFadeVolume <= 0) return; // ensure zero volume doesn't play anything startAudio(); // play the audio file } else if (mSpeak) { // The message is to be spoken. In case of error messges, // call it on a timer to allow the window to display first. QTimer::singleShot(0, this, &MessageWin::slotSpeak); } } /****************************************************************************** * Speak the message. * Called asynchronously to avoid delaying the display of the message. */ void MessageWin::slotSpeak() { KPIMTextEdit::TextToSpeech *tts = KPIMTextEdit::TextToSpeech::self(); if (!tts->isReady()) { KAMessageBox::detailedError(MainWindow::mainMainWindow(), i18nc("@info", "Unable to speak message"), i18nc("@info", "Text-to-speech subsystem is not available")); clearErrorMessage(ErrMsg_Speak); return; } tts->say(mMessage); } /****************************************************************************** * Called when another window's audio thread has been destructed. * Start playing this window's audio file. Because initialising the sound system * and loading the file may take some time, it is called in a separate thread to * allow the window to display first. */ void MessageWin::startAudio() { if (mAudioThread) { // An audio file is already playing for another message // window, so wait until it has finished. connect(mAudioThread.data(), &QObject::destroyed, this, &MessageWin::audioTerminating); } else { qCDebug(KALARM_LOG) << "MessageWin::startAudio:" << QThread::currentThread(); mAudioThread = new AudioThread(this, mAudioFile, mVolume, mFadeVolume, mFadeSeconds, mAudioRepeatPause); connect(mAudioThread.data(), &AudioThread::readyToPlay, this, &MessageWin::playReady); connect(mAudioThread.data(), &QThread::finished, this, &MessageWin::playFinished); if (mSilenceButton) connect(mSilenceButton, &QAbstractButton::clicked, mAudioThread.data(), &QThread::quit); // Notify after creating mAudioThread, so that isAudioPlaying() will // return the correct value. theApp()->notifyAudioPlaying(true); mAudioThread->start(); } } /****************************************************************************** * Return whether audio playback is currently active. */ bool MessageWin::isAudioPlaying() { return mAudioThread; } /****************************************************************************** * Stop audio playback. */ void MessageWin::stopAudio(bool wait) { qCDebug(KALARM_LOG) << "MessageWin::stopAudio"; if (mAudioThread) mAudioThread->stop(wait); } /****************************************************************************** * Called when another window's audio thread is being destructed. * Wait until the destructor has finished. */ void MessageWin::audioTerminating() { QTimer::singleShot(0, this, &MessageWin::startAudio); } /****************************************************************************** * Called when the audio file is ready to start playing. */ void MessageWin::playReady() { if (mSilenceButton) mSilenceButton->setEnabled(true); } /****************************************************************************** * Called when the audio file thread finishes. */ void MessageWin::playFinished() { if (mSilenceButton) mSilenceButton->setEnabled(false); if (mAudioThread) // mAudioThread can actually be null here! { const QString errmsg = mAudioThread->error(); if (!errmsg.isEmpty() && !haveErrorMessage(ErrMsg_AudioFile)) { KAMessageBox::error(this, errmsg); clearErrorMessage(ErrMsg_AudioFile); } } delete mAudioThread.data(); if (mAlwaysHide) close(); } /****************************************************************************** * Constructor for audio thread. */ AudioThread::AudioThread(MessageWin* parent, const QString& audioFile, float volume, float fadeVolume, int fadeSeconds, int repeatPause) : QThread(parent), mFile(audioFile), mVolume(volume), mFadeVolume(fadeVolume), mFadeSeconds(fadeSeconds), mRepeatPause(repeatPause), mAudioObject(nullptr) { if (mAudioOwner) qCCritical(KALARM_LOG) << "MessageWin::AudioThread: mAudioOwner already set"; mAudioOwner = parent; } /****************************************************************************** * Destructor for audio thread. Waits for thread completion and tidies up. * Note that this destructor is executed in the parent thread. */ AudioThread::~AudioThread() { qCDebug(KALARM_LOG) << "~MessageWin::AudioThread"; stop(true); // stop playing and tidy up (timeout 3 seconds) delete mAudioObject; mAudioObject = nullptr; if (mAudioOwner == parent()) mAudioOwner = nullptr; // Notify after deleting mAudioThread, so that isAudioPlaying() will // return the correct value. QTimer::singleShot(0, theApp(), &KAlarmApp::notifyAudioStopped); } /****************************************************************************** * Quits the thread and waits for thread completion and tidies up. */ void AudioThread::stop(bool waiT) { qCDebug(KALARM_LOG) << "MessageWin::AudioThread::stop"; quit(); // stop playing and tidy up wait(3000); // wait for run() to exit (timeout 3 seconds) if (!isFinished()) { // Something has gone wrong - forcibly kill the thread terminate(); if (waiT) wait(); } } /****************************************************************************** * Kick off the thread to play the audio file. */ void AudioThread::run() { mMutex.lock(); if (mAudioObject) { mMutex.unlock(); return; } qCDebug(KALARM_LOG) << "MessageWin::AudioThread::run:" << QThread::currentThread() << mFile; const QString audioFile = mFile; const QUrl url = QUrl::fromUserInput(mFile); mFile = url.isLocalFile() ? url.toLocalFile() : url.toString(); Phonon::MediaSource source(url); if (source.type() == Phonon::MediaSource::Invalid) { mError = xi18nc("@info", "Cannot open audio file: %1", audioFile); mMutex.unlock(); qCCritical(KALARM_LOG) << "MessageWin::AudioThread::run: Open failure:" << audioFile; return; } mAudioObject = new Phonon::MediaObject(); mAudioObject->setCurrentSource(source); mAudioObject->setTransitionTime(100); // workaround to prevent clipping of end of files in Xine backend Phonon::AudioOutput* output = new Phonon::AudioOutput(Phonon::NotificationCategory, mAudioObject); mPath = Phonon::createPath(mAudioObject, output); if (mVolume >= 0 || mFadeVolume >= 0) { const float vol = (mVolume >= 0) ? mVolume : output->volume(); const float maxvol = qMax(vol, mFadeVolume); output->setVolume(maxvol); if (mFadeVolume >= 0 && mFadeSeconds > 0) { Phonon::VolumeFaderEffect* fader = new Phonon::VolumeFaderEffect(mAudioObject); fader->setVolume(mFadeVolume / maxvol); fader->fadeTo(mVolume / maxvol, mFadeSeconds * 1000); mPath.insertEffect(fader); } } connect(mAudioObject, &Phonon::MediaObject::stateChanged, this, &AudioThread::playStateChanged, Qt::DirectConnection); connect(mAudioObject, &Phonon::MediaObject::finished, this, &AudioThread::checkAudioPlay, Qt::DirectConnection); mPlayedOnce = false; mPausing = false; mMutex.unlock(); Q_EMIT readyToPlay(); checkAudioPlay(); // Start an event loop. // The function will exit once exit() or quit() is called. // First, ensure that the thread object is deleted once it has completed. connect(this, &QThread::finished, this, &QObject::deleteLater); exec(); stopPlay(); } /****************************************************************************** * Called when the audio file has loaded and is ready to play, or when play * has completed. * If it is ready to play, start playing it (for the first time or repeated). * If play has not yet completed, wait a bit longer. */ void AudioThread::checkAudioPlay() { mMutex.lock(); if (!mAudioObject) { mMutex.unlock(); return; } if (mPausing) mPausing = false; else { // The file has loaded and is ready to play, or play has completed if (mPlayedOnce) { if (mRepeatPause < 0) { // Play has completed mMutex.unlock(); stopPlay(); return; } if (mRepeatPause > 0) { // Pause before playing the file again mPausing = true; QTimer::singleShot(mRepeatPause * 1000, this, &AudioThread::checkAudioPlay); mMutex.unlock(); return; } } mPlayedOnce = true; } // Start playing the file, either for the first time or again qCDebug(KALARM_LOG) << "MessageWin::AudioThread::checkAudioPlay: start"; mAudioObject->play(); mMutex.unlock(); } /****************************************************************************** * Called when the playback object changes state. * If an error has occurred, quit and return the error to the caller. */ void AudioThread::playStateChanged(Phonon::State newState) { if (newState == Phonon::ErrorState) { QMutexLocker locker(&mMutex); const QString err = mAudioObject->errorString(); if (!err.isEmpty()) { qCCritical(KALARM_LOG) << "MessageWin::AudioThread::playStateChanged: Play failure:" << mFile << ":" << err; mError = xi18nc("@info", "Error playing audio file: %1%2", mFile, err); exit(1); } } } /****************************************************************************** * Called when play completes, the Silence button is clicked, or the window is * closed, to terminate audio access. */ void AudioThread::stopPlay() { mMutex.lock(); if (mAudioObject) { mAudioObject->stop(); const QList effects = mPath.effects(); for (int i = 0; i < effects.count(); ++i) { mPath.removeEffect(effects[i]); delete effects[i]; } delete mAudioObject; mAudioObject = nullptr; } mMutex.unlock(); quit(); // exit the event loop, if it's still running } QString AudioThread::error() const { QMutexLocker locker(&mMutex); return mError; } /****************************************************************************** * Raise the alarm window, re-output any required audio notification, and * reschedule the alarm in the calendar file. */ void MessageWin::repeat(const KAAlarm& alarm) { if (!mInitialised) return; if (mDeferDlg) { // Cancel any deferral dialog so that the user notices something's going on, // and also because the deferral time limit will have changed. delete mDeferDlg; mDeferDlg = nullptr; } KAEvent* event = mEventId.isEmpty() ? nullptr : AlarmCalendar::resources()->event(mEventId); if (event) { mAlarmType = alarm.type(); // store new alarm type for use if it is later deferred if (mAlwaysHide) playAudio(); else { if (!mDeferDlg || Preferences::modalMessages()) { raise(); playAudio(); } if (mDeferButton->isVisible()) { mDeferButton->setEnabled(true); setDeferralLimit(*event); // ensure that button is disabled when alarm can't be deferred any more } } alarmShowing(*event); } } /****************************************************************************** * Display the window. * If windows are being positioned away from the mouse cursor, it is initially * positioned at the top left to slightly reduce the number of times the * windows need to be moved in showEvent(). */ void MessageWin::show() { if (mCloseTime.isValid()) { // Set a timer to auto-close the window int delay = QDateTime::currentDateTimeUtc().secsTo(mCloseTime); if (delay < 0) delay = 0; QTimer::singleShot(delay * 1000, this, &QWidget::close); if (!delay) return; // don't show the window if auto-closing is already due } if (Preferences::messageButtonDelay() == 0) move(0, 0); MainWindowBase::show(); } /****************************************************************************** * Returns the window's recommended size exclusive of its frame. */ QSize MessageWin::sizeHint() const { QSize desired; switch (mAction) { case KAEvent::MESSAGE: desired = MainWindowBase::sizeHint(); break; case KAEvent::COMMAND: if (mShown) { // For command output, expand the window to accommodate the text const QSize texthint = mCommandText->sizeHint(); int w = texthint.width() + 2 * style()->pixelMetric(QStyle::PM_DefaultChildMargin); if (w < width()) w = width(); const int ypadding = height() - mCommandText->height(); desired = QSize(w, texthint.height() + ypadding); break; } // fall through to default Q_FALLTHROUGH(); default: return MainWindowBase::sizeHint(); } // Limit the size to fit inside the working area of the desktop const QSize desktop = KAlarm::desktopWorkArea(mScreenNumber).size(); const QSize frameThickness = frameGeometry().size() - geometry().size(); // title bar & window frame return desired.boundedTo(desktop - frameThickness); } /****************************************************************************** * Called when the window is shown. * The first time, output any required audio notification, and reschedule or * delete the event from the calendar file. */ void MessageWin::showEvent(QShowEvent* se) { MainWindowBase::showEvent(se); if (mShown || !mInitialised) return; if (mErrorWindow || mAlarmType == KAAlarm::INVALID_ALARM) { // Don't bother repositioning error messages, // and invalid alarms should be deleted anyway. enableButtons(); } else { /* Set the window size. * Note that the frame thickness is not yet known when this * method is called, so for large windows the size needs to be * set again later. */ bool execComplete = true; QSize s = sizeHint(); // fit the window round the message if (mAction == KAEvent::FILE && !mErrorMsgs.count()) KAlarm::readConfigWindowSize("FileMessage", s); resize(s); const QRect desk = KAlarm::desktopWorkArea(mScreenNumber); const QRect frame = frameGeometry(); mButtonDelay = Preferences::messageButtonDelay() * 1000; if (mButtonDelay) { // Position the window in the middle of the screen, and // delay enabling the buttons. mPositioning = true; move((desk.width() - frame.width())/2, (desk.height() - frame.height())/2); execComplete = false; } else { /* Try to ensure that the window can't accidentally be acknowledged * by the user clicking the mouse just as it appears. * To achieve this, move the window so that the OK button is as far away * from the cursor as possible. If the buttons are still too close to the * cursor, disable the buttons for a short time. * N.B. This can't be done in show(), since the geometry of the window * is not known until it is displayed. Unfortunately by moving the * window in showEvent(), a flicker is unavoidable. * See the Qt documentation on window geometry for more details. */ // PROBLEM: The frame size is not known yet! const QPoint cursor = QCursor::pos(); const QRect rect = geometry(); // Find the offsets from the outside of the frame to the edges of the OK button const QRect button(mOkButton->mapToParent(QPoint(0, 0)), mOkButton->mapToParent(mOkButton->rect().bottomRight())); const int buttonLeft = button.left() + rect.left() - frame.left(); const int buttonRight = width() - button.right() + frame.right() - rect.right(); const int buttonTop = button.top() + rect.top() - frame.top(); const int buttonBottom = height() - button.bottom() + frame.bottom() - rect.bottom(); const int centrex = (desk.width() + buttonLeft - buttonRight) / 2; const int centrey = (desk.height() + buttonTop - buttonBottom) / 2; const int x = (cursor.x() < centrex) ? desk.right() - frame.width() : desk.left(); const int y = (cursor.y() < centrey) ? desk.bottom() - frame.height() : desk.top(); // Find the enclosing rectangle for the new button positions // and check if the cursor is too near QRect buttons = mOkButton->geometry().united(mKAlarmButton->geometry()); buttons.translate(rect.left() + x - frame.left(), rect.top() + y - frame.top()); const int minDistance = proximityMultiple * mOkButton->height(); if ((abs(cursor.x() - buttons.left()) < minDistance || abs(cursor.x() - buttons.right()) < minDistance) && (abs(cursor.y() - buttons.top()) < minDistance || abs(cursor.y() - buttons.bottom()) < minDistance)) mButtonDelay = proximityButtonDelay; // too near - disable buttons initially if (x != frame.left() || y != frame.top()) { mPositioning = true; move(x, y); execComplete = false; } } if (execComplete) displayComplete(); // play audio, etc. } // Set the window size etc. once the frame size is known QTimer::singleShot(0, this, &MessageWin::frameDrawn); mShown = true; } /****************************************************************************** * Called when the window has been moved. */ void MessageWin::moveEvent(QMoveEvent* e) { MainWindowBase::moveEvent(e); theApp()->setSpreadWindowsState(isSpread(KAlarm::desktopWorkArea(mScreenNumber).topLeft())); if (mPositioning) { // The window has just been initially positioned mPositioning = false; displayComplete(); // play audio, etc. } } /****************************************************************************** * Called after (hopefully) the window frame size is known. * Reset the initial window size if it exceeds the working area of the desktop. * Set the 'spread windows' menu item status. */ void MessageWin::frameDrawn() { if (!mErrorWindow && mAction == KAEvent::MESSAGE) { const QSize s = sizeHint(); if (width() > s.width() || height() > s.height()) resize(s); } theApp()->setSpreadWindowsState(isSpread(KAlarm::desktopWorkArea(mScreenNumber).topLeft())); } /****************************************************************************** * Called when the window has been displayed properly (in its correct position), * to play sounds and reschedule the event. */ void MessageWin::displayComplete() { playAudio(); if (mRescheduleEvent) alarmShowing(mEvent); if (!mAlwaysHide) { // Enable the window's buttons either now or after the configured delay if (mButtonDelay > 0) QTimer::singleShot(mButtonDelay, this, &MessageWin::enableButtons); else enableButtons(); } } /****************************************************************************** * Enable the window's buttons. */ void MessageWin::enableButtons() { mOkButton->setEnabled(true); mKAlarmButton->setEnabled(true); if (mDeferButton->isVisible() && !mDisableDeferral) mDeferButton->setEnabled(true); if (mEditButton) mEditButton->setEnabled(true); if (mKMailButton) mKMailButton->setEnabled(true); } /****************************************************************************** * Called when the window's size has changed (before it is painted). */ void MessageWin::resizeEvent(QResizeEvent* re) { if (mRestoreHeight) { // Restore the window height on session restoration if (mRestoreHeight != re->size().height()) { QSize size = re->size(); size.setHeight(mRestoreHeight); resize(size); } else if (isVisible()) mRestoreHeight = 0; } else { if (mShown && mAction == KAEvent::FILE && !mErrorMsgs.count()) KAlarm::writeConfigWindowSize("FileMessage", re->size()); MainWindowBase::resizeEvent(re); } } /****************************************************************************** * Called when a close event is received. * Only quits the application if there is no system tray icon displayed. */ void MessageWin::closeEvent(QCloseEvent* ce) { // Don't prompt or delete the alarm from the display calendar if the session is closing if (!mErrorWindow && !qApp->isSavingSession()) { if (mConfirmAck && !mNoCloseConfirm) { // Ask for confirmation of acknowledgement. Use warningYesNo() because its default is No. if (KAMessageBox::warningYesNo(this, i18nc("@info", "Do you really want to acknowledge this alarm?"), i18nc("@action:button", "Acknowledge Alarm"), KGuiItem(i18nc("@action:button", "Acknowledge")), KStandardGuiItem::cancel()) != KMessageBox::Yes) { ce->ignore(); return; } } if (!mEventId.isEmpty()) { // Delete from the display calendar KAlarm::deleteDisplayEvent(CalEvent::uid(mEventId.eventId(), CalEvent::DISPLAYING)); } } MainWindowBase::closeEvent(ce); } /****************************************************************************** * Called when the OK button is clicked. */ void MessageWin::slotOk() { if (mDontShowAgainCheck && mDontShowAgainCheck->isChecked()) KAlarm::setDontShowErrors(mEventId, mDontShowAgain); close(); } /****************************************************************************** * Called when the KMail button is clicked. * Tells KMail to display the email message displayed in this message window. */ void MessageWin::slotShowKMailMessage() { qCDebug(KALARM_LOG) << "MessageWin::slotShowKMailMessage"; if (!mKMailSerialNumber) return; const QString err = KAlarm::runKMail(false); if (!err.isNull()) { KAMessageBox::sorry(this, err); return; } org::kde::kmail::kmail kmail(KMAIL_DBUS_SERVICE, KMAIL_DBUS_PATH, QDBusConnection::sessionBus()); QDBusReply reply = kmail.showMail((qint64)mKMailSerialNumber); if (!reply.isValid()) qCCritical(KALARM_LOG) << "kmail D-Bus call failed:" << reply.error().message(); else if (!reply.value()) KAMessageBox::sorry(this, xi18nc("@info", "Unable to locate this email in KMail")); } /****************************************************************************** * Called when the Edit... button is clicked. * Displays the alarm edit dialog. * * NOTE: The alarm edit dialog is made a child of the main window, not this * window, so that if this window closes before the dialog (e.g. on * auto-close), KAlarm doesn't crash. The dialog is set non-modal so that * the main window is unaffected, but modal mode is simulated so that * this window is inactive while the dialog is open. */ void MessageWin::slotEdit() { qCDebug(KALARM_LOG) << "MessageWin::slotEdit"; MainWindow* mainWin = MainWindow::mainMainWindow(); mEditDlg = EditAlarmDlg::create(false, &mOriginalEvent, false, mainWin, EditAlarmDlg::RES_IGNORE); KWindowSystem::setMainWindow(mEditDlg, winId()); KWindowSystem::setOnAllDesktops(mEditDlg->winId(), false); setButtonsReadOnly(true); connect(mEditDlg, &QDialog::accepted, this, &MessageWin::editCloseOk); connect(mEditDlg, &QDialog::rejected, this, &MessageWin::editCloseCancel); connect(mEditDlg, &QObject::destroyed, this, &MessageWin::editCloseCancel); connect(KWindowSystem::self(), &KWindowSystem::activeWindowChanged, this, &MessageWin::activeWindowChanged); mainWin->editAlarm(mEditDlg, mOriginalEvent); } /****************************************************************************** * Called when OK is clicked in the alarm edit dialog invoked by the Edit button. * Closes the window. */ void MessageWin::editCloseOk() { mEditDlg = nullptr; mNoCloseConfirm = true; // allow window to close without confirmation prompt close(); } /****************************************************************************** * Called when Cancel is clicked in the alarm edit dialog invoked by the Edit * button, or when the dialog is deleted. */ void MessageWin::editCloseCancel() { mEditDlg = nullptr; setButtonsReadOnly(false); } /****************************************************************************** * Called when the active window has changed. If this window has become the * active window and there is an alarm edit dialog, simulate a modal dialog by * making the alarm edit dialog the active window instead. */ void MessageWin::activeWindowChanged(WId win) { if (mEditDlg && win == winId()) KWindowSystem::activateWindow(mEditDlg->winId()); } /****************************************************************************** * Set or clear the read-only state of the dialog buttons. */ void MessageWin::setButtonsReadOnly(bool ro) { mOkButton->setReadOnly(ro, true); mDeferButton->setReadOnly(ro, true); mEditButton->setReadOnly(ro, true); if (mSilenceButton) mSilenceButton->setReadOnly(ro, true); if (mKMailButton) mKMailButton->setReadOnly(ro, true); mKAlarmButton->setReadOnly(ro, true); } /****************************************************************************** * Set up to disable the defer button when the deferral limit is reached. */ void MessageWin::setDeferralLimit(const KAEvent& event) { mDeferLimit = event.deferralLimit().effectiveKDateTime().toUtc().qDateTime(); MidnightTimer::connect(this, SLOT(checkDeferralLimit())); // check every day mDisableDeferral = false; checkDeferralLimit(); } /****************************************************************************** * Check whether the deferral limit has been reached. * If so, disable the Defer button. * N.B. Ideally, just a single QTimer::singleShot() call would be made to disable * the defer button at the corret time. But for a 32-bit integer, the * milliseconds parameter overflows in about 25 days, so instead a daily * check is done until the day when the deferral limit is reached, followed * by a non-overflowing QTimer::singleShot() call. */ void MessageWin::checkDeferralLimit() { if (!mDeferButton->isEnabled() || !mDeferLimit.isValid()) return; int n = KADateTime::currentLocalDate().daysTo(KADateTime(mDeferLimit, KADateTime::LocalZone).date()); if (n > 0) return; MidnightTimer::disconnect(this, SLOT(checkDeferralLimit())); if (n == 0) { // The deferral limit will be reached today n = QDateTime::currentDateTimeUtc().secsTo(mDeferLimit); if (n > 0) { QTimer::singleShot(n * 1000, this, &MessageWin::checkDeferralLimit); return; } } mDeferButton->setEnabled(false); mDisableDeferral = true; } /****************************************************************************** * Called when the Defer... button is clicked. * Displays the defer message dialog. */ void MessageWin::slotDefer() { mDeferDlg = new DeferAlarmDlg(KADateTime::currentDateTime(Preferences::timeSpec()).addSecs(60), mDateTime.isDateOnly(), false, this); mDeferDlg->setObjectName(QStringLiteral("DeferDlg")); // used by LikeBack mDeferDlg->setDeferMinutes(mDefaultDeferMinutes > 0 ? mDefaultDeferMinutes : Preferences::defaultDeferTime()); mDeferDlg->setLimit(mEvent); if (!Preferences::modalMessages()) lower(); if (mDeferDlg->exec() == QDialog::Accepted) { const DateTime dateTime = mDeferDlg->getDateTime(); const int delayMins = mDeferDlg->deferMinutes(); // Fetch the up-to-date alarm from the calendar. Note that it could have // changed since it was displayed. const KAEvent* event = mEventId.isEmpty() ? nullptr : AlarmCalendar::resources()->event(mEventId); if (event) { // The event still exists in the active calendar qCDebug(KALARM_LOG) << "MessageWin::slotDefer: Deferring event" << mEventId; KAEvent newev(*event); newev.defer(dateTime, (mAlarmType & KAAlarm::REMINDER_ALARM), true); newev.setDeferDefaultMinutes(delayMins); KAlarm::updateEvent(newev, mDeferDlg, true); if (newev.deferred()) mNoPostAction = true; } else { // Try to retrieve the event from the displaying or archive calendars Akonadi::Collection collection; KAEvent event; bool showEdit, showDefer; if (!retrieveEvent(event, collection, showEdit, showDefer)) { // The event doesn't exist any more !?!, so recurrence data, // flags, and more, have been lost. KAMessageBox::error(this, xi18nc("@info", "Cannot defer alarm:Alarm not found.")); raise(); delete mDeferDlg; mDeferDlg = nullptr; mDeferButton->setEnabled(false); mEditButton->setEnabled(false); return; } qCDebug(KALARM_LOG) << "MessageWin::slotDefer: Deferring retrieved event" << mEventId; event.defer(dateTime, (mAlarmType & KAAlarm::REMINDER_ALARM), true); event.setDeferDefaultMinutes(delayMins); event.setCommandError(mCommandError); // Add the event back into the calendar file, retaining its ID // and not updating KOrganizer. KAlarm::addEvent(event, &collection, mDeferDlg, KAlarm::USE_EVENT_ID); if (event.deferred()) mNoPostAction = true; // Finally delete it from the archived calendar now that it has // been reactivated. event.setCategory(CalEvent::ARCHIVED); KAlarm::deleteEvent(event, false); } if (theApp()->wantShowInSystemTray()) { // Alarms are to be displayed only if the system tray icon is running, // so start it if necessary so that the deferred alarm will be shown. theApp()->displayTrayIcon(true); } mNoCloseConfirm = true; // allow window to close without confirmation prompt close(); } else raise(); delete mDeferDlg; mDeferDlg = nullptr; } /****************************************************************************** * Called when the KAlarm icon button in the message window is clicked. * Displays the main window, with the appropriate alarm selected. */ void MessageWin::displayMainWindow() { KAlarm::displayMainWindowSelected(mEventItemId); } /****************************************************************************** * Check whether the specified error message is already displayed for this * alarm, and note that it will now be displayed. * Reply = true if message is already displayed. */ bool MessageWin::haveErrorMessage(unsigned msg) const { if (!mErrorMessages.contains(mEventId)) mErrorMessages.insert(mEventId, 0); const bool result = (mErrorMessages[mEventId] & msg); mErrorMessages[mEventId] |= msg; return result; } void MessageWin::clearErrorMessage(unsigned msg) const { if (mErrorMessages.contains(mEventId)) { if (mErrorMessages[mEventId] == msg) mErrorMessages.remove(mEventId); else mErrorMessages[mEventId] &= ~msg; } } /****************************************************************************** * Check whether the message window should be modal, i.e. with title bar etc. * Normally this follows the Preferences setting, but if there is a full screen * window displayed, on X11 the message window has to bypass the window manager * in order to display on top of it (which has the side effect that it will have * no window decoration). * * Also find the usable area of the desktop (excluding panel etc.), on the * appropriate screen if there are multiple screens. */ bool MessageWin::getWorkAreaAndModal() { mScreenNumber = -1; const bool modal = Preferences::modalMessages(); #if KDEPIM_HAVE_X11 - const QDesktopWidget* desktop = qApp->desktop(); - const int numScreens = desktop->numScreens(); + const QList screens = QGuiApplication::screens(); + const int numScreens = screens.count(); if (numScreens > 1) { // There are multiple screens. // Check for any full screen windows, even if they are not the active // window, and try not to show the alarm message their screens. - mScreenNumber = desktop->screenNumber(MainWindow::mainMainWindow()); // default = KAlarm's screen - if (desktop->isVirtualDesktop()) + mScreenNumber = QApplication::desktop()->screenNumber(MainWindow::mainMainWindow()); // default = KAlarm's screen + if (QGuiApplication::primaryScreen()->virtualSiblings().size() > 1) { // The screens form a single virtual desktop. // Xinerama, for example, uses this scheme. QVector screenTypes(numScreens); QVector screenRects(numScreens); for (int s = 0; s < numScreens; ++s) - screenRects[s] = desktop->screenGeometry(s); + screenRects[s] = screens[s]->geometry(); const FullScreenType full = findFullScreenWindows(screenRects, screenTypes); if (full == NoFullScreen || screenTypes[mScreenNumber] == NoFullScreen) return modal; for (int s = 0; s < numScreens; ++s) { if (screenTypes[s] == NoFullScreen) { // There is no full screen window on this screen mScreenNumber = s; return modal; } } // All screens contain a full screen window: use one without // an active full screen window. for (int s = 0; s < numScreens; ++s) { if (screenTypes[s] == FullScreen) { mScreenNumber = s; return modal; } } } else { // The screens are completely separate from each other. int inactiveScreen = -1; FullScreenType full = haveFullScreenWindow(mScreenNumber); -qCDebug(KALARM_LOG)<<"full="<