diff --git a/src/akonadimodel.cpp b/src/akonadimodel.cpp index e1eea34f..a40deffd 100644 --- a/src/akonadimodel.cpp +++ b/src/akonadimodel.cpp @@ -1,1983 +1,1944 @@ /* * akonadimodel.cpp - KAlarm calendar file access using Akonadi * Program: kalarm * Copyright © 2007-2019 David Jarvie * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "akonadimodel.h" #include "alarmtime.h" #include "autoqpointer.h" #include "calendarmigrator.h" #include "mainwindow.h" #include "messagebox.h" #include "preferences.h" #include "synchtimer.h" #include "kalarmsettings.h" #include "kalarmdirsettings.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "kalarm_debug.h" using namespace Akonadi; using namespace KAlarmCal; static const Collection::Rights writableRights = Collection::CanChangeItem | Collection::CanCreateItem | Collection::CanDeleteItem; //static bool checkItem_true(const Item&) { return true; } /*============================================================================= = Class: AkonadiModel =============================================================================*/ AkonadiModel* AkonadiModel::mInstance = nullptr; QPixmap* AkonadiModel::mTextIcon = nullptr; QPixmap* AkonadiModel::mFileIcon = nullptr; QPixmap* AkonadiModel::mCommandIcon = nullptr; QPixmap* AkonadiModel::mEmailIcon = nullptr; QPixmap* AkonadiModel::mAudioIcon = nullptr; QSize AkonadiModel::mIconSize; int AkonadiModel::mTimeHourPos = -2; /****************************************************************************** * Construct and return the singleton. */ AkonadiModel* AkonadiModel::instance() { if (!mInstance) mInstance = new AkonadiModel(new ChangeRecorder(qApp), qApp); return mInstance; } /****************************************************************************** * Constructor. */ AkonadiModel::AkonadiModel(ChangeRecorder* monitor, QObject* parent) - : EntityTreeModel(monitor, parent), - mMonitor(monitor), - mResourcesChecked(false), - mMigrating(false) + : EntityTreeModel(monitor, parent) + , mMonitor(monitor) + , mResourcesChecked(false) + , mMigrating(false) { // Set lazy population to enable the contents of unselected collections to be ignored setItemPopulationStrategy(LazyPopulation); // Restrict monitoring to collections containing the KAlarm mime types monitor->setCollectionMonitored(Collection::root()); monitor->setResourceMonitored("akonadi_kalarm_resource"); monitor->setResourceMonitored("akonadi_kalarm_dir_resource"); monitor->setMimeTypeMonitored(KAlarmCal::MIME_ACTIVE); monitor->setMimeTypeMonitored(KAlarmCal::MIME_ARCHIVED); monitor->setMimeTypeMonitored(KAlarmCal::MIME_TEMPLATE); monitor->itemFetchScope().fetchFullPayload(); monitor->itemFetchScope().fetchAttribute(); AttributeFactory::registerAttribute(); AttributeFactory::registerAttribute(); AttributeFactory::registerAttribute(); if (!mTextIcon) { mTextIcon = new QPixmap(QIcon::fromTheme(QStringLiteral("dialog-information")).pixmap(16, 16)); mFileIcon = new QPixmap(QIcon::fromTheme(QStringLiteral("document-open")).pixmap(16, 16)); mCommandIcon = new QPixmap(QIcon::fromTheme(QStringLiteral("system-run")).pixmap(16, 16)); mEmailIcon = new QPixmap(QIcon::fromTheme(QStringLiteral("mail-unread")).pixmap(16, 16)); mAudioIcon = new QPixmap(QIcon::fromTheme(QStringLiteral("audio-x-generic")).pixmap(16, 16)); mIconSize = mTextIcon->size().expandedTo(mFileIcon->size()).expandedTo(mCommandIcon->size()).expandedTo(mEmailIcon->size()).expandedTo(mAudioIcon->size()); } -#ifdef __GNUC__ -#warning Only want to monitor collection properties, not content, when this becomes possible -#endif +#pragma message("Only want to monitor collection properties, not content, when this becomes possible") connect(monitor, SIGNAL(collectionChanged(Akonadi::Collection,QSet)), SLOT(slotCollectionChanged(Akonadi::Collection,QSet))); connect(monitor, &Monitor::collectionRemoved, this, &AkonadiModel::slotCollectionRemoved); initCalendarMigrator(); MinuteTimer::connect(this, SLOT(slotUpdateTimeTo())); Preferences::connect(SIGNAL(archivedColourChanged(QColor)), this, SLOT(slotUpdateArchivedColour(QColor))); Preferences::connect(SIGNAL(disabledColourChanged(QColor)), this, SLOT(slotUpdateDisabledColour(QColor))); Preferences::connect(SIGNAL(holidaysChanged(KHolidays::HolidayRegion)), this, SLOT(slotUpdateHolidays())); Preferences::connect(SIGNAL(workTimeChanged(QTime,QTime,QBitArray)), this, SLOT(slotUpdateWorkingHours())); connect(this, &AkonadiModel::rowsInserted, this, &AkonadiModel::slotRowsInserted); connect(this, &AkonadiModel::rowsAboutToBeRemoved, this, &AkonadiModel::slotRowsAboutToBeRemoved); connect(monitor, &Monitor::itemChanged, this, &AkonadiModel::slotMonitoredItemChanged); connect(ServerManager::self(), &ServerManager::stateChanged, this, &AkonadiModel::checkResources); checkResources(ServerManager::state()); } AkonadiModel::~AkonadiModel() { if (mInstance == this) mInstance = nullptr; } /****************************************************************************** * Called when the server manager changes state. * If it is now running, i.e. the agent manager knows about * all existing resources. * Once it is running, i.e. the agent manager knows about * all existing resources, if necessary migrate any KResources alarm calendars from * pre-Akonadi versions of KAlarm, or create default Akonadi calendar resources * if any are missing. */ void AkonadiModel::checkResources(ServerManager::State state) { switch (state) { case ServerManager::Running: if (!mResourcesChecked) { qCDebug(KALARM_LOG) << "AkonadiModel::checkResources: Server running"; mResourcesChecked = true; mMigrating = true; CalendarMigrator::execute(); } break; case ServerManager::NotRunning: qCDebug(KALARM_LOG) << "AkonadiModel::checkResources: Server stopped"; mResourcesChecked = false; mMigrating = false; mCollectionAlarmTypes.clear(); mCollectionRights.clear(); mCollectionEnabled.clear(); initCalendarMigrator(); Q_EMIT serverStopped(); break; default: break; } } /****************************************************************************** * Initialise the calendar migrator so that it can be run (either for the first * time, or again). */ void AkonadiModel::initCalendarMigrator() { CalendarMigrator::reset(); connect(CalendarMigrator::instance(), &CalendarMigrator::creating, this, &AkonadiModel::slotCollectionBeingCreated); connect(CalendarMigrator::instance(), &QObject::destroyed, this, &AkonadiModel::slotMigrationCompleted); } /****************************************************************************** * Return whether calendar migration has completed. */ bool AkonadiModel::isMigrationCompleted() const { return mResourcesChecked && !mMigrating; } /****************************************************************************** * Return the data for a given role, for a specified item. */ QVariant AkonadiModel::data(const QModelIndex& index, int role) const { // First check that it's a role we're interested in - if not, use the base method switch (role) { case Qt::BackgroundRole: case Qt::ForegroundRole: case Qt::DisplayRole: case Qt::TextAlignmentRole: case Qt::DecorationRole: case Qt::SizeHintRole: case Qt::AccessibleTextRole: case Qt::ToolTipRole: case Qt::CheckStateRole: case SortRole: case TimeDisplayRole: case ValueRole: case StatusRole: case AlarmActionsRole: case AlarmSubActionRole: case EnabledRole: case EnabledTypesRole: case CommandErrorRole: case BaseColourRole: case AlarmTypeRole: case IsStandardRole: break; default: return EntityTreeModel::data(index, role); } const Collection collection = index.data(CollectionRole).value(); if (collection.isValid()) { // This is a Collection row switch (role) { case Qt::DisplayRole: return collection.displayName(); case EnabledTypesRole: if (mCollectionAttributes.contains(collection.id())) return static_cast(mCollectionAttributes.value(collection.id()).enabled()); if (collection.hasAttribute()) return static_cast(collection.attribute()->enabled()); return 0; case BaseColourRole: role = Qt::BackgroundRole; break; case Qt::BackgroundRole: { const QColor colour = backgroundColor_p(collection); if (colour.isValid()) return colour; break; } case Qt::ForegroundRole: return foregroundColor(collection, collection.contentMimeTypes()); case Qt::ToolTipRole: return tooltip(collection, CalEvent::ACTIVE | CalEvent::ARCHIVED | CalEvent::TEMPLATE); case AlarmTypeRole: return static_cast(types(collection)); case IsStandardRole: if (isCompatible(collection)) { if (mCollectionAttributes.contains(collection.id())) return static_cast(mCollectionAttributes.value(collection.id()).standard()); if (collection.hasAttribute()) return static_cast(collection.attribute()->standard()); } return 0; case KeepFormatRole: if (mCollectionAttributes.contains(collection.id())) return mCollectionAttributes.value(collection.id()).keepFormat(); if (collection.hasAttribute()) return collection.attribute()->keepFormat(); return false; default: break; } } else { const Item item = index.data(ItemRole).value(); if (item.isValid()) { // This is an Item row const QString mime = item.mimeType(); if ((mime != KAlarmCal::MIME_ACTIVE && mime != KAlarmCal::MIME_ARCHIVED && mime != KAlarmCal::MIME_TEMPLATE) || !item.hasPayload()) return QVariant(); switch (role) { case StatusRole: // Mime type has a one-to-one relationship to event's category() if (mime == KAlarmCal::MIME_ACTIVE) return CalEvent::ACTIVE; if (mime == KAlarmCal::MIME_ARCHIVED) return CalEvent::ARCHIVED; if (mime == KAlarmCal::MIME_TEMPLATE) return CalEvent::TEMPLATE; return QVariant(); case CommandErrorRole: if (!item.hasAttribute()) return KAEvent::CMD_NO_ERROR; return item.attribute()->commandError(); default: break; } const int column = index.column(); if (role == Qt::WhatsThisRole) return whatsThisText(column); const KAEvent event(this->event(item)); if (!event.isValid()) return QVariant(); if (role == AlarmActionsRole) return event.actionTypes(); if (role == AlarmSubActionRole) return event.actionSubType(); bool calendarColour = false; switch (column) { case TimeColumn: switch (role) { case Qt::BackgroundRole: calendarColour = true; break; case Qt::DisplayRole: if (event.expired()) return AlarmTime::alarmTimeText(event.startDateTime(), '0'); return AlarmTime::alarmTimeText(event.nextTrigger(KAEvent::DISPLAY_TRIGGER), '0'); case TimeDisplayRole: if (event.expired()) return AlarmTime::alarmTimeText(event.startDateTime(), '~'); return AlarmTime::alarmTimeText(event.nextTrigger(KAEvent::DISPLAY_TRIGGER), '~'); case Qt::TextAlignmentRole: return Qt::AlignRight; case SortRole: { DateTime due; if (event.expired()) due = event.startDateTime(); else due = event.nextTrigger(KAEvent::DISPLAY_TRIGGER); return due.isValid() ? due.effectiveKDateTime().toUtc().qDateTime() : QDateTime(QDate(9999,12,31), QTime(0,0,0)); } default: break; } break; case TimeToColumn: switch (role) { case Qt::BackgroundRole: calendarColour = true; break; case Qt::DisplayRole: if (event.expired()) return QString(); return AlarmTime::timeToAlarmText(event.nextTrigger(KAEvent::DISPLAY_TRIGGER)); case Qt::TextAlignmentRole: return Qt::AlignRight; case SortRole: { if (event.expired()) return -1; const DateTime due = event.nextTrigger(KAEvent::DISPLAY_TRIGGER); const KADateTime now = KADateTime::currentUtcDateTime(); if (due.isDateOnly()) return now.date().daysTo(due.date()) * 1440; return (now.secsTo(due.effectiveKDateTime()) + 59) / 60; } } break; case RepeatColumn: switch (role) { case Qt::BackgroundRole: calendarColour = true; break; case Qt::DisplayRole: return repeatText(event); case Qt::TextAlignmentRole: return Qt::AlignHCenter; case SortRole: return repeatOrder(event); } break; case ColourColumn: switch (role) { case Qt::BackgroundRole: { const KAEvent::Actions type = event.actionTypes(); if (type & KAEvent::ACT_DISPLAY) return event.bgColour(); if (type == KAEvent::ACT_COMMAND) { if (event.commandError() != KAEvent::CMD_NO_ERROR) return QColor(Qt::red); } break; } case Qt::ForegroundRole: if (event.commandError() != KAEvent::CMD_NO_ERROR) { if (event.actionTypes() == KAEvent::ACT_COMMAND) return QColor(Qt::white); QColor colour = Qt::red; int r, g, b; event.bgColour().getRgb(&r, &g, &b); if (r > 128 && g <= 128 && b <= 128) colour = QColor(Qt::white); return colour; } break; case Qt::DisplayRole: if (event.commandError() != KAEvent::CMD_NO_ERROR) return QLatin1String("!"); break; case SortRole: { const unsigned i = (event.actionTypes() == KAEvent::ACT_DISPLAY) ? event.bgColour().rgb() : 0; return QStringLiteral("%1").arg(i, 6, 10, QLatin1Char('0')); } default: break; } break; case TypeColumn: switch (role) { case Qt::BackgroundRole: calendarColour = true; break; case Qt::DecorationRole: { QVariant v; v.setValue(*eventIcon(event)); return v; } case Qt::TextAlignmentRole: return Qt::AlignHCenter; case Qt::SizeHintRole: return mIconSize; case Qt::AccessibleTextRole: -#ifdef __GNUC__ -#warning Implement accessibility -#endif +#pragma message("Implement accessibility") return QString(); case ValueRole: return static_cast(event.actionSubType()); case SortRole: return QStringLiteral("%1").arg(event.actionSubType(), 2, 10, QLatin1Char('0')); } break; case TextColumn: switch (role) { case Qt::BackgroundRole: calendarColour = true; break; case Qt::DisplayRole: case SortRole: return AlarmText::summary(event, 1); case Qt::ToolTipRole: return AlarmText::summary(event, 10); default: break; } break; case TemplateNameColumn: switch (role) { case Qt::BackgroundRole: calendarColour = true; break; case Qt::DisplayRole: return event.templateName(); case SortRole: return event.templateName().toUpper(); } break; default: break; } switch (role) { case Qt::ForegroundRole: if (!event.enabled()) return Preferences::disabledColour(); if (event.expired()) return Preferences::archivedColour(); break; // use the default for normal active alarms case Qt::ToolTipRole: // Show the last command execution error message switch (event.commandError()) { case KAEvent::CMD_ERROR: return i18nc("@info:tooltip", "Command execution failed"); case KAEvent::CMD_ERROR_PRE: return i18nc("@info:tooltip", "Pre-alarm action execution failed"); case KAEvent::CMD_ERROR_POST: return i18nc("@info:tooltip", "Post-alarm action execution failed"); case KAEvent::CMD_ERROR_PRE_POST: return i18nc("@info:tooltip", "Pre- and post-alarm action execution failed"); default: case KAEvent::CMD_NO_ERROR: break; } break; case EnabledRole: return event.enabled(); default: break; } if (calendarColour) { Collection parent = item.parentCollection(); const QColor colour = backgroundColor(parent); if (colour.isValid()) return colour; } } } return EntityTreeModel::data(index, role); } /****************************************************************************** * Set the font to use for all items, or the checked state of one item. * The font must always be set at initialisation. */ bool AkonadiModel::setData(const QModelIndex& index, const QVariant& value, int role) { if (!index.isValid()) return false; // NOTE: need to Q_EMIT dataChanged() whenever something is updated (except via a job). - Collection collection = index.data(CollectionRole).value(); + const Collection collection = index.data(CollectionRole).value(); if (collection.isValid()) { // This is a Collection row bool updateCollectionAttr = false; CollectionAttribute attr; switch (role) { case Qt::BackgroundRole: { const QColor colour = value.value(); attr = mCollectionAttributes.value(collection.id()); if (attr.backgroundColor() == colour) return true; // no change attr.setBackgroundColor(colour); updateCollectionAttr = true; break; } case EnabledTypesRole: { const CalEvent::Types types = static_cast(value.toInt()); - bool newAttr = !collection.hasAttribute(); + const bool newAttr = !collection.hasAttribute(); attr = mCollectionAttributes.value(collection.id()); if (attr.enabled() == types) return true; // no change qCDebug(KALARM_LOG) << "AkonadiModel:" << collection.id() << "Set enabled:" << types << " was=" << attr.enabled(); attr.setEnabled(types); updateCollectionAttr = true; if (newAttr) { // Akonadi often doesn't notify changes to the enabled status // (surely a bug?), so ensure that the change is noticed. mNewCollectionEnabled[collection.id()] = types; } break; } case IsStandardRole: if (collection.hasAttribute() && isCompatible(collection)) { const CalEvent::Types types = static_cast(value.toInt()); attr = mCollectionAttributes.value(collection.id()); qCDebug(KALARM_LOG) << "AkonadiModel:" << collection.id() << "Set standard:" << types << " was=" << attr.standard(); attr.setStandard(types); updateCollectionAttr = true; } break; case KeepFormatRole: { const bool keepFormat = value.toBool(); attr = mCollectionAttributes.value(collection.id()); if (attr.keepFormat() == keepFormat) return true; // no change attr.setKeepFormat(keepFormat); updateCollectionAttr = true; break; } default: break; } if (updateCollectionAttr) { // Update the CollectionAttribute value. // Note that we can't supply 'collection' to CollectionModifyJob since // that also contains the CompatibilityAttribute value, which is read-only // for applications. So create a new Collection instance and only set a // value for CollectionAttribute. mCollectionAttributes[collection.id()] = attr; Collection c(collection.id()); CollectionAttribute* att = c.attribute(Collection::AddIfMissing); *att = attr; CollectionModifyJob* job = new CollectionModifyJob(c, this); connect(job, &CollectionModifyJob::result, this, &AkonadiModel::modifyCollectionAttrJobDone); return true; } } else { Item item = index.data(ItemRole).value(); if (item.isValid()) { bool updateItem = false; switch (role) { case CommandErrorRole: { const KAEvent::CmdErrType err = static_cast(value.toInt()); switch (err) { case KAEvent::CMD_NO_ERROR: case KAEvent::CMD_ERROR: case KAEvent::CMD_ERROR_PRE: case KAEvent::CMD_ERROR_POST: case KAEvent::CMD_ERROR_PRE_POST: { if (err == KAEvent::CMD_NO_ERROR && !item.hasAttribute()) return true; // no change EventAttribute* attr = item.attribute(Item::AddIfMissing); if (attr->commandError() == err) return true; // no change attr->setCommandError(err); updateItem = true; qCDebug(KALARM_LOG)<<"Item:"<"<= ColumnCount) return QVariant(); if (role == Qt::DisplayRole || role == ColumnTitleRole) { switch (section) { case TimeColumn: return i18nc("@title:column", "Time"); case TimeToColumn: return i18nc("@title:column", "Time To"); case RepeatColumn: return i18nc("@title:column", "Repeat"); case ColourColumn: return (role == Qt::DisplayRole) ? QString() : i18nc("@title:column", "Color"); case TypeColumn: return (role == Qt::DisplayRole) ? QString() : i18nc("@title:column", "Type"); case TextColumn: return i18nc("@title:column", "Message, File or Command"); case TemplateNameColumn: return i18nc("@title:column Template name", "Name"); } } else if (role == Qt::WhatsThisRole) return whatsThisText(section); break; default: break; } } return EntityTreeModel::entityHeaderData(section, orientation, role, group); } /****************************************************************************** * Recursive function to Q_EMIT the dataChanged() signal for all items in a * specified column range. */ void AkonadiModel::signalDataChanged(bool (*checkFunc)(const Item&), int startColumn, int endColumn, const QModelIndex& parent) { int start = -1; int end = -1; for (int row = 0, count = rowCount(parent); row < count; ++row) { const QModelIndex ix = index(row, 0, parent); const Item item = data(ix, ItemRole).value(); const bool isItem = item.isValid(); if (isItem) { if ((*checkFunc)(item)) { // For efficiency, Q_EMIT a single signal for each group of // consecutive items, rather than a separate signal for each item. if (start < 0) start = row; end = row; continue; } } if (start >= 0) Q_EMIT dataChanged(index(start, startColumn, parent), index(end, endColumn, parent)); start = -1; if (!isItem) signalDataChanged(checkFunc, startColumn, endColumn, ix); } if (start >= 0) Q_EMIT dataChanged(index(start, startColumn, parent), index(end, endColumn, parent)); } /****************************************************************************** * Signal every minute that the time-to-alarm values have changed. */ static bool checkItem_isActive(const Item& item) { return item.mimeType() == KAlarmCal::MIME_ACTIVE; } void AkonadiModel::slotUpdateTimeTo() { signalDataChanged(&checkItem_isActive, TimeToColumn, TimeToColumn, QModelIndex()); } /****************************************************************************** * Called when the colour used to display archived alarms has changed. */ static bool checkItem_isArchived(const Item& item) { return item.mimeType() == KAlarmCal::MIME_ARCHIVED; } void AkonadiModel::slotUpdateArchivedColour(const QColor&) { qCDebug(KALARM_LOG) << "AkonadiModel::slotUpdateArchivedColour"; signalDataChanged(&checkItem_isArchived, 0, ColumnCount - 1, QModelIndex()); } /****************************************************************************** * Called when the colour used to display disabled alarms has changed. */ static bool checkItem_isDisabled(const Item& item) { if (item.hasPayload()) { const KAEvent event = item.payload(); if (event.isValid()) return !event.enabled(); } return false; } void AkonadiModel::slotUpdateDisabledColour(const QColor&) { qCDebug(KALARM_LOG) << "AkonadiModel::slotUpdateDisabledColour"; signalDataChanged(&checkItem_isDisabled, 0, ColumnCount - 1, QModelIndex()); } /****************************************************************************** * Called when the definition of holidays has changed. */ static bool checkItem_excludesHolidays(const Item& item) { if (item.hasPayload()) { const KAEvent event = item.payload(); if (event.isValid() && event.holidaysExcluded()) return true; } return false; } void AkonadiModel::slotUpdateHolidays() { qCDebug(KALARM_LOG) << "AkonadiModel::slotUpdateHolidays"; Q_ASSERT(TimeToColumn == TimeColumn + 1); // signal should be emitted only for TimeTo and Time columns signalDataChanged(&checkItem_excludesHolidays, TimeColumn, TimeToColumn, QModelIndex()); } /****************************************************************************** * Called when the definition of working hours has changed. */ static bool checkItem_workTimeOnly(const Item& item) { if (item.hasPayload()) { const KAEvent event = item.payload(); if (event.isValid() && event.workTimeOnly()) return true; } return false; } void AkonadiModel::slotUpdateWorkingHours() { qCDebug(KALARM_LOG) << "AkonadiModel::slotUpdateWorkingHours"; Q_ASSERT(TimeToColumn == TimeColumn + 1); // signal should be emitted only for TimeTo and Time columns signalDataChanged(&checkItem_workTimeOnly, TimeColumn, TimeToColumn, QModelIndex()); } /****************************************************************************** * Called when the command error status of an alarm has changed, to save the new * status and update the visual command error indication. */ void AkonadiModel::updateCommandError(const KAEvent& event) { const QModelIndex ix = itemIndex(event.itemId()); if (ix.isValid()) setData(ix, QVariant(static_cast(event.commandError())), CommandErrorRole); } /****************************************************************************** * Return the foreground color for displaying a collection, based on the * supplied mime types which it contains, and on whether it is fully writable. */ QColor AkonadiModel::foregroundColor(const Akonadi::Collection& collection, const QStringList& mimeTypes) { QColor colour; if (mimeTypes.contains(KAlarmCal::MIME_ACTIVE)) colour = KColorScheme(QPalette::Active).foreground(KColorScheme::NormalText).color(); else if (mimeTypes.contains(KAlarmCal::MIME_ARCHIVED)) colour = Preferences::archivedColour(); else if (mimeTypes.contains(KAlarmCal::MIME_TEMPLATE)) colour = KColorScheme(QPalette::Active).foreground(KColorScheme::LinkText).color(); if (colour.isValid() && isWritable(collection) <= 0) return KColorUtils::lighten(colour, 0.2); return colour; } /****************************************************************************** * Set the background color for displaying the collection and its alarms. */ void AkonadiModel::setBackgroundColor(Collection& collection, const QColor& colour) { const QModelIndex ix = modelIndexForCollection(this, collection); if (ix.isValid()) setData(ix, QVariant(colour), Qt::BackgroundRole); } /****************************************************************************** * Return the background color for displaying the collection and its alarms, * after updating the collection from the Akonadi database. */ QColor AkonadiModel::backgroundColor(Akonadi::Collection& collection) const { if (!collection.isValid()) return QColor(); refresh(collection); return backgroundColor_p(collection); } /****************************************************************************** * Return the background color for displaying the collection and its alarms. */ QColor AkonadiModel::backgroundColor_p(const Akonadi::Collection& collection) const { if (collection.isValid()) { if (mCollectionAttributes.contains(collection.id())) return mCollectionAttributes.value(collection.id()).backgroundColor(); if (collection.hasAttribute()) return collection.attribute()->backgroundColor(); } return QColor(); } /****************************************************************************** * Return the display name for the collection, after updating the collection * from the Akonadi database. */ QString AkonadiModel::displayName(Akonadi::Collection& collection) const { if (!collection.isValid()) return QString(); refresh(collection); return collection.displayName(); } /****************************************************************************** * Return the storage type (file, directory, etc.) for the collection. */ QString AkonadiModel::storageType(const Akonadi::Collection& collection) const { const QUrl url = QUrl::fromUserInput(collection.remoteId(), QString(), QUrl::AssumeLocalFile); - return !url.isLocalFile() ? i18nc("@info", "URL") - : QFileInfo(url.toLocalFile()).isDir() ? i18nc("@info Directory in filesystem", "Directory") - : i18nc("@info", "File"); + return !url.isLocalFile() ? i18nc("@info", "URL") + : QFileInfo(url.toLocalFile()).isDir() ? i18nc("@info Directory in filesystem", "Directory") + : i18nc("@info", "File"); } /****************************************************************************** * Return a collection's tooltip text. The collection's enabled status is * evaluated for specified alarm types. */ QString AkonadiModel::tooltip(const Collection& collection, CalEvent::Types types) const { const QString name = QLatin1Char('@') + collection.displayName(); // insert markers for stripping out name const QUrl url = QUrl::fromUserInput(collection.remoteId(), QString(), QUrl::AssumeLocalFile); const QString type = QLatin1Char('@') + storageType(collection); // file/directory/URL etc. const QString locn = url.toDisplayString(QUrl::PreferLocalFile); const bool inactive = !collection.hasAttribute() || !(collection.attribute()->enabled() & types); const QString disabled = i18nc("@info", "Disabled"); const QString readonly = readOnlyTooltip(collection); const bool writable = readonly.isEmpty(); if (inactive && !writable) return xi18nc("@info:tooltip", "%1" "%2: %3" "%4, %5", name, type, locn, disabled, readonly); if (inactive || !writable) return xi18nc("@info:tooltip", "%1" "%2: %3" "%4", name, type, locn, (inactive ? disabled : readonly)); return xi18nc("@info:tooltip", "%1" "%2: %3", name, type, locn); } /****************************************************************************** * Return the read-only status tooltip for a collection. * A null string is returned if the collection is fully writable. */ QString AkonadiModel::readOnlyTooltip(const Collection& collection) { KACalendar::Compat compat; switch (AkonadiModel::isWritable(collection, compat)) { case 1: return QString(); case 0: return i18nc("@info", "Read-only (old format)"); default: if (compat == KACalendar::Current) return i18nc("@info", "Read-only"); return i18nc("@info", "Read-only (other format)"); } } /****************************************************************************** * Return the repetition text. */ QString AkonadiModel::repeatText(const KAEvent& event) const { - QString repeatText = event.recurrenceText(true); - if (repeatText.isEmpty()) - repeatText = event.repetitionText(true); - return repeatText; + const QString repeatText = event.recurrenceText(true); + return repeatText.isEmpty() ? event.repetitionText(true) : repeatText; } /****************************************************************************** * Return a string for sorting the repetition column. */ QString AkonadiModel::repeatOrder(const KAEvent& event) const { int repeatOrder = 0; int repeatInterval = 0; if (event.repeatAtLogin()) repeatOrder = 1; else { repeatInterval = event.recurInterval(); switch (event.recurType()) { case KARecurrence::MINUTELY: repeatOrder = 2; break; case KARecurrence::DAILY: repeatOrder = 3; break; case KARecurrence::WEEKLY: repeatOrder = 4; break; case KARecurrence::MONTHLY_DAY: case KARecurrence::MONTHLY_POS: repeatOrder = 5; break; case KARecurrence::ANNUAL_DATE: case KARecurrence::ANNUAL_POS: repeatOrder = 6; break; case KARecurrence::NO_RECUR: default: break; } } return QStringLiteral("%1%2").arg(static_cast('0' + repeatOrder)).arg(repeatInterval, 8, 10, QLatin1Char('0')); } /****************************************************************************** * Return the icon associated with the event's action. */ QPixmap* AkonadiModel::eventIcon(const KAEvent& event) const { switch (event.actionTypes()) { case KAEvent::ACT_EMAIL: return mEmailIcon; case KAEvent::ACT_AUDIO: return mAudioIcon; case KAEvent::ACT_COMMAND: return mCommandIcon; case KAEvent::ACT_DISPLAY: if (event.actionSubType() == KAEvent::FILE) return mFileIcon; // fall through to ACT_DISPLAY_COMMAND Q_FALLTHROUGH(); case KAEvent::ACT_DISPLAY_COMMAND: default: return mTextIcon; } } /****************************************************************************** * Returns the QWhatsThis text for a specified column. */ QString AkonadiModel::whatsThisText(int column) const { switch (column) { case TimeColumn: return i18nc("@info:whatsthis", "Next scheduled date and time of the alarm"); case TimeToColumn: return i18nc("@info:whatsthis", "How long until the next scheduled trigger of the alarm"); case RepeatColumn: return i18nc("@info:whatsthis", "How often the alarm recurs"); case ColourColumn: return i18nc("@info:whatsthis", "Background color of alarm message"); case TypeColumn: return i18nc("@info:whatsthis", "Alarm type (message, file, command or email)"); case TextColumn: return i18nc("@info:whatsthis", "Alarm message text, URL of text file to display, command to execute, or email subject line"); case TemplateNameColumn: return i18nc("@info:whatsthis", "Name of the alarm template"); default: return QString(); } } /****************************************************************************** * Remove a collection from Akonadi. The calendar file is not removed. */ bool AkonadiModel::removeCollection(const Akonadi::Collection& collection) { if (!collection.isValid()) return false; qCDebug(KALARM_LOG) << "AkonadiModel::removeCollection:" << collection.id(); Collection col = collection; mCollectionsDeleting << collection.id(); // Note: CollectionDeleteJob deletes the backend storage also. AgentManager* agentManager = AgentManager::self(); const AgentInstance instance = agentManager->instance(collection.resource()); if (instance.isValid()) agentManager->removeInstance(instance); -#if 0 - CollectionDeleteJob* job = new CollectionDeleteJob(col); - connect(job, &CollectionDeleteJob::result, this, &AkonadiModel::deleteCollectionJobDone); - mPendingCollectionJobs[job] = CollJobData(col.id(), displayName(col)); - job->start(); -#endif return true; } /****************************************************************************** * Return whether a collection is currently being deleted. */ bool AkonadiModel::isCollectionBeingDeleted(Collection::Id id) const { return mCollectionsDeleting.contains(id); } -#if 0 -/****************************************************************************** -* Called when a collection deletion job has completed. -* Checks for any error. -*/ -void AkonadiModel::deleteCollectionJobDone(KJob* j) -{ - QHash::iterator it = mPendingCollectionJobs.find(j); - CollJobData jobData; - if (it != mPendingCollectionJobs.end()) - { - jobData = it.value(); - mPendingCollectionJobs.erase(it); - } - if (j->error()) - { - Q_EMIT collectionDeleted(jobData.id, false); - const QString errMsg = xi18nc("@info", "Failed to remove calendar %1.", jobData.displayName); - qCCritical(KALARM_LOG) << "AkonadiModel::deleteCollectionJobDone:" << errMsg << ":" << j->errorString(); - KAMessageBox::error(MainWindow::mainMainWindow(), xi18nc("@info", "%1(%2)", errMsg, j->errorString())); - } - else - Q_EMIT collectionDeleted(jobData.id, true); -} -#endif - /****************************************************************************** * Reload a collection from Akonadi storage. The backend data is not reloaded. */ bool AkonadiModel::reloadCollection(const Akonadi::Collection& collection) { if (!collection.isValid()) return false; qCDebug(KALARM_LOG) << "AkonadiModel::reloadCollection:" << collection.id(); mMonitor->setCollectionMonitored(collection, false); mMonitor->setCollectionMonitored(collection, true); return true; } /****************************************************************************** * Reload a collection from Akonadi storage. The backend data is not reloaded. */ void AkonadiModel::reload() { qCDebug(KALARM_LOG) << "AkonadiModel::reload"; const Collection::List collections = mMonitor->collectionsMonitored(); for (const Collection& collection : collections) { mMonitor->setCollectionMonitored(collection, false); mMonitor->setCollectionMonitored(collection, true); } } /****************************************************************************** * Called when a CollectionAttribute modification job has completed. * Checks for any error. */ void AkonadiModel::modifyCollectionAttrJobDone(KJob* j) { Collection collection = static_cast(j)->collection(); const Collection::Id id = collection.id(); - bool newEnable = mNewCollectionEnabled.contains(id); - bool newEnabled = mNewCollectionEnabled.value(id, false); + const bool newEnable = mNewCollectionEnabled.contains(id); + const bool newEnabled = mNewCollectionEnabled.value(id, false); mNewCollectionEnabled.remove(id); if (j->error()) { if (mCollectionsDeleted.contains(id)) mCollectionsDeleted.removeAll(id); else { const QString errMsg = i18nc("@info", "Failed to update calendar \"%1\".", displayName(collection)); qCCritical(KALARM_LOG) << "AkonadiModel::modifyCollectionAttrJobDone:" << collection.id() << errMsg << ":" << j->errorString(); KAMessageBox::error(MainWindow::mainMainWindow(), i18nc("@info", "%1\n(%2)", errMsg, j->errorString())); } } else { if (newEnable) Q_EMIT collectionStatusChanged(collection, Enabled, newEnabled, false); } } /****************************************************************************** * Returns the index to a specified event. */ QModelIndex AkonadiModel::eventIndex(const KAEvent& event) { return itemIndex(event.itemId()); } /****************************************************************************** * Search for an event's item ID. This method ignores any itemId() value * contained in the KAEvent. The collectionId() is used if available. */ Item::Id AkonadiModel::findItemId(const KAEvent& event) { - Collection::Id colId = event.collectionId(); - QModelIndex start = (colId < 0) ? index(0, 0) : collectionIndex(Collection(colId)); - Qt::MatchFlags flags = (colId < 0) ? Qt::MatchExactly | Qt::MatchRecursive | Qt::MatchCaseSensitive | Qt::MatchWrap - : Qt::MatchExactly | Qt::MatchRecursive | Qt::MatchCaseSensitive; + const Collection::Id colId = event.collectionId(); + const QModelIndex start = (colId < 0) ? index(0, 0) : collectionIndex(Collection(colId)); + const Qt::MatchFlags flags = (colId < 0) ? Qt::MatchExactly | Qt::MatchRecursive | Qt::MatchCaseSensitive | Qt::MatchWrap + : Qt::MatchExactly | Qt::MatchRecursive | Qt::MatchCaseSensitive; const QModelIndexList indexes = match(start, RemoteIdRole, event.id(), -1, flags); for (const QModelIndex& ix : indexes) { if (ix.isValid()) { - Item::Id id = ix.data(ItemIdRole).toLongLong(); + const Item::Id id = ix.data(ItemIdRole).toLongLong(); if (id >= 0) { if (colId < 0 || ix.data(ParentCollectionRole).value().id() == colId) return id; } } } return -1; } #if 0 /****************************************************************************** * Return all events of a given type belonging to a collection. */ KAEvent::List AkonadiModel::events(Akonadi::Collection& collection, CalEvent::Type type) const { KAEvent::List list; const QModelIndex ix = modelIndexForCollection(this, collection); if (ix.isValid()) getChildEvents(ix, type, list); return list; } /****************************************************************************** * Recursive function to append all child Events with a given mime type. */ void AkonadiModel::getChildEvents(const QModelIndex& parent, CalEvent::Type type, KAEvent::List& events) const { for (int row = 0, count = rowCount(parent); row < count; ++row) { const QModelIndex ix = index(row, 0, parent); const Item item = data(ix, ItemRole).value(); if (item.isValid()) { if (item.hasPayload()) { KAEvent event = item.payload(); if (event.isValid() && event.category() == type) events += event; } } else { const Collection c = ix.data(CollectionRole).value(); if (c.isValid()) getChildEvents(ix, type, events); } } } #endif KAEvent AkonadiModel::event(Akonadi::Item::Id itemId) const { const QModelIndex ix = itemIndex(itemId); if (!ix.isValid()) return KAEvent(); return event(ix.data(ItemRole).value(), ix, nullptr); } KAEvent AkonadiModel::event(const QModelIndex& index) const { return event(index.data(ItemRole).value(), index, nullptr); } KAEvent AkonadiModel::event(const Akonadi::Item& item, const QModelIndex& index, Akonadi::Collection* collection) const { if (!item.isValid() || !item.hasPayload()) return KAEvent(); const QModelIndex ix = index.isValid() ? index : itemIndex(item.id()); if (!ix.isValid()) return KAEvent(); KAEvent e = item.payload(); if (e.isValid()) { Collection c = data(ix, ParentCollectionRole).value(); // Set collection ID using a const method, to avoid unnecessary copying of KAEvent e.setCollectionId_const(c.id()); if (collection) *collection = c; } return e; } #if 0 /****************************************************************************** * Add an event to the default or a user-selected Collection. */ AkonadiModel::Result AkonadiModel::addEvent(KAEvent* event, CalEvent::Type type, QWidget* promptParent, bool noPrompt) { qCDebug(KALARM_LOG) << "AkonadiModel::addEvent:" << event->id(); // Determine parent collection - prompt or use default bool cancelled; const Collection collection = destination(type, Collection::CanCreateItem, promptParent, noPrompt, &cancelled); if (!collection.isValid()) { delete event; if (cancelled) return Cancelled; qCDebug(KALARM_LOG) << "No collection"; return Failed; } if (!addEvent(event, collection)) { qCDebug(KALARM_LOG) << "Failed"; return Failed; // event was deleted by addEvent() } return Success; } #endif /****************************************************************************** * Add events to a specified Collection. * Events which are scheduled to be added to the collection are updated with * their Akonadi item ID. * The caller must connect to the itemDone() signal to check whether events * have been added successfully. Note that the first signal may be emitted * before this function returns. * Reply = true if item creation has been scheduled for all events, * = false if at least one item creation failed to be scheduled. */ bool AkonadiModel::addEvents(const KAEvent::List& events, Collection& collection) { bool ok = true; - for (int i = 0, count = events.count(); i < count; ++i) - ok = ok && addEvent(*events[i], collection); + for (KAEvent* event : events) + ok = ok && addEvent(*event, collection); return ok; } /****************************************************************************** * Add an event to a specified Collection. * If the event is scheduled to be added to the collection, it is updated with * its Akonadi item ID. * The event's 'updated' flag is cleared. * The caller must connect to the itemDone() signal to check whether events * have been added successfully. * Reply = true if item creation has been scheduled. */ bool AkonadiModel::addEvent(KAEvent& event, Collection& collection) { qCDebug(KALARM_LOG) << "AkonadiModel::addEvent: ID:" << event.id(); Item item; if (!event.setItemPayload(item, collection.contentMimeTypes())) { qCWarning(KALARM_LOG) << "AkonadiModel::addEvent: Invalid mime type for collection"; return false; } event.setItemId(item.id()); qCDebug(KALARM_LOG)<<"-> item id="<start(); qCDebug(KALARM_LOG)<<"...exiting"; return true; } /****************************************************************************** * Update an event in its collection. * The event retains its existing Akonadi item ID. * The event's 'updated' flag is cleared. * The caller must connect to the itemDone() signal to check whether the event * has been updated successfully. * Reply = true if item update has been scheduled. */ bool AkonadiModel::updateEvent(KAEvent& event) { qCDebug(KALARM_LOG) << "AkonadiModel::updateEvent: ID:" << event.id(); return updateEvent(event.itemId(), event); } bool AkonadiModel::updateEvent(Akonadi::Item::Id itemId, KAEvent& newEvent) { qCDebug(KALARM_LOG)<<"item id="<(); Item item = ix.data(ItemRole).value(); qCDebug(KALARM_LOG)<<"item id="<().id())) { qCDebug(KALARM_LOG) << "Collection being deleted"; return true; // the event's collection is being deleted } const Item item = ix.data(ItemRole).value(); ItemDeleteJob* job = new ItemDeleteJob(item); connect(job, &ItemDeleteJob::result, this, &AkonadiModel::itemJobDone); mPendingItemJobs[job] = itemId; job->start(); return true; } /****************************************************************************** * Queue an ItemModifyJob for execution. Ensure that only one job is * simultaneously active for any one Item. * * This is necessary because we can't call two ItemModifyJobs for the same Item * at the same time; otherwise Akonadi will detect a conflict and require manual * intervention to resolve it. */ void AkonadiModel::queueItemModifyJob(const Item& item) { qCDebug(KALARM_LOG) << "AkonadiModel::queueItemModifyJob:" << item.id(); QHash::Iterator it = mItemModifyJobQueue.find(item.id()); if (it != mItemModifyJobQueue.end()) { // A job is already queued for this item. Replace the queued item value with the new one. qCDebug(KALARM_LOG) << "Replacing previously queued job"; it.value() = item; } else { // There is no job already queued for this item if (mItemsBeingCreated.contains(item.id())) { qCDebug(KALARM_LOG) << "Waiting for item initialisation"; mItemModifyJobQueue[item.id()] = item; // wait for item initialisation to complete } else { Item newItem = item; const Item current = itemById(item.id()); // fetch the up-to-date item if (current.isValid()) newItem.setRevision(current.revision()); mItemModifyJobQueue[item.id()] = Item(); // mark the queued item as now executing ItemModifyJob* job = new ItemModifyJob(newItem); job->disableRevisionCheck(); connect(job, &ItemModifyJob::result, this, &AkonadiModel::itemJobDone); mPendingItemJobs[job] = item.id(); qCDebug(KALARM_LOG) << "Executing Modify job for item" << item.id() << ", revision=" << newItem.revision(); } } } /****************************************************************************** * Called when an item job has completed. * Checks for any error. * Note that for an ItemModifyJob, the item revision number may not be updated * to the post-modification value. The next queued ItemModifyJob is therefore * not kicked off from here, but instead from the slot attached to the * itemChanged() signal, which has the revision updated. */ void AkonadiModel::itemJobDone(KJob* j) { const QHash::iterator it = mPendingItemJobs.find(j); Item::Id itemId = -1; if (it != mPendingItemJobs.end()) { itemId = it.value(); mPendingItemJobs.erase(it); } const QByteArray jobClass = j->metaObject()->className(); qCDebug(KALARM_LOG) << "AkonadiModel::itemJobDone:" << jobClass; if (j->error()) { QString errMsg; if (jobClass == "Akonadi::ItemCreateJob") errMsg = i18nc("@info", "Failed to create alarm."); else if (jobClass == "Akonadi::ItemModifyJob") errMsg = i18nc("@info", "Failed to update alarm."); else if (jobClass == "Akonadi::ItemDeleteJob") errMsg = i18nc("@info", "Failed to delete alarm."); else Q_ASSERT(0); qCCritical(KALARM_LOG) << "AkonadiModel::itemJobDone:" << errMsg << itemId << ":" << j->errorString(); Q_EMIT itemDone(itemId, false); if (itemId >= 0 && jobClass == "Akonadi::ItemModifyJob") { // Execute the next queued job for this item const Item current = itemById(itemId); // fetch the up-to-date item checkQueuedItemModifyJob(current); } // Don't show error details by default, since it's from Akonadi and likely // to be too technical for general users. KAMessageBox::detailedError(MainWindow::mainMainWindow(), errMsg, j->errorString()); } else { if (jobClass == "Akonadi::ItemCreateJob") { // Prevent modification of the item until it is fully initialised. // Either slotMonitoredItemChanged() or slotRowsInserted(), or both, // will be called when the item is done. qCDebug(KALARM_LOG) << "item id=" << static_cast(j)->item().id(); mItemsBeingCreated << static_cast(j)->item().id(); } Q_EMIT itemDone(itemId); } /* if (itemId >= 0 && jobClass == "Akonadi::ItemModifyJob") { const QHash::iterator it = mItemModifyJobQueue.find(itemId); if (it != mItemModifyJobQueue.end()) { if (!it.value().isValid()) mItemModifyJobQueue.erase(it); // there are no more jobs queued for the item } }*/ } /****************************************************************************** * Check whether there are any ItemModifyJobs waiting for a specified item, and * if so execute the first one provided its creation has completed. This * prevents clashes in Akonadi conflicts between simultaneous ItemModifyJobs for * the same item. * * Note that when an item is newly created (e.g. via addEvent()), the KAlarm * resource itemAdded() function creates an ItemModifyJob to give it a remote * ID. Until that job is complete, any other ItemModifyJob for the item will * cause a conflict. */ void AkonadiModel::checkQueuedItemModifyJob(const Item& item) { if (mItemsBeingCreated.contains(item.id())) {qCDebug(KALARM_LOG)<<"Still being created"; return; // the item hasn't been fully initialised yet } const QHash::iterator it = mItemModifyJobQueue.find(item.id()); if (it == mItemModifyJobQueue.end()) {qCDebug(KALARM_LOG)<<"No jobs queued"; return; // there are no jobs queued for the item } Item qitem = it.value(); if (!qitem.isValid()) { // There is no further job queued for the item, so remove the item from the list qCDebug(KALARM_LOG)<<"No more jobs queued"; mItemModifyJobQueue.erase(it); } else { // Queue the next job for the Item, after updating the Item's // revision number to match that set by the job just completed. qitem.setRevision(item.revision()); mItemModifyJobQueue[item.id()] = Item(); // mark the queued item as now executing ItemModifyJob* job = new ItemModifyJob(qitem); job->disableRevisionCheck(); connect(job, &ItemModifyJob::result, this, &AkonadiModel::itemJobDone); mPendingItemJobs[job] = qitem.id(); qCDebug(KALARM_LOG) << "Executing queued Modify job for item" << qitem.id() << ", revision=" << qitem.revision(); } } /****************************************************************************** * Called when rows have been inserted into the model. */ void AkonadiModel::slotRowsInserted(const QModelIndex& parent, int start, int end) { qCDebug(KALARM_LOG) << "AkonadiModel::slotRowsInserted:" << start << "-" << end << "(parent =" << parent << ")"; for (int row = start; row <= end; ++row) { const QModelIndex ix = index(row, 0, parent); const Collection collection = ix.data(CollectionRole).value(); if (collection.isValid()) { // A collection has been inserted. // Ignore it if it isn't owned by a valid resource. qCDebug(KALARM_LOG) << "Collection" << collection.id() << collection.name(); if (AgentManager::self()->instance(collection.resource()).isValid()) { - QSet attrs; - attrs += CollectionAttribute::name(); + const QSet attrs{ CollectionAttribute::name() }; if (collection.hasAttribute()) mCollectionAttributes[collection.id()] = *collection.attribute(); setCollectionChanged(collection, attrs, true); Q_EMIT collectionAdded(collection); if (!mCollectionsBeingCreated.contains(collection.remoteId()) && (collection.rights() & writableRights) == writableRights) { // Update to current KAlarm format if necessary, and if the user agrees CalendarMigrator::updateToCurrentFormat(collection, false, MainWindow::mainMainWindow()); } } } else { // An item has been inserted const Item item = ix.data(ItemRole).value(); if (item.isValid()) { qCDebug(KALARM_LOG) << "item id=" << item.id() << ", revision=" << item.revision(); if (mItemsBeingCreated.removeAll(item.id())) // the new item has now been initialised checkQueuedItemModifyJob(item); // execute the next job queued for the item } } } const EventList events = eventList(parent, start, end); if (!events.isEmpty()) Q_EMIT eventsAdded(events); } /****************************************************************************** * Called when rows are about to be removed from the model. */ void AkonadiModel::slotRowsAboutToBeRemoved(const QModelIndex& parent, int start, int end) { qCDebug(KALARM_LOG) << "AkonadiModel::slotRowsAboutToBeRemoved:" << start << "-" << end << "(parent =" << parent << ")"; const EventList events = eventList(parent, start, end); if (!events.isEmpty()) { for (const Event& event : events) qCDebug(KALARM_LOG) << "Collection:" << event.collection.id() << ", Event ID:" << event.event.id(); Q_EMIT eventsToBeRemoved(events); } } /****************************************************************************** * Return a list of KAEvent/Collection pairs for a given range of rows. */ AkonadiModel::EventList AkonadiModel::eventList(const QModelIndex& parent, int start, int end) { EventList events; for (int row = start; row <= end; ++row) { Collection c; const QModelIndex ix = index(row, 0, parent); const KAEvent evnt = event(ix.data(ItemRole).value(), ix, &c); if (evnt.isValid()) events += Event(evnt, c); } return events; } /****************************************************************************** * Called when a monitored collection has changed. */ void AkonadiModel::slotCollectionChanged(const Akonadi::Collection& c, const QSet& attrNames) { qCDebug(KALARM_LOG) << "AkonadiModel::slotCollectionChanged:" << c.id() << attrNames; setCollectionChanged(c, attrNames, false); } /****************************************************************************** * Called when a monitored collection's properties or content have changed. * Optionally emits a signal if properties of interest have changed. */ void AkonadiModel::setCollectionChanged(const Collection& collection, const QSet& attributeNames, bool rowInserted) { // Check for a read/write permission change const Collection::Rights oldRights = mCollectionRights.value(collection.id(), Collection::AllRights); const Collection::Rights newRights = collection.rights() & writableRights; if (newRights != oldRights) { qCDebug(KALARM_LOG) << "AkonadiModel::setCollectionChanged:" << collection.id() << ": rights ->" << newRights; mCollectionRights[collection.id()] = newRights; Q_EMIT collectionStatusChanged(collection, ReadOnly, (newRights != writableRights), rowInserted); } // Check for a change in content mime types // (e.g. when a collection is first created at startup). const CalEvent::Types oldAlarmTypes = mCollectionAlarmTypes.value(collection.id(), CalEvent::EMPTY); const CalEvent::Types newAlarmTypes = CalEvent::types(collection.contentMimeTypes()); if (newAlarmTypes != oldAlarmTypes) { qCDebug(KALARM_LOG) << "AkonadiModel::setCollectionChanged:" << collection.id() << ": alarm types ->" << newAlarmTypes; mCollectionAlarmTypes[collection.id()] = newAlarmTypes; Q_EMIT collectionStatusChanged(collection, AlarmTypes, static_cast(newAlarmTypes), rowInserted); } // Check for the collection being enabled/disabled if (attributeNames.contains(CollectionAttribute::name()) && collection.hasAttribute()) { // Enabled/disabled can only be set by KAlarm (not the resource), so if the // attibute doesn't exist, it is ignored. static bool firstEnabled = true; const CalEvent::Types oldEnabled = mCollectionEnabled.value(collection.id(), CalEvent::EMPTY); const CalEvent::Types newEnabled = collection.attribute()->enabled(); if (firstEnabled || newEnabled != oldEnabled) { qCDebug(KALARM_LOG) << "AkonadiModel::setCollectionChanged:" << collection.id() << ": enabled ->" << newEnabled; firstEnabled = false; mCollectionEnabled[collection.id()] = newEnabled; Q_EMIT collectionStatusChanged(collection, Enabled, static_cast(newEnabled), rowInserted); } } // Check for the backend calendar format changing if (attributeNames.contains(CompatibilityAttribute::name())) { // Update to current KAlarm format if necessary, and if the user agrees qCDebug(KALARM_LOG) << "AkonadiModel::setCollectionChanged: CompatibilityAttribute"; Collection col(collection); refresh(col); CalendarMigrator::updateToCurrentFormat(col, false, MainWindow::mainMainWindow()); } if (mMigrating) { mCollectionIdsBeingCreated.removeAll(collection.id()); if (mCollectionsBeingCreated.isEmpty() && mCollectionIdsBeingCreated.isEmpty() && CalendarMigrator::completed()) { qCDebug(KALARM_LOG) << "AkonadiModel::setCollectionChanged: Migration completed"; mMigrating = false; Q_EMIT migrationCompleted(); } } } /****************************************************************************** * Called when a monitored collection is removed. */ void AkonadiModel::slotCollectionRemoved(const Collection& collection) { const Collection::Id id = collection.id(); qCDebug(KALARM_LOG) << "AkonadiModel::slotCollectionRemoved:" << id; mCollectionRights.remove(id); mCollectionsDeleting.removeAll(id); while (mCollectionsDeleted.count() > 20) // don't let list grow indefinitely mCollectionsDeleted.removeFirst(); mCollectionsDeleted << id; Q_EMIT collectionDeleted(id); } /****************************************************************************** * Called when a collection creation is about to start, or has completed. */ void AkonadiModel::slotCollectionBeingCreated(const QString& path, Akonadi::Collection::Id id, bool finished) { if (finished) { mCollectionsBeingCreated.removeAll(path); mCollectionIdsBeingCreated << id; } else mCollectionsBeingCreated << path; } /****************************************************************************** * Called when calendar migration has completed. */ void AkonadiModel::slotMigrationCompleted() { if (mCollectionsBeingCreated.isEmpty() && mCollectionIdsBeingCreated.isEmpty()) { qCDebug(KALARM_LOG) << "AkonadiModel: Migration completed"; mMigrating = false; Q_EMIT migrationCompleted(); } } /****************************************************************************** * Called when an item in the monitored collections has changed. */ void AkonadiModel::slotMonitoredItemChanged(const Akonadi::Item& item, const QSet&) { qCDebug(KALARM_LOG) << "AkonadiModel::slotMonitoredItemChanged: item id=" << item.id() << ", revision=" << item.revision(); mItemsBeingCreated.removeAll(item.id()); // the new item has now been initialised checkQueuedItemModifyJob(item); // execute the next job queued for the item KAEvent evnt = event(item); if (!evnt.isValid()) return; const QModelIndexList indexes = modelIndexesForItem(this, item); for (const QModelIndex& index : indexes) { if (index.isValid()) { // Wait to ensure that the base EntityTreeModel has processed the // itemChanged() signal first, before we Q_EMIT eventChanged(). Collection c = data(index, ParentCollectionRole).value(); evnt.setCollectionId(c.id()); mPendingEventChanges.enqueue(Event(evnt, c)); QTimer::singleShot(0, this, &AkonadiModel::slotEmitEventChanged); break; } } } /****************************************************************************** * Called to Q_EMIT a signal when an event in the monitored collections has * changed. */ void AkonadiModel::slotEmitEventChanged() { while (!mPendingEventChanges.isEmpty()) { Q_EMIT eventChanged(mPendingEventChanges.dequeue()); } } /****************************************************************************** * Refresh the specified Collection with up to date data. * Return: true if successful, false if collection not found. */ bool AkonadiModel::refresh(Akonadi::Collection& collection) const { const QModelIndex ix = modelIndexForCollection(this, collection); if (!ix.isValid()) return false; collection = ix.data(CollectionRole).value(); return true; } /****************************************************************************** * Refresh the specified Item with up to date data. * Return: true if successful, false if item not found. */ bool AkonadiModel::refresh(Akonadi::Item& item) const { const QModelIndexList ixs = modelIndexesForItem(this, item); if (ixs.isEmpty() || !ixs[0].isValid()) return false; item = ixs[0].data(ItemRole).value(); return true; } /****************************************************************************** * Find the QModelIndex of a collection. */ QModelIndex AkonadiModel::collectionIndex(const Akonadi::Collection& collection) const { const QModelIndex ix = modelIndexForCollection(this, collection); if (!ix.isValid()) return QModelIndex(); return ix; } /****************************************************************************** * Return the up to date collection with the specified Akonadi ID. */ Collection AkonadiModel::collectionById(Collection::Id id) const { const QModelIndex ix = modelIndexForCollection(this, Collection(id)); if (!ix.isValid()) return Collection(); return ix.data(CollectionRole).value(); } /****************************************************************************** * Find the QModelIndex of an item. */ QModelIndex AkonadiModel::itemIndex(const Akonadi::Item& item) const { const QModelIndexList ixs = modelIndexesForItem(this, item); if (ixs.isEmpty() || !ixs[0].isValid()) return QModelIndex(); return ixs[0]; } /****************************************************************************** * Return the up to date item with the specified Akonadi ID. */ Item AkonadiModel::itemById(Item::Id id) const { const QModelIndexList ixs = modelIndexesForItem(this, Item(id)); if (ixs.isEmpty() || !ixs[0].isValid()) return Item(); return ixs[0].data(ItemRole).value(); } /****************************************************************************** * Find the collection containing the specified Akonadi item ID. */ Collection AkonadiModel::collectionForItem(Item::Id id) const { const QModelIndex ix = itemIndex(id); if (!ix.isValid()) return Collection(); return ix.data(ParentCollectionRole).value(); } bool AkonadiModel::isCompatible(const Collection& collection) { return collection.hasAttribute() && collection.attribute()->compatibility() == KACalendar::Current; } /****************************************************************************** * Return whether a collection is fully writable. */ int AkonadiModel::isWritable(const Akonadi::Collection& collection) { KACalendar::Compat format; return isWritable(collection, format); } int AkonadiModel::isWritable(const Akonadi::Collection& collection, KACalendar::Compat& format) { format = KACalendar::Incompatible; if (!collection.isValid()) return -1; Collection col = collection; instance()->refresh(col); // update with latest data if ((col.rights() & writableRights) != writableRights) { format = KACalendar::Current; return -1; } if (!col.hasAttribute()) return -1; format = col.attribute()->compatibility(); switch (format) { case KACalendar::Current: return 1; case KACalendar::Converted: case KACalendar::Convertible: return 0; default: return -1; } } CalEvent::Types AkonadiModel::types(const Collection& collection) { return CalEvent::types(collection.contentMimeTypes()); } // vim: et sw=4: diff --git a/src/alarmcalendar.cpp b/src/alarmcalendar.cpp index caf46c41..8a210873 100644 --- a/src/alarmcalendar.cpp +++ b/src/alarmcalendar.cpp @@ -1,1606 +1,1599 @@ /* * alarmcalendar.cpp - KAlarm calendar file access * Program: kalarm * Copyright © 2001-2019 David Jarvie * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kalarm.h" #include "alarmcalendar.h" #include "collectionmodel.h" #include "filedialog.h" #include "functions.h" #include "kalarmapp.h" #include "mainwindow.h" #include "messagebox.h" #include "preferences.h" #include #include #include #include #include #include #include #include #include #include #include #include "kalarm_debug.h" using namespace Akonadi; using namespace KCalendarCore; using namespace KAlarmCal; static KACalendar::Compat fix(const KCalendarCore::FileStorage::Ptr&); static const QString displayCalendarName = QStringLiteral("displaying.ics"); static const Collection::Id DISPLAY_COL_ID = -1; // collection ID used for displaying calendar AlarmCalendar* AlarmCalendar::mResourcesCalendar = nullptr; AlarmCalendar* AlarmCalendar::mDisplayCalendar = nullptr; QUrl AlarmCalendar::mLastImportUrl; /****************************************************************************** * Initialise the alarm calendars, and ensure that their file names are different. * There are 2 calendars: * 1) A resources calendar containing the active alarms, archived alarms and * alarm templates; * 2) A user-specific one which contains details of alarms which are currently * being displayed to that user and which have not yet been acknowledged; * Reply = true if success, false if calendar name error. */ bool AlarmCalendar::initialiseCalendars() { QDir dir; dir.mkpath(QStandardPaths::writableLocation(QStandardPaths::DataLocation)); QString displayCal = QStandardPaths::writableLocation(QStandardPaths::DataLocation) + QLatin1Char('/') + displayCalendarName; AkonadiModel::instance(); CollectionControlModel::setAskDestinationPolicy(Preferences::askResource()); Preferences::setBackend(Preferences::Akonadi); Preferences::self()->save(); mResourcesCalendar = new AlarmCalendar(); mDisplayCalendar = new AlarmCalendar(displayCal, CalEvent::DISPLAYING); KACalendar::setProductId(KALARM_NAME, KALARM_VERSION); CalFormat::setApplication(QStringLiteral(KALARM_NAME), QString::fromLatin1(KACalendar::icalProductId())); return true; } /****************************************************************************** * Terminate access to all calendars. */ void AlarmCalendar::terminateCalendars() { delete mResourcesCalendar; mResourcesCalendar = nullptr; delete mDisplayCalendar; mDisplayCalendar = nullptr; } /****************************************************************************** * Return the display calendar, opening it first if necessary. */ AlarmCalendar* AlarmCalendar::displayCalendarOpen() { if (mDisplayCalendar->open()) return mDisplayCalendar; qCCritical(KALARM_LOG) << "AlarmCalendar::displayCalendarOpen: Open error"; return nullptr; } /****************************************************************************** * Find and return the event with the specified ID. * The calendar searched is determined by the calendar identifier in the ID. */ KAEvent* AlarmCalendar::getEvent(const EventId& eventId) { if (eventId.eventId().isEmpty()) return nullptr; return mResourcesCalendar->event(eventId); } /****************************************************************************** * Constructor for the resources calendar. */ AlarmCalendar::AlarmCalendar() - : - mCalType(RESOURCES), - mEventType(CalEvent::EMPTY), - mOpen(false), - mUpdateCount(0), - mUpdateSave(false), - mHaveDisabledAlarms(false) + : mCalType(RESOURCES) + , mEventType(CalEvent::EMPTY) + , mOpen(false) + , mUpdateCount(0) + , mUpdateSave(false) + , mHaveDisabledAlarms(false) { AkonadiModel* model = AkonadiModel::instance(); connect(model, &AkonadiModel::eventsAdded, this, &AlarmCalendar::slotEventsAdded); connect(model, &AkonadiModel::eventsToBeRemoved, this, &AlarmCalendar::slotEventsToBeRemoved); connect(model, &AkonadiModel::eventChanged, this, &AlarmCalendar::slotEventChanged); connect(model, &AkonadiModel::collectionStatusChanged, this, &AlarmCalendar::slotCollectionStatusChanged); Preferences::connect(SIGNAL(askResourceChanged(bool)), this, SLOT(setAskResource(bool))); } /****************************************************************************** * Constructor for a calendar file. */ AlarmCalendar::AlarmCalendar(const QString& path, CalEvent::Type type) - : - mEventType(type), - mOpen(false), - mUpdateCount(0), - mUpdateSave(false), - mHaveDisabledAlarms(false) + : mEventType(type) + , mOpen(false) + , mUpdateCount(0) + , mUpdateSave(false) + , mHaveDisabledAlarms(false) { switch (type) { case CalEvent::ACTIVE: case CalEvent::ARCHIVED: case CalEvent::TEMPLATE: case CalEvent::DISPLAYING: break; default: Q_ASSERT(false); // invalid event type for a calendar break; } mUrl = QUrl::fromUserInput(path, QString(), QUrl::AssumeLocalFile); QString icalPath = path; icalPath.replace(QStringLiteral("\\.vcs$"), QStringLiteral(".ics")); mICalUrl = QUrl::fromUserInput(icalPath, QString(), QUrl::AssumeLocalFile); mCalType = (path == icalPath) ? LOCAL_ICAL : LOCAL_VCAL; // is the calendar in ICal or VCal format? } AlarmCalendar::~AlarmCalendar() { close(); } /****************************************************************************** * Check whether the calendar is open. */ bool AlarmCalendar::isOpen() { return mOpen; } /****************************************************************************** * Open the calendar if not already open, and load it into memory. */ bool AlarmCalendar::open() { if (isOpen()) return true; if (mCalType == RESOURCES) { mOpen = true; } else { if (!mUrl.isValid()) return false; qCDebug(KALARM_LOG) << "AlarmCalendar::open:" << mUrl.toDisplayString(); if (!mCalendarStorage) { MemoryCalendar::Ptr calendar(new MemoryCalendar(Preferences::timeSpecAsZone())); mCalendarStorage = FileStorage::Ptr(new FileStorage(calendar)); } // Check for file's existence, assuming that it does exist when uncertain, // to avoid overwriting it. auto statJob = KIO::stat(mUrl, KIO::StatJob::SourceSide, 2); KJobWidgets::setWindow(statJob, MainWindow::mainMainWindow()); if (!statJob->exec() || load() == 0) { // The calendar file doesn't yet exist, or it's zero length, so create a new one bool created = false; if (mICalUrl.isLocalFile()) created = saveCal(mICalUrl.toLocalFile()); else { QTemporaryFile tmpFile; tmpFile.setAutoRemove(false); tmpFile.open(); created = saveCal(tmpFile.fileName()); } if (created) load(); } } if (!mOpen) { mCalendarStorage->calendar().clear(); mCalendarStorage.clear(); } return isOpen(); } /****************************************************************************** * Load the calendar into memory. * Reply = 1 if success * = 0 if zero-length file exists. * = -1 if failure to load calendar file * = -2 if instance uninitialised. */ int AlarmCalendar::load() { if (mCalType == RESOURCES) { } else { if (!mCalendarStorage) return -2; QString filename; qCDebug(KALARM_LOG) << "AlarmCalendar::load:" << mUrl.toDisplayString(); - if (!mUrl.isLocalFile()) { + if (!mUrl.isLocalFile()) + { auto getJob = KIO::storedGet(mUrl); KJobWidgets::setWindow(getJob, MainWindow::mainMainWindow()); if (!getJob->exec()) { qCCritical(KALARM_LOG) << "AlarmCalendar::load: Download failure"; KAMessageBox::error(MainWindow::mainMainWindow(), xi18nc("@info", "Cannot download calendar: %1", mUrl.toDisplayString())); return -1; } QTemporaryFile tmpFile; tmpFile.setAutoRemove(false); tmpFile.write(getJob->data()); qCDebug(KALARM_LOG) << "--- Downloaded to" << tmpFile.fileName(); filename = tmpFile.fileName(); - } else { - filename = mUrl.toLocalFile(); } + else + filename = mUrl.toLocalFile(); mCalendarStorage->calendar()->setTimeZone(Preferences::timeSpecAsZone()); mCalendarStorage->setFileName(filename); if (!mCalendarStorage->load()) { // Check if the file is zero length - if (mUrl.isLocalFile()) { + if (mUrl.isLocalFile()) + { auto statJob = KIO::stat(KIO::upUrl(mUrl)); KJobWidgets::setWindow(statJob, MainWindow::mainMainWindow()); statJob->exec(); KFileItem fi(statJob->statResult(), mUrl); if (!fi.size()) return 0; // file is zero length } qCCritical(KALARM_LOG) << "AlarmCalendar::load: Error loading calendar file '" << filename <<"'"; KAMessageBox::error(MainWindow::mainMainWindow(), xi18nc("@info", "Error loading calendar:%1Please fix or delete the file.", mUrl.toDisplayString())); // load() could have partially populated the calendar, so clear it out mCalendarStorage->calendar()->close(); mCalendarStorage->calendar().clear(); mCalendarStorage.clear(); mOpen = false; return -1; } if (!mLocalFile.isEmpty()) - if (mLocalFile.startsWith(QDir::tempPath())) { + { + if (mLocalFile.startsWith(QDir::tempPath())) QFile::remove(mLocalFile); - } + } mLocalFile = filename; fix(mCalendarStorage); // convert events to current KAlarm format for when calendar is saved updateDisplayKAEvents(); } mOpen = true; return 1; } /****************************************************************************** * Reload the calendar file into memory. */ bool AlarmCalendar::reload() { if (mCalType == RESOURCES) return true; if (!mCalendarStorage) return false; { qCDebug(KALARM_LOG) << "AlarmCalendar::reload:" << mUrl.toDisplayString(); close(); return open(); } } /****************************************************************************** * Save the calendar from memory to file. * If a filename is specified, create a new calendar file. */ bool AlarmCalendar::saveCal(const QString& newFile) { if (mCalType == RESOURCES) return true; if (!mCalendarStorage) return false; { if (!mOpen && newFile.isNull()) return false; qCDebug(KALARM_LOG) << "AlarmCalendar::saveCal:" << "\"" << newFile << "\"," << mEventType; QString saveFilename = newFile.isNull() ? mLocalFile : newFile; if (mCalType == LOCAL_VCAL && newFile.isNull() && mUrl.isLocalFile()) saveFilename = mICalUrl.toLocalFile(); mCalendarStorage->setFileName(saveFilename); mCalendarStorage->setSaveFormat(new ICalFormat); if (!mCalendarStorage->save()) { qCCritical(KALARM_LOG) << "AlarmCalendar::saveCal: Saving" << saveFilename << "failed."; KAMessageBox::error(MainWindow::mainMainWindow(), xi18nc("@info", "Failed to save calendar to %1", mICalUrl.toDisplayString())); return false; } if (!mICalUrl.isLocalFile()) { QFile file(saveFilename); file.open(QIODevice::ReadOnly); auto putJob = KIO::storedPut(&file, mICalUrl, -1); KJobWidgets::setWindow(putJob, MainWindow::mainMainWindow()); if (!putJob->exec()) { qCCritical(KALARM_LOG) << "AlarmCalendar::saveCal:" << saveFilename << "upload failed."; KAMessageBox::error(MainWindow::mainMainWindow(), xi18nc("@info", "Cannot upload calendar to %1", mICalUrl.toDisplayString())); return false; } } if (mCalType == LOCAL_VCAL) { // The file was in vCalendar format, but has now been saved in iCalendar format. mUrl = mICalUrl; mCalType = LOCAL_ICAL; } Q_EMIT calendarSaved(this); } mUpdateSave = false; return true; } /****************************************************************************** * Delete any temporary file at program exit. */ void AlarmCalendar::close() { if (mCalType != RESOURCES) { if (!mLocalFile.isEmpty()) { - if (mLocalFile.startsWith(QDir::tempPath())) { // removes it only if it IS a temporary file + if (mLocalFile.startsWith(QDir::tempPath())) // removes it only if it IS a temporary file QFile::remove(mLocalFile); - } mLocalFile = QStringLiteral(""); } } // Flag as closed now to prevent removeKAEvents() doing silly things // when it's called again mOpen = false; if (mCalendarStorage) { mCalendarStorage->calendar()->close(); mCalendarStorage->calendar().clear(); mCalendarStorage.clear(); } // Resource map should be empty, but just in case... while (!mResourceMap.isEmpty()) removeKAEvents(mResourceMap.begin().key(), true, CalEvent::ACTIVE | CalEvent::ARCHIVED | CalEvent::TEMPLATE | CalEvent::DISPLAYING); } /****************************************************************************** * Update whether to prompt for the resource to store new alarms in. */ void AlarmCalendar::setAskResource(bool ask) { CollectionControlModel::setAskDestinationPolicy(ask); } void AlarmCalendar::updateDisplayKAEvents() { if (mCalType == RESOURCES) return; qCDebug(KALARM_LOG) << "AlarmCalendar::updateDisplayKAEvents"; const Collection::Id key = DISPLAY_COL_ID; KAEvent::List& events = mResourceMap[key]; - int i, end; - for (i = 0, end = events.count(); i < end; ++i) + for (KAEvent* event : events) { - KAEvent* event = events[i]; mEventMap.remove(EventId(key, event->id())); delete event; } events.clear(); mEarliestAlarm[key] = nullptr; Calendar::Ptr cal = mCalendarStorage->calendar(); if (!cal) return; - Event::List kcalevents = cal->rawEvents(); - for (i = 0, end = kcalevents.count(); i < end; ++i) + const Event::List kcalevents = cal->rawEvents(); + for (Event::Ptr kcalevent : kcalevents) { - Event::Ptr kcalevent = kcalevents[i]; if (kcalevent->alarms().isEmpty()) continue; // ignore events without alarms KAEvent* event = new KAEvent(kcalevent); if (!event->isValid()) { qCWarning(KALARM_LOG) << "AlarmCalendar::updateDisplayKAEvents: Ignoring unusable event" << kcalevent->uid(); delete event; continue; // ignore events without usable alarms } event->setCollectionId(key); events += event; mEventMap[EventId(key, kcalevent->uid())] = event; } } /****************************************************************************** * Delete a calendar and all its KAEvent instances of specified alarm types from * the lists. * Called after the calendar is deleted or alarm types have been disabled, or * the AlarmCalendar is closed. */ void AlarmCalendar::removeKAEvents(Collection::Id key, bool closing, CalEvent::Types types) { bool removed = false; ResourceMap::Iterator rit = mResourceMap.find(key); if (rit != mResourceMap.end()) { bool empty = true; KAEvent::List& events = rit.value(); for (int i = 0, end = events.count(); i < end; ++i) { KAEvent* event = events[i]; bool remove = (event->collectionId() != key); if (remove) { if (key != DISPLAY_COL_ID) qCCritical(KALARM_LOG) << "AlarmCalendar::removeKAEvents: Event" << event->id() << ", collection" << event->collectionId() << "Indexed under collection" << key; } else remove = event->category() & types; if (remove) { mEventMap.remove(EventId(key, event->id())); delete event; removed = true; } else empty = false; } if (empty) mResourceMap.erase(rit); } if (removed) { mEarliestAlarm.remove(key); // Emit signal only if we're not in the process of closing the calendar if (!closing && mOpen) { Q_EMIT earliestAlarmChanged(); if (mHaveDisabledAlarms) checkForDisabledAlarms(); } } } /****************************************************************************** * Called when the enabled or read-only status of a collection has changed. * If the collection is now disabled, remove its events from the calendar. */ void AlarmCalendar::slotCollectionStatusChanged(const Collection& collection, AkonadiModel::Change change, const QVariant& value, bool inserted) { if (!inserted && change == AkonadiModel::Enabled) { // For each alarm type which has been disabled, remove the collection's // events from the map, but not from AkonadiModel. CalEvent::Types enabled = static_cast(value.toInt()); CalEvent::Types disabled = ~enabled & (CalEvent::ACTIVE | CalEvent::ARCHIVED | CalEvent::TEMPLATE); removeKAEvents(collection.id(), false, disabled); } } /****************************************************************************** * Called when events have been added to AkonadiModel. * Add corresponding KAEvent instances to those held by AlarmCalendar. */ void AlarmCalendar::slotEventsAdded(const AkonadiModel::EventList& events) { - for (int i = 0, count = events.count(); i < count; ++i) - slotEventChanged(events[i]); + for (const AkonadiModel::Event& event : events) + slotEventChanged(event); } /****************************************************************************** * Called when an event has been changed in AkonadiModel. * Change the corresponding KAEvent instance held by AlarmCalendar. */ void AlarmCalendar::slotEventChanged(const AkonadiModel::Event& event) { if (!event.isConsistent()) { qCCritical(KALARM_LOG) << "AlarmCalendar::slotEventChanged: Inconsistent AkonadiModel::Event: event:" << event.event.collectionId() << ", collection" << event.collection.id(); return; } bool added = true; bool updated = false; KAEventMap::Iterator it = mEventMap.find(event.eventId()); if (it != mEventMap.end()) { // The event ID already exists - remove the existing event first KAEvent* storedEvent = it.value(); if (event.event.category() == storedEvent->category()) { // The existing event is the same type - update it in place *storedEvent = event.event; addNewEvent(event.collection, storedEvent, true); updated = true; } else delete storedEvent; added = false; } if (!updated) addNewEvent(event.collection, new KAEvent(event.event)); if (event.event.category() == CalEvent::ACTIVE) { bool enabled = event.event.enabled(); checkForDisabledAlarms(!enabled, enabled); if (added && enabled && event.event.repeatAtLogin()) Q_EMIT atLoginEventAdded(event.event); } } /****************************************************************************** * Called when events are about to be removed from AkonadiModel. * Remove the corresponding KAEvent instances held by AlarmCalendar. */ void AlarmCalendar::slotEventsToBeRemoved(const AkonadiModel::EventList& events) { - for (int i = 0, count = events.count(); i < count; ++i) + for (const AkonadiModel::Event& event : events) { - if (!events[i].isConsistent()) - qCCritical(KALARM_LOG) << "AlarmCalendar::slotEventsToBeRemoved: Inconsistent AkonadiModel::Event: event:" << events[i].event.collectionId() << ", collection" << events[i].collection.id(); - else if (mEventMap.contains(events[i].eventId())) - deleteEventInternal(events[i].event, events[i].collection, false); + if (!event.isConsistent()) + qCCritical(KALARM_LOG) << "AlarmCalendar::slotEventsToBeRemoved: Inconsistent AkonadiModel::Event: event:" << event.event.collectionId() << ", collection" << event.collection.id(); + else if (mEventMap.contains(event.eventId())) + deleteEventInternal(event.event, event.collection, false); } } /****************************************************************************** * Import alarms from an external calendar and merge them into KAlarm's calendar. * The alarms are given new unique event IDs. * Parameters: parent = parent widget for error message boxes * Reply = true if all alarms in the calendar were successfully imported * = false if any alarms failed to be imported. */ bool AlarmCalendar::importAlarms(QWidget* parent, Collection* collection) { qCDebug(KALARM_LOG) << "AlarmCalendar::importAlarms"; const QUrl url = QFileDialog::getOpenFileUrl(parent, QString(), mLastImportUrl, QStringLiteral("%1 (*.vcs *.ics)").arg(i18nc("@info", "Calendar Files"))); if (url.isEmpty()) { qCCritical(KALARM_LOG) << "AlarmCalendar::importAlarms: Empty URL"; return false; } if (!url.isValid()) { qCDebug(KALARM_LOG) << "AlarmCalendar::importAlarms: Invalid URL"; return false; } mLastImportUrl = url.adjusted(QUrl::RemoveFilename); qCDebug(KALARM_LOG) << "AlarmCalendar::importAlarms:" << url.toDisplayString(); bool success = true; QString filename; bool local = url.isLocalFile(); if (local) { filename = url.toLocalFile(); if (!QFile::exists(filename)) { qCDebug(KALARM_LOG) << "AlarmCalendar::importAlarms: File '" << url.toDisplayString() <<"' not found"; KAMessageBox::error(parent, xi18nc("@info", "Could not load calendar %1.", url.toDisplayString())); return false; } } else { auto getJob = KIO::storedGet(url); KJobWidgets::setWindow(getJob, MainWindow::mainMainWindow()); if (!getJob->exec()) { qCCritical(KALARM_LOG) << "AlarmCalendar::importAlarms: Download failure"; KAMessageBox::error(parent, xi18nc("@info", "Cannot download calendar: %1", url.toDisplayString())); return false; } QTemporaryFile tmpFile; tmpFile.setAutoRemove(false); tmpFile.write(getJob->data()); tmpFile.seek(0); filename = tmpFile.fileName(); qCDebug(KALARM_LOG) << "--- Downloaded to" << filename; } // Read the calendar and add its alarms to the current calendars MemoryCalendar::Ptr cal(new MemoryCalendar(Preferences::timeSpecAsZone())); FileStorage::Ptr calStorage(new FileStorage(cal, filename)); success = calStorage->load(); if (!success) { qCDebug(KALARM_LOG) << "AlarmCalendar::importAlarms: Error loading calendar '" << filename <<"'"; KAMessageBox::error(parent, xi18nc("@info", "Could not load calendar %1.", url.toDisplayString())); } else { - KACalendar::Compat caltype = fix(calStorage); - CalEvent::Types wantedTypes = collection && collection->isValid() ? CalEvent::types(collection->contentMimeTypes()) : CalEvent::EMPTY; + const KACalendar::Compat caltype = fix(calStorage); + const CalEvent::Types wantedTypes = collection && collection->isValid() ? CalEvent::types(collection->contentMimeTypes()) : CalEvent::EMPTY; Collection activeColl, archiveColl, templateColl; - Event::List events = cal->rawEvents(); - for (int i = 0, end = events.count(); i < end; ++i) + const Event::List events = cal->rawEvents(); + for (Event::Ptr event : events) { - Event::Ptr event = events[i]; if (event->alarms().isEmpty() || !KAEvent(event).isValid()) continue; // ignore events without alarms, or usable alarms CalEvent::Type type = CalEvent::status(event); if (type == CalEvent::TEMPLATE) { // If we know the event was not created by KAlarm, don't treat it as a template if (caltype == KACalendar::Incompatible) type = CalEvent::ACTIVE; } Collection* coll; if (collection && collection->isValid()) { if (!(type & wantedTypes)) continue; coll = collection; } else { switch (type) { case CalEvent::ACTIVE: coll = &activeColl; break; case CalEvent::ARCHIVED: coll = &archiveColl; break; case CalEvent::TEMPLATE: coll = &templateColl; break; default: continue; } if (!coll->isValid()) *coll = CollectionControlModel::destination(type); } Event::Ptr newev(new Event(*event)); // If there is a display alarm without display text, use the event // summary text instead. if (type == CalEvent::ACTIVE && !newev->summary().isEmpty()) { const Alarm::List& alarms = newev->alarms(); - for (int ai = 0, aend = alarms.count(); ai < aend; ++ai) + for (Alarm::Ptr alarm : alarms) { - Alarm::Ptr alarm = alarms[ai]; if (alarm->type() == Alarm::Display && alarm->text().isEmpty()) alarm->setText(newev->summary()); } newev->setSummary(QString()); // KAlarm only uses summary for template names } // Give the event a new ID and add it to the calendars newev->setUid(CalEvent::uid(CalFormat::createUniqueId(), type)); KAEvent* newEvent = new KAEvent(newev); if (!AkonadiModel::instance()->addEvent(*newEvent, *coll)) success = false; } } if (!local) QFile::remove(filename); return success; } /****************************************************************************** * Export all selected alarms to an external calendar. * The alarms are given new unique event IDs. * Parameters: parent = parent widget for error message boxes * Reply = true if all alarms in the calendar were successfully exported * = false if any alarms failed to be exported. */ bool AlarmCalendar::exportAlarms(const KAEvent::List& events, QWidget* parent) { bool append; QString file = FileDialog::getSaveFileName(QUrl(QStringLiteral("kfiledialog:///exportalarms")), QStringLiteral("*.ics|%1").arg(i18nc("@info", "Calendar Files")), parent, i18nc("@title:window", "Choose Export Calendar"), &append); if (file.isEmpty()) return false; - QUrl url = QUrl::fromLocalFile(file); + const QUrl url = QUrl::fromLocalFile(file); if (!url.isValid()) { qCDebug(KALARM_LOG) << "AlarmCalendar::exportAlarms: Invalid URL" << url; return false; } qCDebug(KALARM_LOG) << "AlarmCalendar::exportAlarms:" << url.toDisplayString(); MemoryCalendar::Ptr calendar(new MemoryCalendar(Preferences::timeSpecAsZone())); FileStorage::Ptr calStorage(new FileStorage(calendar, file)); if (append && !calStorage->load()) { KIO::UDSEntry uds; auto statJob = KIO::stat(url, KIO::StatJob::SourceSide, 2); KJobWidgets::setWindow(statJob, parent); statJob->exec(); KFileItem fi(statJob->statResult(), url); if (fi.size()) { qCCritical(KALARM_LOG) << "AlarmCalendar::exportAlarms: Error loading calendar file" << file << "for append"; KAMessageBox::error(MainWindow::mainMainWindow(), xi18nc("@info", "Error loading calendar to append to:%1", url.toDisplayString())); return false; } } KACalendar::setKAlarmVersion(calendar); // Add the alarms to the calendar bool success = true; bool exported = false; for (int i = 0, end = events.count(); i < end; ++i) { const KAEvent* event = events[i]; Event::Ptr kcalEvent(new Event); - CalEvent::Type type = event->category(); - QString id = CalEvent::uid(kcalEvent->uid(), type); + const CalEvent::Type type = event->category(); + const QString id = CalEvent::uid(kcalEvent->uid(), type); kcalEvent->setUid(id); event->updateKCalEvent(kcalEvent, KAEvent::UID_IGNORE); if (calendar->addEvent(kcalEvent)) exported = true; else success = false; } if (exported) { // One or more alarms have been exported to the calendar. // Save the calendar to file. QTemporaryFile* tempFile = nullptr; bool local = url.isLocalFile(); if (!local) { tempFile = new QTemporaryFile; file = tempFile->fileName(); } calStorage->setFileName(file); calStorage->setSaveFormat(new ICalFormat); if (!calStorage->save()) { qCCritical(KALARM_LOG) << "AlarmCalendar::exportAlarms:" << file << ": failed"; KAMessageBox::error(MainWindow::mainMainWindow(), xi18nc("@info", "Failed to save new calendar to:%1", url.toDisplayString())); success = false; } else if (!local) { QFile qFile(file); qFile.open(QIODevice::ReadOnly); auto uploadJob = KIO::storedPut(&qFile, url, -1); KJobWidgets::setWindow(uploadJob, parent); if (!uploadJob->exec()) { qCCritical(KALARM_LOG) << "AlarmCalendar::exportAlarms:" << file << ": upload failed"; KAMessageBox::error(MainWindow::mainMainWindow(), xi18nc("@info", "Cannot upload new calendar to:%1", url.toDisplayString())); success = false; } } delete tempFile; } calendar->close(); return success; } /****************************************************************************** * Flag the start of a group of calendar update calls. * The purpose is to avoid multiple calendar saves during a group of operations. */ void AlarmCalendar::startUpdate() { ++mUpdateCount; } /****************************************************************************** * Flag the end of a group of calendar update calls. * The calendar is saved if appropriate. */ bool AlarmCalendar::endUpdate() { if (mUpdateCount > 0) --mUpdateCount; if (!mUpdateCount) { if (mUpdateSave) return saveCal(); } return true; } /****************************************************************************** * Save the calendar, or flag it for saving if in a group of calendar update calls. * Note that this method has no effect for Akonadi calendars. */ bool AlarmCalendar::save() { if (mUpdateCount) { mUpdateSave = true; return true; } else return saveCal(); } /****************************************************************************** * This method must only be called from the main KAlarm queue processing loop, * to prevent asynchronous calendar operations interfering with one another. * * Purge a list of archived events from the calendar. */ void AlarmCalendar::purgeEvents(const KAEvent::List& events) { - for (int i = 0, end = events.count(); i < end; ++i) + for (const KAEvent* event : events) { - deleteEventInternal(*events[i]); + deleteEventInternal(*event); } if (mHaveDisabledAlarms) checkForDisabledAlarms(); saveCal(); } /****************************************************************************** * Add the specified event to the calendar. * If it is an active event and 'useEventID' is false, a new event ID is * created. In all other cases, the event ID is taken from 'event' (if non-null). * 'event' is updated with the actual event ID. * The event is added to 'resource' if specified; otherwise the default resource * is used or the user is prompted, depending on policy. If 'noPrompt' is true, * the user will not be prompted so that if no default resource is defined, the * function will fail. * Reply = true if 'event' was written to the calendar, in which case (not * Akonadi) ownership of 'event' is taken by the calendar. 'event' * is updated. * = false if an error occurred, in which case 'event' is unchanged. */ bool AlarmCalendar::addEvent(KAEvent& evnt, QWidget* promptParent, bool useEventID, Collection* collection, bool noPrompt, bool* cancelled) { if (cancelled) *cancelled = false; if (!mOpen) return false; // Check that the event type is valid for the calendar qCDebug(KALARM_LOG) << "AlarmCalendar::addEvent:" << evnt.id(); - CalEvent::Type type = evnt.category(); + const CalEvent::Type type = evnt.category(); if (type != mEventType) { switch (type) { case CalEvent::ACTIVE: case CalEvent::ARCHIVED: case CalEvent::TEMPLATE: if (mEventType == CalEvent::EMPTY) break; // fall through to default Q_FALLTHROUGH(); default: return false; } } Collection::Id key = (collection && collection->isValid()) ? collection->id() : -1; Event::Ptr kcalEvent((mCalType == RESOURCES) ? (Event*)nullptr : new Event); KAEvent* event = new KAEvent(evnt); QString id = event->id(); if (type == CalEvent::ACTIVE) { if (id.isEmpty()) useEventID = false; else if (!useEventID) id.clear(); } else useEventID = true; if (id.isEmpty()) id = (mCalType == RESOURCES) ? CalFormat::createUniqueId() : kcalEvent->uid(); if (useEventID) { id = CalEvent::uid(id, type); if (kcalEvent) kcalEvent->setUid(id); } event->setEventId(id); bool ok = false; bool remove = false; if (mCalType == RESOURCES) { Collection col; if (collection && CollectionControlModel::isEnabled(*collection, type)) col = *collection; else col = CollectionControlModel::destination(type, promptParent, noPrompt, cancelled); if (col.isValid()) { // Don't add event to mEventMap yet - its Akonadi item id is not yet known. // It will be added once it is inserted into AkonadiModel. ok = AkonadiModel::instance()->addEvent(*event, col); remove = ok; // if success, delete the local event instance on exit if (ok && type == CalEvent::ACTIVE && !event->enabled()) checkForDisabledAlarms(true, false); } } else { // It's the display calendar event->updateKCalEvent(kcalEvent, KAEvent::UID_IGNORE); key = DISPLAY_COL_ID; if (!mEventMap.contains(EventId(key, event->id()))) { addNewEvent(Collection(), event); ok = mCalendarStorage->calendar()->addEvent(kcalEvent); remove = !ok; } } if (!ok) { if (remove) { // Adding to mCalendar failed, so undo AlarmCalendar::addEvent() mEventMap.remove(EventId(key, event->id())); KAEvent::List& events = mResourceMap[key]; int i = events.indexOf(event); if (i >= 0) events.remove(i); if (mEarliestAlarm[key] == event) findEarliestAlarm(key); } delete event; return false; } evnt = *event; if (remove) delete event; return true; } /****************************************************************************** * Internal method to add an already checked event to the calendar. * mEventMap takes ownership of the KAEvent. * If 'replace' is true, an existing event is being updated (NOTE: its category() * must remain the same). */ void AlarmCalendar::addNewEvent(const Collection& collection, KAEvent* event, bool replace) { - Collection::Id key = collection.isValid() ? collection.id() : -1; + const Collection::Id key = collection.isValid() ? collection.id() : -1; event->setCollectionId(key); if (!replace) { mResourceMap[key] += event; mEventMap[EventId(key, event->id())] = event; } if (collection.isValid() && (AkonadiModel::types(collection) & CalEvent::ACTIVE) && event->category() == CalEvent::ACTIVE) { // Update the earliest alarm to trigger - KAEvent* earliest = mEarliestAlarm.value(key, (KAEvent*)nullptr); + const KAEvent* earliest = mEarliestAlarm.value(key, (KAEvent*)nullptr); if (replace && earliest == event) findEarliestAlarm(key); else { const KADateTime dt = event->nextTrigger(KAEvent::ALL_TRIGGER).effectiveKDateTime(); if (dt.isValid() && (!earliest || dt < earliest->nextTrigger(KAEvent::ALL_TRIGGER))) { mEarliestAlarm[key] = event; Q_EMIT earliestAlarmChanged(); } } } } /****************************************************************************** * Modify the specified event in the calendar with its new contents. * The new event must have a different event ID from the old one. * It is assumed to be of the same event type as the old one (active, etc.) * Reply = true if 'newEvent' was written to the calendar, in which case (not * Akonadi) ownership of 'newEvent' is taken by the calendar. * 'newEvent' is updated. * = false if an error occurred, in which case 'newEvent' is unchanged. */ bool AlarmCalendar::modifyEvent(const EventId& oldEventId, KAEvent& newEvent) { - EventId newId(oldEventId.collectionId(), newEvent.id()); + const EventId newId(oldEventId.collectionId(), newEvent.id()); qCDebug(KALARM_LOG) << "AlarmCalendar::modifyEvent:" << oldEventId << "->" << newId; bool noNewId = newId.isEmpty(); if (!noNewId && oldEventId == newId) { qCCritical(KALARM_LOG) << "AlarmCalendar::modifyEvent: Same IDs"; return false; } if (!mOpen) return false; if (mCalType == RESOURCES) { // Set the event's ID and Akonadi ID, and update the old // event in Akonadi. - KAEvent* storedEvent = event(oldEventId); + const KAEvent* storedEvent = event(oldEventId); if (!storedEvent) { qCCritical(KALARM_LOG) << "AlarmCalendar::modifyEvent: Old event not found"; return false; } if (noNewId) newEvent.setEventId(CalFormat::createUniqueId()); Collection c = AkonadiModel::instance()->collectionById(oldEventId.collectionId()); if (!c.isValid()) return false; // Don't add new event to mEventMap yet - its Akonadi item id is not yet known if (!AkonadiModel::instance()->addEvent(newEvent, c)) return false; // Note: deleteEventInternal() will delete storedEvent before using the // event parameter, so need to pass a copy as the parameter. deleteEventInternal(KAEvent(*storedEvent), c); if (mHaveDisabledAlarms) checkForDisabledAlarms(); } else { // This functionality isn't needed for the display calendar. // The calendar would take ownership of newEvent. return false; } return true; } /****************************************************************************** * Update the specified event in the calendar with its new contents. * The event retains the same ID. The event must be in the resource calendar. * Reply = event which has been updated * = 0 if error. */ KAEvent* AlarmCalendar::updateEvent(const KAEvent& evnt) { return updateEvent(&evnt); } KAEvent* AlarmCalendar::updateEvent(const KAEvent* evnt) { if (!mOpen || mCalType != RESOURCES) return nullptr; KAEvent* kaevnt = event(EventId(*evnt)); if (kaevnt) { KAEvent newEvnt(*evnt); newEvnt.setItemId(evnt->itemId()); if (AkonadiModel::instance()->updateEvent(newEvnt)) { *kaevnt = newEvnt; return kaevnt; } } qCDebug(KALARM_LOG) << "AlarmCalendar::updateEvent: error"; return nullptr; } /****************************************************************************** * Delete the specified event from the resource calendar, if it exists. * The calendar is then optionally saved. */ bool AlarmCalendar::deleteEvent(const KAEvent& event, bool saveit) { if (mOpen && mCalType == RESOURCES) { - CalEvent::Type status = deleteEventInternal(event); + const CalEvent::Type status = deleteEventInternal(event); if (mHaveDisabledAlarms) checkForDisabledAlarms(); if (status != CalEvent::EMPTY) { if (saveit) return save(); return true; } } return false; } /****************************************************************************** * Delete the specified event from the calendar, if it exists. * The calendar is then optionally saved. */ bool AlarmCalendar::deleteDisplayEvent(const QString& eventID, bool saveit) { if (mOpen && mCalType != RESOURCES) { - CalEvent::Type status = deleteEventInternal(eventID); + const CalEvent::Type status = deleteEventInternal(eventID); if (mHaveDisabledAlarms) checkForDisabledAlarms(); if (status != CalEvent::EMPTY) { if (saveit) return save(); return true; } } return false; } /****************************************************************************** * Internal method to delete the specified event from the calendar and lists. * Reply = event status, if it was found in the resource calendar/collection or * local calendar * = CalEvent::EMPTY otherwise. */ CalEvent::Type AlarmCalendar::deleteEventInternal(const KAEvent& event, bool deleteFromAkonadi) { - Collection collection = AkonadiModel::instance()->collectionById(event.collectionId()); + const Collection collection = AkonadiModel::instance()->collectionById(event.collectionId()); if (!collection.isValid()) return CalEvent::EMPTY; return deleteEventInternal(event.id(), event, collection, deleteFromAkonadi); } CalEvent::Type AlarmCalendar::deleteEventInternal(const KAEvent& event, const Akonadi::Collection& collection, bool deleteFromAkonadi) { if (!collection.isValid()) return CalEvent::EMPTY; if (event.collectionId() != collection.id()) { qCCritical(KALARM_LOG) << "AlarmCalendar::deleteEventInternal: Event" << event.id() << ": collection" << event.collectionId() << "differs from 'collection'" << collection.id(); return CalEvent::EMPTY; } return deleteEventInternal(event.id(), event, collection, deleteFromAkonadi); } CalEvent::Type AlarmCalendar::deleteEventInternal(const QString& eventID, const KAEvent& event, const Akonadi::Collection& collection, bool deleteFromAkonadi) { // Make a copy of the KAEvent and the ID QString, since the supplied // references might be destructed when the event is deleted below. const QString id = eventID; const KAEvent paramEvent = event; Event::Ptr kcalEvent; if (mCalendarStorage) kcalEvent = mCalendarStorage->calendar()->event(id); - Collection::Id key = collection.isValid() ? collection.id() : -1; + const Collection::Id key = collection.isValid() ? collection.id() : -1; KAEventMap::Iterator it = mEventMap.find(EventId(key, id)); if (it != mEventMap.end()) { KAEvent* ev = it.value(); mEventMap.erase(it); KAEvent::List& events = mResourceMap[key]; int i = events.indexOf(ev); if (i >= 0) events.remove(i); delete ev; if (mEarliestAlarm[key] == ev) findEarliestAlarm(collection); } else { for (EarliestMap::Iterator eit = mEarliestAlarm.begin(); eit != mEarliestAlarm.end(); ++eit) { KAEvent* ev = eit.value(); if (ev && ev->id() == id) { findEarliestAlarm(eit.key()); break; } } } CalEvent::Type status = CalEvent::EMPTY; if (kcalEvent) { status = CalEvent::status(kcalEvent); mCalendarStorage->calendar()->deleteEvent(kcalEvent); } else if (deleteFromAkonadi) { // It's an Akonadi event CalEvent::Type s = paramEvent.category(); if (AkonadiModel::instance()->deleteEvent(paramEvent)) status = s; } return status; } /****************************************************************************** * Return the event with the specified ID. * If 'checkDuplicates' is true, and the collection ID is invalid, if there is * a unique event with the given ID, it will be returned. */ KAEvent* AlarmCalendar::event(const EventId& uniqueID, bool checkDuplicates) { if (!isValid()) return nullptr; const QString eventId = uniqueID.eventId(); if (uniqueID.collectionId() == -1 && checkDuplicates) { // The collection isn't known, but use the event ID if it is // unique among all collections. - KAEvent::List list = events(eventId); + const KAEvent::List list = events(eventId); if (list.count() > 1) { qCWarning(KALARM_LOG) << "AlarmCalendar::event: Multiple events found with ID" << eventId; return nullptr; } if (list.isEmpty()) return nullptr; return list[0]; } KAEventMap::ConstIterator it = mEventMap.constFind(uniqueID); if (it == mEventMap.constEnd()) return nullptr; return it.value(); } /****************************************************************************** * Return the event with the specified ID. * For the Akonadi version, this method is for the display calendar only. */ Event::Ptr AlarmCalendar::kcalEvent(const QString& uniqueID) { Q_ASSERT(mCalType != RESOURCES); // only allowed for display calendar if (!mCalendarStorage) return Event::Ptr(); return mCalendarStorage->calendar()->event(uniqueID); } /****************************************************************************** * Find the alarm template with the specified name. * Reply = 0 if not found. */ KAEvent* AlarmCalendar::templateEvent(const QString& templateName) { if (templateName.isEmpty()) return nullptr; - KAEvent::List eventlist = events(CalEvent::TEMPLATE); - for (int i = 0, end = eventlist.count(); i < end; ++i) + const KAEvent::List eventlist = events(CalEvent::TEMPLATE); + for (KAEvent* event : eventlist) { - if (eventlist[i]->templateName() == templateName) - return eventlist[i]; + if (event->templateName() == templateName) + return event; } return nullptr; } /****************************************************************************** * Return all events with the specified ID, from all calendars. */ KAEvent::List AlarmCalendar::events(const QString& uniqueId) const { KAEvent::List list; if (mCalType == RESOURCES && isValid()) { for (ResourceMap::ConstIterator rit = mResourceMap.constBegin(); rit != mResourceMap.constEnd(); ++rit) { const Collection::Id id = rit.key(); KAEventMap::ConstIterator it = mEventMap.constFind(EventId(id, uniqueId)); if (it != mEventMap.constEnd()) list += it.value(); } } return list; } /****************************************************************************** * Return all events in the calendar which contain alarms. * Optionally the event type can be filtered, using an OR of event types. */ KAEvent::List AlarmCalendar::events(const Akonadi::Collection& collection, CalEvent::Types type) const { KAEvent::List list; if (mCalType != RESOURCES && (!mCalendarStorage || collection.isValid())) return list; if (collection.isValid()) { - Collection::Id key = collection.isValid() ? collection.id() : -1; + const Collection::Id key = collection.isValid() ? collection.id() : -1; ResourceMap::ConstIterator rit = mResourceMap.constFind(key); if (rit == mResourceMap.constEnd()) return list; const KAEvent::List events = rit.value(); if (type == CalEvent::EMPTY) return events; - for (int i = 0, end = events.count(); i < end; ++i) - if (type & events[i]->category()) - list += events[i]; + for (KAEvent* const event : events) + if (type & event->category()) + list += event; } else { for (ResourceMap::ConstIterator rit = mResourceMap.constBegin(); rit != mResourceMap.constEnd(); ++rit) { const KAEvent::List events = rit.value(); if (type == CalEvent::EMPTY) list += events; else { - for (int i = 0, end = events.count(); i < end; ++i) - if (type & events[i]->category()) - list += events[i]; + for (KAEvent* const event : events) + if (type & event->category()) + list += event; } } } return list; } /****************************************************************************** * Return all events in the calendar which contain usable alarms. * For the Akonadi version, this method is for the display calendar only. * Optionally the event type can be filtered, using an OR of event types. */ Event::List AlarmCalendar::kcalEvents(CalEvent::Type type) { Event::List list; Q_ASSERT(mCalType != RESOURCES); // only allowed for display calendar if (!mCalendarStorage) return list; list = mCalendarStorage->calendar()->rawEvents(); for (int i = 0; i < list.count(); ) { - Event::Ptr event = list[i]; + Event::Ptr event = list.at(i); if (event->alarms().isEmpty() || (type != CalEvent::EMPTY && !(type & CalEvent::status(event))) || !KAEvent(event).isValid()) list.remove(i); else ++i; } return list; } /****************************************************************************** * Return whether an event is read-only. * Display calendar events are always returned as read-only. */ bool AlarmCalendar::eventReadOnly(Item::Id id) const { if (mCalType != RESOURCES) return true; AkonadiModel* model = AkonadiModel::instance(); - Collection collection = model->collectionForItem(id); - KAEvent event = model->event(id); + const Collection collection = model->collectionForItem(id); + const KAEvent event = model->event(id); if (!CollectionControlModel::isWritableEnabled(collection, event.category())) return true; return !event.isValid() || event.isReadOnly(); // || compatibility(event) != KACalendar::Current; } /****************************************************************************** * Return the collection containing a specified event. */ Collection AlarmCalendar::collectionForEvent(Item::Id itemId) const { if (mCalType != RESOURCES) return Collection(); return AkonadiModel::instance()->collectionForItem(itemId); } /****************************************************************************** * Called when an alarm's enabled status has changed. */ void AlarmCalendar::disabledChanged(const KAEvent* event) { if (event->category() == CalEvent::ACTIVE) { bool status = event->enabled(); checkForDisabledAlarms(!status, status); } } /****************************************************************************** * Check whether there are any individual disabled alarms, following an alarm * creation or modification. Must only be called for an ACTIVE alarm. */ void AlarmCalendar::checkForDisabledAlarms(bool oldEnabled, bool newEnabled) { if (mCalType == RESOURCES && newEnabled != oldEnabled) { if (newEnabled && mHaveDisabledAlarms) checkForDisabledAlarms(); else if (!newEnabled && !mHaveDisabledAlarms) { mHaveDisabledAlarms = true; Q_EMIT haveDisabledAlarmsChanged(true); } } } /****************************************************************************** * Check whether there are any individual disabled alarms. */ void AlarmCalendar::checkForDisabledAlarms() { if (mCalType != RESOURCES) return; bool disabled = false; - KAEvent::List eventlist = events(CalEvent::ACTIVE); - for (int i = 0, end = eventlist.count(); i < end; ++i) + const KAEvent::List eventlist = events(CalEvent::ACTIVE); + for (const KAEvent* const event : eventlist) { - if (!eventlist[i]->enabled()) + if (!event->enabled()) { disabled = true; break; } } if (disabled != mHaveDisabledAlarms) { mHaveDisabledAlarms = disabled; Q_EMIT haveDisabledAlarmsChanged(disabled); } } /****************************************************************************** * Return a list of all active at-login alarms. */ KAEvent::List AlarmCalendar::atLoginAlarms() const { KAEvent::List atlogins; if (mCalType != RESOURCES) return atlogins; AkonadiModel* model = AkonadiModel::instance(); if (!mCalendarStorage || mCalType != RESOURCES) return atlogins; for (ResourceMap::ConstIterator rit = mResourceMap.constBegin(); rit != mResourceMap.constEnd(); ++rit) { const Collection::Id id = rit.key(); if (id < 0 || !(AkonadiModel::types(model->collectionById(id)) & CalEvent::ACTIVE)) continue; const KAEvent::List& events = rit.value(); - for (int i = 0, end = events.count(); i < end; ++i) + for (KAEvent* event : events) { - KAEvent* event = events[i]; if (event->category() == CalEvent::ACTIVE && event->repeatAtLogin()) atlogins += event; } } return atlogins; } /****************************************************************************** * Find and note the active alarm with the earliest trigger time for a calendar. */ void AlarmCalendar::findEarliestAlarm(const Akonadi::Collection& collection) { if (mCalType != RESOURCES) return; if (!collection.isValid() || !(AkonadiModel::types(collection) & CalEvent::ACTIVE)) return; findEarliestAlarm(collection.id()); } void AlarmCalendar::findEarliestAlarm(Akonadi::Collection::Id key) { EarliestMap::Iterator eit = mEarliestAlarm.find(key); if (eit != mEarliestAlarm.end()) eit.value() = nullptr; if (mCalType != RESOURCES || key < 0) return; ResourceMap::ConstIterator rit = mResourceMap.constFind(key); if (rit == mResourceMap.constEnd()) return; const KAEvent::List& events = rit.value(); KAEvent* earliest = nullptr; KADateTime earliestTime; - for (int i = 0, end = events.count(); i < end; ++i) + for (KAEvent* event : events) { - KAEvent* event = events[i]; if (event->category() != CalEvent::ACTIVE || mPendingAlarms.contains(event->id())) continue; const KADateTime dt = event->nextTrigger(KAEvent::ALL_TRIGGER).effectiveKDateTime(); if (dt.isValid() && (!earliest || dt < earliestTime)) { earliestTime = dt; earliest = event; } } mEarliestAlarm[key] = earliest; Q_EMIT earliestAlarmChanged(); } /****************************************************************************** * Return the active alarm with the earliest trigger time. * Reply = 0 if none. */ KAEvent* AlarmCalendar::earliestAlarm() const { KAEvent* earliest = nullptr; KADateTime earliestTime; for (EarliestMap::ConstIterator eit = mEarliestAlarm.constBegin(); eit != mEarliestAlarm.constEnd(); ++eit) { KAEvent* event = eit.value(); if (!event) continue; const KADateTime dt = event->nextTrigger(KAEvent::ALL_TRIGGER).effectiveKDateTime(); if (dt.isValid() && (!earliest || dt < earliestTime)) { earliestTime = dt; earliest = event; } } return earliest; } /****************************************************************************** * Note that an alarm which has triggered is now being processed. While pending, * it will be ignored for the purposes of finding the earliest trigger time. */ void AlarmCalendar::setAlarmPending(KAEvent* event, bool pending) { - QString id = event->id(); + const QString id = event->id(); bool wasPending = mPendingAlarms.contains(id); qCDebug(KALARM_LOG) << "AlarmCalendar::setAlarmPending:" << id << "," << pending << "(was" << wasPending << ")"; if (pending) { if (wasPending) return; mPendingAlarms.append(id); } else { if (!wasPending) return; mPendingAlarms.removeAll(id); } // Now update the earliest alarm to trigger for its calendar findEarliestAlarm(AkonadiModel::instance()->collection(*event)); } /****************************************************************************** * Called when the user changes the start-of-day time. * Adjust the start times of all date-only alarms' recurrences. */ void AlarmCalendar::adjustStartOfDay() { if (!isValid()) return; for (ResourceMap::ConstIterator rit = mResourceMap.constBegin(); rit != mResourceMap.constEnd(); ++rit) KAEvent::adjustStartOfDay(rit.value()); } /****************************************************************************** * Find the version of KAlarm which wrote the calendar file, and do any * necessary conversions to the current format. */ KACalendar::Compat fix(const FileStorage::Ptr& fileStorage) { QString versionString; int version = KACalendar::updateVersion(fileStorage, versionString); if (version == KACalendar::IncompatibleFormat) return KACalendar::Incompatible; // calendar was created by another program, or an unknown version of KAlarm return KACalendar::Current; } // vim: et sw=4: diff --git a/src/alarmlistview.cpp b/src/alarmlistview.cpp index f90ca46d..5c29a9ba 100644 --- a/src/alarmlistview.cpp +++ b/src/alarmlistview.cpp @@ -1,188 +1,188 @@ /* * alarmlistview.cpp - widget showing list of alarms * Program: kalarm * Copyright © 2007,2008,2010,2019 David Jarvie * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kalarm.h" #include "alarmlistview.h" #include #include #include #include #include #include AlarmListView::AlarmListView(const QByteArray& configGroup, QWidget* parent) - : EventListView(parent), - mConfigGroup(configGroup) + : EventListView(parent) + , mConfigGroup(configGroup) { setEditOnSingleClick(true); connect(header(), &QHeaderView::sectionMoved, this, &AlarmListView::sectionMoved); } void AlarmListView::setModel(QAbstractItemModel* model) { EventListView::setModel(model); KConfigGroup config(KSharedConfig::openConfig(), mConfigGroup.constData()); const QByteArray settings = config.readEntry("ListHead", QByteArray()); if (!settings.isEmpty()) header()->restoreState(settings); header()->setSectionsMovable(true); header()->setStretchLastSection(false); header()->setSectionResizeMode(AlarmListModel::TimeColumn, QHeaderView::ResizeToContents); header()->setSectionResizeMode(AlarmListModel::TimeToColumn, QHeaderView::ResizeToContents); header()->setSectionResizeMode(AlarmListModel::RepeatColumn, QHeaderView::ResizeToContents); header()->setSectionResizeMode(AlarmListModel::ColourColumn, QHeaderView::Fixed); header()->setSectionResizeMode(AlarmListModel::TypeColumn, QHeaderView::Fixed); header()->setSectionResizeMode(AlarmListModel::TextColumn, QHeaderView::Stretch); header()->setStretchLastSection(true); // necessary to ensure ResizeToContents columns do resize to contents! const int minWidth = viewOptions().fontMetrics.lineSpacing() * 3 / 4; header()->setMinimumSectionSize(minWidth); const int margin = QApplication::style()->pixelMetric(QStyle::PM_FocusFrameHMargin); header()->resizeSection(AlarmListModel::ColourColumn, minWidth); header()->resizeSection(AlarmListModel::TypeColumn, AlarmListModel::iconWidth() + 2*margin + 2); header()->setContextMenuPolicy(Qt::CustomContextMenu); connect(header(), &QWidget::customContextMenuRequested, this, &AlarmListView::headerContextMenuRequested); } /****************************************************************************** * Return which of the optional columns are currently shown. * Note that the column order must be the same as in setColumnsVisible(). */ QList AlarmListView::columnsVisible() const { if (!model()) return {}; return { !header()->isSectionHidden(AlarmListModel::TimeColumn), !header()->isSectionHidden(AlarmListModel::TimeToColumn), !header()->isSectionHidden(AlarmListModel::RepeatColumn), !header()->isSectionHidden(AlarmListModel::ColourColumn), !header()->isSectionHidden(AlarmListModel::TypeColumn) }; } /****************************************************************************** * Set which of the optional columns are to be shown. * Note that the column order must be the same as in columnsVisible(). */ void AlarmListView::setColumnsVisible(const QList& show) { if (!model()) return; const QList vis = (show.size() < 5) ? QList{true, false, true, true, true} : show; header()->setSectionHidden(AlarmListModel::TimeColumn, !vis[0]); header()->setSectionHidden(AlarmListModel::TimeToColumn, !vis[1]); header()->setSectionHidden(AlarmListModel::RepeatColumn, !vis[2]); header()->setSectionHidden(AlarmListModel::ColourColumn, !vis[3]); header()->setSectionHidden(AlarmListModel::TypeColumn, !vis[4]); sortByColumn(vis[0] ? AlarmListModel::TimeColumn : AlarmListModel::TimeToColumn, Qt::AscendingOrder); } /****************************************************************************** * Called when the column order is changed. * Save the new order for restoration on program restart. */ void AlarmListView::sectionMoved() { KConfigGroup config(KSharedConfig::openConfig(), mConfigGroup.constData()); config.writeEntry("ListHead", header()->saveState()); config.sync(); } /****************************************************************************** * Called when a context menu is requested for the header. * Allow the user to choose which columns to display. */ void AlarmListView::headerContextMenuRequested(const QPoint& pt) { QAbstractItemModel* almodel = model(); int count = header()->count(); QMenu menu; for (int col = 0; col < count; ++col) { const QString title = almodel->headerData(col, Qt::Horizontal, AkonadiModel::ColumnTitleRole).toString(); if (!title.isEmpty()) { QAction* act = menu.addAction(title); act->setData(col); act->setCheckable(true); act->setChecked(!header()->isSectionHidden(col)); if (col == AlarmListModel::TextColumn) act->setEnabled(false); // don't allow text column to be hidden else QObject::connect(act, &QAction::triggered, this, [this, &menu, act] { showHideColumn(menu, act); }); } } enableTimeColumns(&menu); menu.exec(header()->mapToGlobal(pt)); } /****************************************************************************** * Show or hide a column according to the header context menu. */ void AlarmListView::showHideColumn(QMenu& menu, QAction* act) { int col = act->data().toInt(); if (col < 0 || col >= header()->count()) return; bool show = act->isChecked(); header()->setSectionHidden(col, !show); if (col == AlarmListModel::TimeColumn || col == AlarmListModel::TimeToColumn) enableTimeColumns(&menu); Q_EMIT columnsVisibleChanged(); } /****************************************************************************** * Disable Time or Time To in the context menu if the other one is not * selected to be displayed, to ensure that at least one is always shown. */ void AlarmListView::enableTimeColumns(QMenu* menu) { bool timeShown = !header()->isSectionHidden(AlarmListModel::TimeColumn); bool timeToShown = !header()->isSectionHidden(AlarmListModel::TimeToColumn); - QList actions = menu->actions(); + const QList actions = menu->actions(); if (!timeToShown) { header()->setSectionHidden(AlarmListModel::TimeColumn, false); - for (QAction* act : qAsConst(actions)) + for (QAction* act : actions) { if (act->data().toInt() == AlarmListModel::TimeColumn) { act->setEnabled(false); break; } } } else if (!timeShown) { header()->setSectionHidden(AlarmListModel::TimeToColumn, false); - for (QAction* act : qAsConst(actions)) + for (QAction* act : actions) { if (act->data().toInt() == AlarmListModel::TimeToColumn) { act->setEnabled(false); break; } } } } // vim: et sw=4: diff --git a/src/birthdaymodel.cpp b/src/birthdaymodel.cpp index 43024851..bf64ea6f 100644 --- a/src/birthdaymodel.cpp +++ b/src/birthdaymodel.cpp @@ -1,128 +1,128 @@ /* * birthdaymodel.cpp - model class for birthdays from address book * Program: kalarm * Copyright © 2009 by Tobias Koenig - * Copyright © 2007-2011 by David Jarvie + * Copyright © 2007-2019 David Jarvie * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "birthdaymodel.h" #include "alarmcalendar.h" #include #include #include #include #include #include #include using namespace KAlarmCal; BirthdayModel* BirthdayModel::mInstance = nullptr; BirthdayModel::BirthdayModel(Akonadi::ChangeRecorder* recorder) : Akonadi::ContactsTreeModel(recorder) { - setColumns(Columns() << FullName << Birthday); + setColumns({FullName, Birthday}); } BirthdayModel::~BirthdayModel() { if (this == mInstance) mInstance = nullptr; } BirthdayModel* BirthdayModel::instance() { if (!mInstance) { Akonadi::Session* session = new Akonadi::Session("KAlarm::BirthdayModelSession"); Akonadi::ItemFetchScope scope; scope.fetchFullPayload(true); scope.fetchAttribute(); Akonadi::ChangeRecorder* recorder = new Akonadi::ChangeRecorder; recorder->setSession(session); recorder->fetchCollection(true); recorder->setItemFetchScope(scope); recorder->setCollectionMonitored(Akonadi::Collection::root()); recorder->setMimeTypeMonitored(KContacts::Addressee::mimeType(), true); mInstance = new BirthdayModel(recorder); } return mInstance; } QVariant BirthdayModel::entityData(const Akonadi::Item& item, int column, int role) const { if (columns().at(column) == Birthday && role == Qt::DisplayRole) { - QDate date = Akonadi::ContactsTreeModel::entityData(item, column, DateRole).toDate(); + const QDate date = Akonadi::ContactsTreeModel::entityData(item, column, DateRole).toDate(); if (date.isValid()) return QLocale().toString(date, QLocale::ShortFormat); } return Akonadi::ContactsTreeModel::entityData(item, column, role); } +/*============================================================================*/ BirthdaySortModel::BirthdaySortModel(QObject* parent) : QSortFilterProxyModel(parent) { } void BirthdaySortModel::setPrefixSuffix(const QString& prefix, const QString& suffix) { mContactsWithAlarm.clear(); mPrefix = prefix; mSuffix = suffix; KAEvent event; const KAEvent::List events = AlarmCalendar::resources()->events(CalEvent::ACTIVE); - for (int i = 0, end = events.count(); i < end; ++i) + for (KAEvent* event : events) { - KAEvent* event = events[i]; if (event->actionSubType() == KAEvent::MESSAGE && event->recurType() == KARecurrence::ANNUAL_DATE && (prefix.isEmpty() || event->message().startsWith(prefix))) mContactsWithAlarm.append(event->message()); } invalidateFilter(); } bool BirthdaySortModel::filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const { const QModelIndex nameIndex = sourceModel()->index(sourceRow, 0, sourceParent); const QModelIndex birthdayIndex = sourceModel()->index(sourceRow, 1, sourceParent); // If the birthday is invalid, the second column is empty if (birthdayIndex.data(Qt::DisplayRole).toString().isEmpty()) return false; const QString text = mPrefix + nameIndex.data(Qt::DisplayRole).toString() + mSuffix; if (mContactsWithAlarm.contains(text)) return false; return true; } // vim: et sw=4: diff --git a/src/collectionmodel.cpp b/src/collectionmodel.cpp index 69c9420a..25342c60 100644 --- a/src/collectionmodel.cpp +++ b/src/collectionmodel.cpp @@ -1,1460 +1,1465 @@ /* * collectionmodel.cpp - Akonadi collection models * Program: kalarm * Copyright © 2007-2019 David Jarvie * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "collectionmodel.h" #include "autoqpointer.h" #include "messagebox.h" #include "preferences.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "kalarm_debug.h" using namespace Akonadi; using namespace KAlarmCal; static Collection::Rights writableRights = Collection::CanChangeItem | Collection::CanCreateItem | Collection::CanDeleteItem; /*============================================================================= = Class: CollectionMimeTypeFilterModel = Proxy model to filter AkonadiModel to restrict its contents to Collections, = not Items, containing specified KAlarm content mime types. = It can optionally be restricted to writable and/or enabled Collections. =============================================================================*/ class CollectionMimeTypeFilterModel : public Akonadi::EntityMimeTypeFilterModel { Q_OBJECT public: explicit CollectionMimeTypeFilterModel(QObject* parent = nullptr); void setEventTypeFilter(CalEvent::Type); void setFilterWritable(bool writable); void setFilterEnabled(bool enabled); Akonadi::Collection collection(int row) const; Akonadi::Collection collection(const QModelIndex&) const; QModelIndex collectionIndex(const Akonadi::Collection&) const; protected: bool filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const override; private: CalEvent::Type mAlarmType; // collection content type contained in this model bool mWritableOnly; // only include writable collections in this model bool mEnabledOnly; // only include enabled collections in this model }; CollectionMimeTypeFilterModel::CollectionMimeTypeFilterModel(QObject* parent) - : EntityMimeTypeFilterModel(parent), - mAlarmType(CalEvent::EMPTY), - mWritableOnly(false), - mEnabledOnly(false) + : EntityMimeTypeFilterModel(parent) + , mAlarmType(CalEvent::EMPTY) + , mWritableOnly(false) + , mEnabledOnly(false) { addMimeTypeInclusionFilter(Collection::mimeType()); // select collections, not items setHeaderGroup(EntityTreeModel::CollectionTreeHeaders); setSourceModel(AkonadiModel::instance()); } void CollectionMimeTypeFilterModel::setEventTypeFilter(CalEvent::Type type) { if (type != mAlarmType) { mAlarmType = type; invalidateFilter(); } } void CollectionMimeTypeFilterModel::setFilterWritable(bool writable) { if (writable != mWritableOnly) { mWritableOnly = writable; invalidateFilter(); } } void CollectionMimeTypeFilterModel::setFilterEnabled(bool enabled) { if (enabled != mEnabledOnly) { Q_EMIT layoutAboutToBeChanged(); mEnabledOnly = enabled; invalidateFilter(); Q_EMIT layoutChanged(); } } bool CollectionMimeTypeFilterModel::filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const { if (!EntityMimeTypeFilterModel::filterAcceptsRow(sourceRow, sourceParent)) return false; AkonadiModel* model = AkonadiModel::instance(); const QModelIndex ix = model->index(sourceRow, 0, sourceParent); const Collection collection = model->data(ix, AkonadiModel::CollectionRole).value(); if (collection.remoteId().isEmpty()) return false; // invalidly configured resource if (!AgentManager::self()->instance(collection.resource()).isValid()) return false; if (!mWritableOnly && mAlarmType == CalEvent::EMPTY) return true; if (mWritableOnly && (collection.rights() & writableRights) != writableRights) return false; if (mAlarmType != CalEvent::EMPTY && !collection.contentMimeTypes().contains(CalEvent::mimeType(mAlarmType))) return false; if ((mWritableOnly || mEnabledOnly) && !collection.hasAttribute()) return false; if (mWritableOnly && (!collection.hasAttribute() || collection.attribute()->compatibility() != KACalendar::Current)) return false; if (mEnabledOnly && !collection.attribute()->isEnabled(mAlarmType)) return false; return true; } /****************************************************************************** * Return the collection for a given row. */ Collection CollectionMimeTypeFilterModel::collection(int row) const { return static_cast(sourceModel())->data(mapToSource(index(row, 0)), EntityTreeModel::CollectionRole).value(); } Collection CollectionMimeTypeFilterModel::collection(const QModelIndex& index) const { return static_cast(sourceModel())->data(mapToSource(index), EntityTreeModel::CollectionRole).value(); } QModelIndex CollectionMimeTypeFilterModel::collectionIndex(const Collection& collection) const { return mapFromSource(static_cast(sourceModel())->collectionIndex(collection)); } /*============================================================================= = Class: CollectionListModel = Proxy model converting the AkonadiModel collection tree into a flat list. = The model may be restricted to specified content mime types. = It can optionally be restricted to writable and/or enabled Collections. =============================================================================*/ CollectionListModel::CollectionListModel(QObject* parent) - : KDescendantsProxyModel(parent), - mUseCollectionColour(true) + : KDescendantsProxyModel(parent) + , mUseCollectionColour(true) { setSourceModel(new CollectionMimeTypeFilterModel(this)); setDisplayAncestorData(false); } /****************************************************************************** * Return the collection for a given row. */ Collection CollectionListModel::collection(int row) const { return data(index(row, 0), EntityTreeModel::CollectionRole).value(); } Collection CollectionListModel::collection(const QModelIndex& index) const { return data(index, EntityTreeModel::CollectionRole).value(); } QModelIndex CollectionListModel::collectionIndex(const Collection& collection) const { return mapFromSource(static_cast(sourceModel())->collectionIndex(collection)); } void CollectionListModel::setEventTypeFilter(CalEvent::Type type) { static_cast(sourceModel())->setEventTypeFilter(type); } void CollectionListModel::setFilterWritable(bool writable) { static_cast(sourceModel())->setFilterWritable(writable); } void CollectionListModel::setFilterEnabled(bool enabled) { static_cast(sourceModel())->setFilterEnabled(enabled); } bool CollectionListModel::isDescendantOf(const QModelIndex& ancestor, const QModelIndex& descendant) const { Q_UNUSED(descendant); return !ancestor.isValid(); } /****************************************************************************** * Return the data for a given role, for a specified item. */ QVariant CollectionListModel::data(const QModelIndex& index, int role) const { switch (role) { case Qt::BackgroundRole: if (!mUseCollectionColour) role = AkonadiModel::BaseColourRole; break; default: break; } return KDescendantsProxyModel::data(index, role); } /*============================================================================= = Class: CollectionCheckListModel = Proxy model providing a checkable list of all Collections. A Collection's = checked status is equivalent to whether it is selected or not. = An alarm type is specified, whereby Collections which are enabled for that = alarm type are checked; Collections which do not contain that alarm type, or = which are disabled for that alarm type, are unchecked. =============================================================================*/ CollectionListModel* CollectionCheckListModel::mModel = nullptr; int CollectionCheckListModel::mInstanceCount = 0; CollectionCheckListModel::CollectionCheckListModel(CalEvent::Type type, QObject* parent) - : KCheckableProxyModel(parent), - mAlarmType(type) + : KCheckableProxyModel(parent) + , mAlarmType(type) { ++mInstanceCount; if (!mModel) mModel = new CollectionListModel(nullptr); setSourceModel(mModel); // the source model is NOT filtered by alarm type mSelectionModel = new QItemSelectionModel(mModel); setSelectionModel(mSelectionModel); connect(mSelectionModel, &QItemSelectionModel::selectionChanged, - this, &CollectionCheckListModel::selectionChanged); + this, &CollectionCheckListModel::selectionChanged); connect(mModel, SIGNAL(rowsAboutToBeInserted(QModelIndex,int,int)), SIGNAL(layoutAboutToBeChanged())); - connect(mModel, &QAbstractItemModel::rowsInserted, this, &CollectionCheckListModel::slotRowsInserted); + connect(mModel, &QAbstractItemModel::rowsInserted, + this, &CollectionCheckListModel::slotRowsInserted); // This is probably needed to make CollectionFilterCheckListModel update // (similarly to when rows are inserted). connect(mModel, SIGNAL(rowsAboutToBeRemoved(QModelIndex,int,int)), SIGNAL(layoutAboutToBeChanged())); connect(mModel, SIGNAL(rowsRemoved(QModelIndex,int,int)), SIGNAL(layoutChanged())); connect(AkonadiModel::instance(), &AkonadiModel::collectionStatusChanged, - this, &CollectionCheckListModel::collectionStatusChanged); + this, &CollectionCheckListModel::collectionStatusChanged); // Initialise checked status for all collections. // Note that this is only necessary if the model is recreated after // being deleted. for (int row = 0, count = mModel->rowCount(); row < count; ++row) setSelectionStatus(mModel->collection(row), mModel->index(row, 0)); } CollectionCheckListModel::~CollectionCheckListModel() { if (--mInstanceCount <= 0) { delete mModel; mModel = nullptr; } } /****************************************************************************** * Return the collection for a given row. */ Collection CollectionCheckListModel::collection(int row) const { return mModel->collection(mapToSource(index(row, 0))); } Collection CollectionCheckListModel::collection(const QModelIndex& index) const { return mModel->collection(mapToSource(index)); } /****************************************************************************** * Return model data for one index. */ QVariant CollectionCheckListModel::data(const QModelIndex& index, int role) const { const Collection collection = mModel->collection(index); if (collection.isValid()) { // This is a Collection row switch (role) { case Qt::ForegroundRole: { const QString mimeType = CalEvent::mimeType(mAlarmType); if (collection.contentMimeTypes().contains(mimeType)) return AkonadiModel::foregroundColor(collection, QStringList(mimeType)); break; } case Qt::FontRole: { if (!collection.hasAttribute() || !AkonadiModel::isCompatible(collection)) break; const CollectionAttribute* attr = collection.attribute(); if (!attr->enabled()) break; const QStringList mimeTypes = collection.contentMimeTypes(); if (attr->isStandard(mAlarmType) && mimeTypes.contains(CalEvent::mimeType(mAlarmType))) { // It's the standard collection for a mime type QFont font = qvariant_cast(KCheckableProxyModel::data(index, role)); font.setBold(true); return font; } break; } default: break; } } return KCheckableProxyModel::data(index, role); } /****************************************************************************** * Set model data for one index. * If the change is to disable a collection, check for eligibility and prevent * the change if necessary. */ bool CollectionCheckListModel::setData(const QModelIndex& index, const QVariant& value, int role) { if (role == Qt::CheckStateRole && static_cast(value.toInt()) != Qt::Checked) { // A collection is to be disabled. const Collection collection = mModel->collection(index); if (collection.isValid() && collection.hasAttribute()) { const CollectionAttribute* attr = collection.attribute(); if (attr->isEnabled(mAlarmType)) { QString errmsg; QWidget* messageParent = qobject_cast(QObject::parent()); if (attr->isStandard(mAlarmType) && AkonadiModel::isCompatible(collection)) { // It's the standard collection for some alarm type. if (mAlarmType == CalEvent::ACTIVE) { errmsg = i18nc("@info", "You cannot disable your default active alarm calendar."); } else if (mAlarmType == CalEvent::ARCHIVED && Preferences::archivedKeepDays()) { // Only allow the archived alarms standard collection to be disabled if // we're not saving expired alarms. errmsg = i18nc("@info", "You cannot disable your default archived alarm calendar " "while expired alarms are configured to be kept."); } else if (KAMessageBox::warningContinueCancel(messageParent, i18nc("@info", "Do you really want to disable your default calendar?")) == KMessageBox::Cancel) return false; } if (!errmsg.isEmpty()) { KAMessageBox::sorry(messageParent, errmsg); return false; } } } } return KCheckableProxyModel::setData(index, value, role); } /****************************************************************************** * Called when rows have been inserted into the model. * Select or deselect them according to their enabled status. */ void CollectionCheckListModel::slotRowsInserted(const QModelIndex& parent, int start, int end) { Q_EMIT layoutAboutToBeChanged(); for (int row = start; row <= end; ++row) { const QModelIndex ix = mapToSource(index(row, 0, parent)); const Collection collection = mModel->collection(ix); if (collection.isValid()) setSelectionStatus(collection, ix); } Q_EMIT layoutChanged(); // this is needed to make CollectionFilterCheckListModel update } /****************************************************************************** * Called when the user has ticked/unticked a collection to enable/disable it * (or when the selection changes for any other reason). */ void CollectionCheckListModel::selectionChanged(const QItemSelection& selected, const QItemSelection& deselected) { const QModelIndexList sel = selected.indexes(); for (const QModelIndex& ix : sel) { // Try to enable the collection, but untick it if not possible if (!CollectionControlModel::setEnabled(mModel->collection(ix), mAlarmType, true)) mSelectionModel->select(ix, QItemSelectionModel::Deselect); } const QModelIndexList desel = deselected.indexes(); for (const QModelIndex& ix : desel) CollectionControlModel::setEnabled(mModel->collection(ix), mAlarmType, false); } /****************************************************************************** * Called when a collection parameter or status has changed. * If the collection's alarm types have been reconfigured, ensure that the * model views are updated to reflect this. */ void CollectionCheckListModel::collectionStatusChanged(const Collection& collection, AkonadiModel::Change change, const QVariant&, bool inserted) { if (inserted || !collection.isValid()) return; switch (change) { case AkonadiModel::Enabled: qCDebug(KALARM_LOG) << debugType("collectionStatusChanged").constData() << "Enabled" << collection.id(); break; case AkonadiModel::AlarmTypes: qCDebug(KALARM_LOG) << debugType("collectionStatusChanged").constData() << "AlarmTypes" << collection.id(); break; default: return; } const QModelIndex ix = mModel->collectionIndex(collection); if (ix.isValid()) setSelectionStatus(collection, ix); if (change == AkonadiModel::AlarmTypes) Q_EMIT collectionTypeChange(this); } /****************************************************************************** * Select or deselect an index according to its enabled status. */ void CollectionCheckListModel::setSelectionStatus(const Collection& collection, const QModelIndex& sourceIndex) { const QItemSelectionModel::SelectionFlags sel = (collection.hasAttribute() && collection.attribute()->isEnabled(mAlarmType)) ? QItemSelectionModel::Select : QItemSelectionModel::Deselect; mSelectionModel->select(sourceIndex, sel); } /****************************************************************************** * Return the instance's alarm type, as a string. */ QByteArray CollectionCheckListModel::debugType(const char* func) const { const char* type; switch (mAlarmType) { case CalEvent::ACTIVE: type = "CollectionCheckListModel[Act]::"; break; case CalEvent::ARCHIVED: type = "CollectionCheckListModel[Arch]::"; break; case CalEvent::TEMPLATE: type = "CollectionCheckListModel[Tmpl]::"; break; default: type = "CollectionCheckListModel::"; break; } return QByteArray(type) + func + ":"; } /*============================================================================= = Class: CollectionFilterCheckListModel = Proxy model providing a checkable collection list. The model contains all = alarm types, but returns only one type at any given time. The selected alarm = type may be changed as desired. =============================================================================*/ CollectionFilterCheckListModel::CollectionFilterCheckListModel(QObject* parent) - : QSortFilterProxyModel(parent), - mActiveModel(new CollectionCheckListModel(CalEvent::ACTIVE, this)), - mArchivedModel(new CollectionCheckListModel(CalEvent::ARCHIVED, this)), - mTemplateModel(new CollectionCheckListModel(CalEvent::TEMPLATE, this)), - mAlarmType(CalEvent::EMPTY) + : QSortFilterProxyModel(parent) + , mActiveModel(new CollectionCheckListModel(CalEvent::ACTIVE, this)) + , mArchivedModel(new CollectionCheckListModel(CalEvent::ARCHIVED, this)) + , mTemplateModel(new CollectionCheckListModel(CalEvent::TEMPLATE, this)) + , mAlarmType(CalEvent::EMPTY) { setDynamicSortFilter(true); - connect(mActiveModel, &CollectionCheckListModel::collectionTypeChange, this, &CollectionFilterCheckListModel::collectionTypeChanged); - connect(mArchivedModel, &CollectionCheckListModel::collectionTypeChange, this, &CollectionFilterCheckListModel::collectionTypeChanged); - connect(mTemplateModel, &CollectionCheckListModel::collectionTypeChange, this, &CollectionFilterCheckListModel::collectionTypeChanged); + connect(mActiveModel, &CollectionCheckListModel::collectionTypeChange, + this, &CollectionFilterCheckListModel::collectionTypeChanged); + connect(mArchivedModel, &CollectionCheckListModel::collectionTypeChange, + this, &CollectionFilterCheckListModel::collectionTypeChanged); + connect(mTemplateModel, &CollectionCheckListModel::collectionTypeChange, + this, &CollectionFilterCheckListModel::collectionTypeChanged); } void CollectionFilterCheckListModel::setEventTypeFilter(CalEvent::Type type) { if (type != mAlarmType) { CollectionCheckListModel* newModel; switch (type) { case CalEvent::ACTIVE: newModel = mActiveModel; break; case CalEvent::ARCHIVED: newModel = mArchivedModel; break; case CalEvent::TEMPLATE: newModel = mTemplateModel; break; default: return; } mAlarmType = type; setSourceModel(newModel); invalidate(); } } /****************************************************************************** * Return the collection for a given row. */ Collection CollectionFilterCheckListModel::collection(int row) const { return static_cast(sourceModel())->collection(mapToSource(index(row, 0))); } Collection CollectionFilterCheckListModel::collection(const QModelIndex& index) const { return static_cast(sourceModel())->collection(mapToSource(index)); } QVariant CollectionFilterCheckListModel::data(const QModelIndex& index, int role) const { switch (role) { case Qt::ToolTipRole: { const Collection col = collection(index); if (col.isValid()) return AkonadiModel::instance()->tooltip(col, mAlarmType); break; } default: break; } return QSortFilterProxyModel::data(index, role); } bool CollectionFilterCheckListModel::filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const { if (mAlarmType == CalEvent::EMPTY) return true; const CollectionCheckListModel* model = static_cast(sourceModel()); const Collection collection = model->collection(model->index(sourceRow, 0, sourceParent)); return collection.contentMimeTypes().contains(CalEvent::mimeType(mAlarmType)); } /****************************************************************************** * Called when a collection alarm type has changed. * Ensure that the collection is removed from or added to the current model view. */ void CollectionFilterCheckListModel::collectionTypeChanged(CollectionCheckListModel* model) { if (model == sourceModel()) invalidateFilter(); } /*============================================================================= = Class: CollectionView = View displaying a list of collections. =============================================================================*/ CollectionView::CollectionView(CollectionFilterCheckListModel* model, QWidget* parent) : QListView(parent) { setModel(model); } void CollectionView::setModel(QAbstractItemModel* model) { QListView::setModel(model); } /****************************************************************************** * Return the collection for a given row. */ Collection CollectionView::collection(int row) const { return static_cast(model())->collection(row); } Collection CollectionView::collection(const QModelIndex& index) const { return static_cast(model())->collection(index); } /****************************************************************************** * Called when a mouse button is released. * Any currently selected collection is deselected. */ void CollectionView::mouseReleaseEvent(QMouseEvent* e) { if (!indexAt(e->pos()).isValid()) clearSelection(); QListView::mouseReleaseEvent(e); } /****************************************************************************** * Called when a ToolTip or WhatsThis event occurs. */ bool CollectionView::viewportEvent(QEvent* e) { if (e->type() == QEvent::ToolTip && isActiveWindow()) { const QHelpEvent* he = static_cast(e); const QModelIndex index = indexAt(he->pos()); QVariant value = static_cast(model())->data(index, Qt::ToolTipRole); if (value.canConvert()) { QString toolTip = value.toString(); int i = toolTip.indexOf(QLatin1Char('@')); if (i > 0) { - int j = toolTip.indexOf(QRegExp(QLatin1String("<(nl|br)"), Qt::CaseInsensitive), i + 1); - int k = toolTip.indexOf(QLatin1Char('@'), j); + const int j = toolTip.indexOf(QRegExp(QLatin1String("<(nl|br)"), Qt::CaseInsensitive), i + 1); + const int k = toolTip.indexOf(QLatin1Char('@'), j); const QString name = toolTip.mid(i + 1, j - i - 1); value = model()->data(index, Qt::FontRole); const QFontMetrics fm(qvariant_cast(value).resolve(viewOptions().font)); int textWidth = fm.boundingRect(name).width() + 1; const int margin = QApplication::style()->pixelMetric(QStyle::PM_FocusFrameHMargin) + 1; QStyleOptionButton opt; opt.QStyleOption::operator=(viewOptions()); opt.rect = rectForIndex(index); - int checkWidth = QApplication::style()->subElementRect(QStyle::SE_ItemViewItemCheckIndicator, &opt).width(); - int left = spacing() + 3*margin + checkWidth + viewOptions().decorationSize.width(); // left offset of text - int right = left + textWidth; + const int checkWidth = QApplication::style()->subElementRect(QStyle::SE_ItemViewItemCheckIndicator, &opt).width(); + const int left = spacing() + 3*margin + checkWidth + viewOptions().decorationSize.width(); // left offset of text + const int right = left + textWidth; if (left >= horizontalOffset() + spacing() && right <= horizontalOffset() + width() - spacing() - 2*frameWidth()) { // The whole of the collection name is already displayed, // so omit it from the tooltip. if (k > 0) toolTip.remove(i, k + 1 - i); } else { toolTip.remove(k, 1); toolTip.remove(i, 1); } } QToolTip::showText(he->globalPos(), toolTip, this); return true; } } return QListView::viewportEvent(e); } /*============================================================================= = Class: CollectionControlModel = Proxy model to select which Collections will be enabled. Disabled Collections = are not populated or monitored; their contents are ignored. The set of = enabled Collections is stored in the config file's "Collections" group. = Note that this model is not used directly for displaying - its purpose is to = allow collections to be disabled, which will remove them from the other = collection models. =============================================================================*/ CollectionControlModel* CollectionControlModel::mInstance = nullptr; bool CollectionControlModel::mAskDestination = false; QHash CollectionControlModel::mAgentPaths; static QRegularExpression matchMimeType(QStringLiteral("^application/x-vnd\\.kde\\.alarm.*"), QRegularExpression::DotMatchesEverythingOption); CollectionControlModel* CollectionControlModel::instance() { if (!mInstance) mInstance = new CollectionControlModel(qApp); return mInstance; } CollectionControlModel::CollectionControlModel(QObject* parent) - : FavoriteCollectionsModel(AkonadiModel::instance(), KConfigGroup(KSharedConfig::openConfig(), "Collections"), parent), - mPopulatedCheckLoop(nullptr) + : FavoriteCollectionsModel(AkonadiModel::instance(), KConfigGroup(KSharedConfig::openConfig(), "Collections"), parent) + , mPopulatedCheckLoop(nullptr) { // Initialise the list of enabled collections EntityMimeTypeFilterModel* filter = new EntityMimeTypeFilterModel(this); filter->addMimeTypeInclusionFilter(Collection::mimeType()); filter->setSourceModel(AkonadiModel::instance()); QList collectionIds; findEnabledCollections(filter, QModelIndex(), collectionIds); setCollections(Collection::List()); for (Collection::Id id : qAsConst(collectionIds)) addCollection(Collection(id)); connect(AkonadiModel::instance(), &AkonadiModel::collectionStatusChanged, - this, &CollectionControlModel::statusChanged); + this, &CollectionControlModel::statusChanged); connect(AkonadiModel::instance(), &EntityTreeModel::collectionTreeFetched, - this, &CollectionControlModel::collectionPopulated); + this, &CollectionControlModel::collectionPopulated); connect(AkonadiModel::instance(), &EntityTreeModel::collectionPopulated, - this, &CollectionControlModel::collectionPopulated); - connect(AkonadiModel::instance(), SIGNAL(serverStopped()), SLOT(reset())); + this, &CollectionControlModel::collectionPopulated); + connect(AkonadiModel::instance(), &AkonadiModel::serverStopped, + this, &CollectionControlModel::reset); } /****************************************************************************** * Recursive function to check all collections' enabled status, and to compile a * list of all collections which have any alarm types enabled. * Collections which duplicate the same backend storage are filtered out, to * avoid crashes due to duplicate events in different resources. */ void CollectionControlModel::findEnabledCollections(const EntityMimeTypeFilterModel* filter, const QModelIndex& parent, QList& collectionIds) const { AkonadiModel* model = AkonadiModel::instance(); for (int row = 0, count = filter->rowCount(parent); row < count; ++row) { const QModelIndex ix = filter->index(row, 0, parent); const Collection collection = model->data(filter->mapToSource(ix), AkonadiModel::CollectionRole).value(); if (!AgentManager::self()->instance(collection.resource()).isValid()) continue; // the collection doesn't belong to a resource, so omit it const CalEvent::Types enabled = !collection.hasAttribute() ? CalEvent::EMPTY : collection.attribute()->enabled(); const CalEvent::Types canEnable = checkTypesToEnable(collection, collectionIds, enabled); if (canEnable != enabled) { // There is another collection which uses the same backend // storage. Disable alarm types enabled in the other collection. if (!model->isCollectionBeingDeleted(collection.id())) model->setData(model->collectionIndex(collection), static_cast(canEnable), AkonadiModel::EnabledTypesRole); } if (canEnable) collectionIds += collection.id(); if (filter->rowCount(ix) > 0) findEnabledCollections(filter, ix, collectionIds); } } /****************************************************************************** * Return whether a collection is enabled for a given alarm type. */ bool CollectionControlModel::isEnabled(const Collection& collection, CalEvent::Type type) { if (!collection.isValid() || !instance()->collectionIds().contains(collection.id())) return false; if (!AgentManager::self()->instance(collection.resource()).isValid()) { // The collection doesn't belong to a resource, so it can't be used. // Remove it from the list of collections. instance()->removeCollection(collection); return false; } Collection col = collection; AkonadiModel::instance()->refresh(col); // update with latest data return col.hasAttribute() && col.attribute()->isEnabled(type); } /****************************************************************************** * Enable or disable the specified alarm types for a collection. * Reply = alarm types which can be enabled */ CalEvent::Types CollectionControlModel::setEnabled(const Collection& collection, CalEvent::Types types, bool enabled) { qCDebug(KALARM_LOG) << "CollectionControlModel::setEnabled:" << collection.id() << ", alarm types" << types << "->" << enabled; if (!collection.isValid() || (!enabled && !instance()->collectionIds().contains(collection.id()))) return CalEvent::EMPTY; Collection col = collection; AkonadiModel::instance()->refresh(col); // update with latest data CalEvent::Types alarmTypes = !col.hasAttribute() ? CalEvent::EMPTY : col.attribute()->enabled(); if (enabled) alarmTypes |= static_cast(types & (CalEvent::ACTIVE | CalEvent::ARCHIVED | CalEvent::TEMPLATE)); else alarmTypes &= ~types; return instance()->setEnabledStatus(collection, alarmTypes, false); } /****************************************************************************** * Change the collection's enabled status. * Add or remove the collection to/from the enabled list. * Reply = alarm types which can be enabled */ CalEvent::Types CollectionControlModel::setEnabledStatus(const Collection& collection, CalEvent::Types types, bool inserted) { qCDebug(KALARM_LOG) << "CollectionControlModel::setEnabledStatus:" << collection.id() << ", types=" << types; CalEvent::Types disallowedStdTypes{}; CalEvent::Types stdTypes{}; // Prevent the enabling of duplicate alarm types if another collection // uses the same backend storage. const QList colIds = collectionIds(); const CalEvent::Types canEnable = checkTypesToEnable(collection, colIds, types); // Update the list of enabled collections if (canEnable) { if (!colIds.contains(collection.id())) { // It's a new collection. // Prevent duplicate standard collections being created for any alarm type. stdTypes = collection.hasAttribute() ? collection.attribute()->standard() : CalEvent::EMPTY; if (stdTypes) { for (const Collection::Id& id : colIds) { Collection c(id); AkonadiModel::instance()->refresh(c); // update with latest data if (c.isValid()) { const CalEvent::Types t = stdTypes & CalEvent::types(c.contentMimeTypes()); if (t) { if (c.hasAttribute() && AkonadiModel::isCompatible(c)) { disallowedStdTypes |= c.attribute()->standard() & t; if (disallowedStdTypes == stdTypes) break; } } } } } addCollection(collection); } } else removeCollection(collection); if (disallowedStdTypes || !inserted || canEnable != types) { // Update the collection's status AkonadiModel* model = static_cast(sourceModel()); if (!model->isCollectionBeingDeleted(collection.id())) { const QModelIndex ix = model->collectionIndex(collection); if (!inserted || canEnable != types) model->setData(ix, static_cast(canEnable), AkonadiModel::EnabledTypesRole); if (disallowedStdTypes) model->setData(ix, static_cast(stdTypes & ~disallowedStdTypes), AkonadiModel::IsStandardRole); } } return canEnable; } /****************************************************************************** * Called when a collection parameter or status has changed. * If it's the enabled status, add or remove the collection to/from the enabled * list. */ void CollectionControlModel::statusChanged(const Collection& collection, AkonadiModel::Change change, const QVariant& value, bool inserted) { if (!collection.isValid()) return; switch (change) { case AkonadiModel::Enabled: { const CalEvent::Types enabled = static_cast(value.toInt()); qCDebug(KALARM_LOG) << "CollectionControlModel::statusChanged:" << collection.id() << ", enabled=" << enabled << ", inserted=" << inserted; setEnabledStatus(collection, enabled, inserted); break; } case AkonadiModel::ReadOnly: { bool readOnly = value.toBool(); qCDebug(KALARM_LOG) << "CollectionControlModel::statusChanged:" << collection.id() << ", readOnly=" << readOnly; if (readOnly) { // A read-only collection can't be the default for any alarm type const CalEvent::Types std = standardTypes(collection, false); if (std != CalEvent::EMPTY) { Collection c(collection); setStandard(c, CalEvent::Types(CalEvent::EMPTY)); QWidget* messageParent = qobject_cast(QObject::parent()); bool singleType = true; QString msg; switch (std) { case CalEvent::ACTIVE: msg = xi18nc("@info", "The calendar %1 has been made read-only. " "This was the default calendar for active alarms.", collection.name()); break; case CalEvent::ARCHIVED: msg = xi18nc("@info", "The calendar %1 has been made read-only. " "This was the default calendar for archived alarms.", collection.name()); break; case CalEvent::TEMPLATE: msg = xi18nc("@info", "The calendar %1 has been made read-only. " "This was the default calendar for alarm templates.", collection.name()); break; default: msg = xi18nc("@info", "The calendar %1 has been made read-only. " "This was the default calendar for:%2" "Please select new default calendars.", collection.name(), typeListForDisplay(std)); singleType = false; break; } if (singleType) msg = xi18nc("@info", "%1Please select a new default calendar.", msg); KAMessageBox::information(messageParent, msg); } } break; } default: break; } } /****************************************************************************** * Check which alarm types can be enabled for a specified collection. * If the collection uses the same backend storage as another collection, any * alarm types already enabled in the other collection must be disabled in this * collection. This is to avoid duplicating events between different resources, * which causes user confusion and annoyance, and causes crashes. * Parameters: * collection - must be up to date (using AkonadiModel::refresh() etc.) * collectionIds = list of collection IDs to search for duplicates. * types = alarm types to be enabled for the collection. * Reply = alarm types which can be enabled without duplicating other collections. */ CalEvent::Types CollectionControlModel::checkTypesToEnable(const Collection& collection, const QList& collectionIds, CalEvent::Types types) { types &= (CalEvent::ACTIVE | CalEvent::ARCHIVED | CalEvent::TEMPLATE); if (types) { // At least one alarm type is to be enabled const QUrl location = QUrl::fromUserInput(collection.remoteId(), QString(), QUrl::AssumeLocalFile); for (const Collection::Id& id : collectionIds) { const Collection c(id); const QUrl cLocation = QUrl::fromUserInput(c.remoteId(), QString(), QUrl::AssumeLocalFile); if (id != collection.id() && cLocation == location) { // The collection duplicates the backend storage // used by another enabled collection. // N.B. don't refresh this collection - assume no change. qCDebug(KALARM_LOG) << "CollectionControlModel::checkTypesToEnable:" << c.id() << "duplicates backend for" << collection.id(); if (c.hasAttribute()) { types &= ~c.attribute()->enabled(); if (!types) break; } } } } return types; } /****************************************************************************** * Create a bulleted list of alarm types for insertion into .... */ QString CollectionControlModel::typeListForDisplay(CalEvent::Types alarmTypes) { QString list; if (alarmTypes & CalEvent::ACTIVE) list += QLatin1String("") + i18nc("@info", "Active Alarms") + QLatin1String(""); if (alarmTypes & CalEvent::ARCHIVED) list += QLatin1String("") + i18nc("@info", "Archived Alarms") + QLatin1String(""); if (alarmTypes & CalEvent::TEMPLATE) list += QLatin1String("") + i18nc("@info", "Alarm Templates") + QLatin1String(""); if (!list.isEmpty()) list = QStringLiteral("") + list + QStringLiteral(""); return list; } /****************************************************************************** * Return whether a collection is both enabled and fully writable for a given * alarm type. * Optionally, the enabled status can be ignored. * Reply: 1 = fully enabled and writable, * 0 = enabled and writable except that backend calendar is in an old KAlarm format, * -1 = not enabled, read-only, or incompatible format. */ int CollectionControlModel::isWritableEnabled(const Akonadi::Collection& collection, CalEvent::Type type) { KACalendar::Compat format; return isWritableEnabled(collection, type, format); } int CollectionControlModel::isWritableEnabled(const Akonadi::Collection& collection, CalEvent::Type type, KACalendar::Compat& format) { - int writable = AkonadiModel::isWritable(collection, format); + const int writable = AkonadiModel::isWritable(collection, format); if (writable == -1) return -1; // Check the collection's enabled status if (!instance()->collectionIds().contains(collection.id()) || !collection.hasAttribute()) return -1; if (!collection.attribute()->isEnabled(type)) return -1; return writable; } /****************************************************************************** * Return the standard collection for a specified mime type. * If the mime type is 'archived' and there is no standard collection, the only * writable archived collection is set to be the standard. */ Collection CollectionControlModel::getStandard(CalEvent::Type type) { const QString mimeType = CalEvent::mimeType(type); int defaultArch = -1; const QList colIds = instance()->collectionIds(); Collection::List cols; for (int i = 0, count = colIds.count(); i < count; ++i) { cols.append(Collection(colIds[i])); Collection& col = cols.last(); AkonadiModel::instance()->refresh(col); // update with latest data if (col.isValid() && col.contentMimeTypes().contains(mimeType)) { if (col.hasAttribute() && (col.attribute()->standard() & type) && AkonadiModel::isCompatible(col)) return col; if (type == CalEvent::ARCHIVED && ((col.rights() & writableRights) == writableRights)) defaultArch = (defaultArch == -1) ? i : -2; } } if (defaultArch >= 0) { // There is no standard collection for archived alarms, but there is // only one writable collection for the type. Set the collection to be // the standard. setStandard(cols[defaultArch], type, true); return cols[defaultArch]; } return Collection(); } /****************************************************************************** * Return whether a collection is the standard collection for a specified * mime type. */ bool CollectionControlModel::isStandard(Akonadi::Collection& collection, CalEvent::Type type) { // If it's for archived alarms, set the standard collection if necessary. if (type == CalEvent::ARCHIVED) return getStandard(type) == collection; if (!instance()->collectionIds().contains(collection.id())) return false; AkonadiModel::instance()->refresh(collection); // update with latest data if (!collection.hasAttribute() || !AkonadiModel::isCompatible(collection)) return false; return collection.attribute()->isStandard(type); } /****************************************************************************** * Return the alarm type(s) for which a collection is the standard collection. */ CalEvent::Types CollectionControlModel::standardTypes(const Collection& collection, bool useDefault) { if (!instance()->collectionIds().contains(collection.id())) return CalEvent::EMPTY; Collection col = collection; AkonadiModel::instance()->refresh(col); // update with latest data if (!AkonadiModel::isCompatible(col)) return CalEvent::EMPTY; CalEvent::Types stdTypes = col.hasAttribute() ? col.attribute()->standard() : CalEvent::EMPTY; if (useDefault) { // Also return alarm types for which this is the only collection. CalEvent::Types wantedTypes = AkonadiModel::types(collection) & ~stdTypes; const QList colIds = instance()->collectionIds(); for (int i = 0, count = colIds.count(); wantedTypes && i < count; ++i) { if (colIds[i] == col.id()) continue; Collection c(colIds[i]); AkonadiModel::instance()->refresh(c); // update with latest data if (c.isValid()) wantedTypes &= ~AkonadiModel::types(c); } stdTypes |= wantedTypes; } return stdTypes; } /****************************************************************************** * Set or clear a collection as the standard collection for a specified mime * type. If it is being set as standard, the standard status for the mime type * is cleared for all other collections. */ void CollectionControlModel::setStandard(Akonadi::Collection& collection, CalEvent::Type type, bool standard) { AkonadiModel* model = AkonadiModel::instance(); model->refresh(collection); // update with latest data if (!AkonadiModel::isCompatible(collection)) standard = false; // the collection isn't writable if (standard) { // The collection is being set as standard. // Clear the 'standard' status for all other collections. const QList colIds = instance()->collectionIds(); if (!colIds.contains(collection.id())) return; const CalEvent::Types ctypes = collection.hasAttribute() ? collection.attribute()->standard() : CalEvent::EMPTY; if (ctypes & type) return; // it's already the standard collection for this type - for (int i = 0, count = colIds.count(); i < count; ++i) + for (Collection::Id colId : colIds) { CalEvent::Types types; - Collection c(colIds[i]); - if (colIds[i] == collection.id()) + Collection c(colId); + if (colId == collection.id()) { c = collection; // update with latest data types = ctypes | type; } else { model->refresh(c); // update with latest data types = c.hasAttribute() ? c.attribute()->standard() : CalEvent::EMPTY; if (!(types & type)) continue; types &= ~type; } const QModelIndex index = model->collectionIndex(c); model->setData(index, static_cast(types), AkonadiModel::IsStandardRole); } } else { // The 'standard' status is being cleared for the collection. // The collection doesn't have to be in this model's list of collections. CalEvent::Types types = collection.hasAttribute() ? collection.attribute()->standard() : CalEvent::EMPTY; if (types & type) { types &= ~type; const QModelIndex index = model->collectionIndex(collection); model->setData(index, static_cast(types), AkonadiModel::IsStandardRole); } } } /****************************************************************************** * Set which mime types a collection is the standard collection for. * If it is being set as standard for any mime types, the standard status for * those mime types is cleared for all other collections. */ void CollectionControlModel::setStandard(Akonadi::Collection& collection, CalEvent::Types types) { AkonadiModel* model = AkonadiModel::instance(); model->refresh(collection); // update with latest data if (!AkonadiModel::isCompatible(collection)) types = CalEvent::EMPTY; // the collection isn't writable if (types) { // The collection is being set as standard for at least one mime type. // Clear the 'standard' status for all other collections. const QList colIds = instance()->collectionIds(); if (!colIds.contains(collection.id())) return; const CalEvent::Types t = collection.hasAttribute() ? collection.attribute()->standard() : CalEvent::EMPTY; if (t == types) return; // there's no change to the collection's status - for (int i = 0, count = colIds.count(); i < count; ++i) + for (Collection::Id colId : colIds) { CalEvent::Types t; - Collection c(colIds[i]); - if (colIds[i] == collection.id()) + Collection c(colId); + if (colId == collection.id()) { c = collection; // update with latest data t = types; } else { model->refresh(c); // update with latest data t = c.hasAttribute() ? c.attribute()->standard() : CalEvent::EMPTY; if (!(t & types)) continue; t &= ~types; } const QModelIndex index = model->collectionIndex(c); model->setData(index, static_cast(t), AkonadiModel::IsStandardRole); } } else { // The 'standard' status is being cleared for the collection. // The collection doesn't have to be in this model's list of collections. if (collection.hasAttribute() && collection.attribute()->standard()) { const QModelIndex index = model->collectionIndex(collection); model->setData(index, static_cast(types), AkonadiModel::IsStandardRole); } } } /****************************************************************************** * Get the collection to use for storing an alarm. * Optionally, the standard collection for the alarm type is returned. If more * than one collection is a candidate, the user is prompted. */ Collection CollectionControlModel::destination(CalEvent::Type type, QWidget* promptParent, bool noPrompt, bool* cancelled) { if (cancelled) *cancelled = false; Collection standard; if (type == CalEvent::EMPTY) return standard; standard = getStandard(type); // Archived alarms are always saved in the default resource, // else only prompt if necessary. if (type == CalEvent::ARCHIVED || noPrompt || (!mAskDestination && standard.isValid())) return standard; // Prompt for which collection to use CollectionListModel* model = new CollectionListModel(promptParent); model->setFilterWritable(true); model->setFilterEnabled(true); model->setEventTypeFilter(type); model->useCollectionColour(false); Collection col; switch (model->rowCount()) { case 0: break; case 1: col = model->collection(0); break; default: { // Use AutoQPointer to guard against crash on application exit while // the dialogue is still open. It prevents double deletion (both on // deletion of 'promptParent', and on return from this function). AutoQPointer dlg = new CollectionDialog(model, promptParent); dlg->setWindowTitle(i18nc("@title:window", "Choose Calendar")); dlg->setDefaultCollection(standard); dlg->setMimeTypeFilter(QStringList(CalEvent::mimeType(type))); if (dlg->exec()) col = dlg->selectedCollection(); if (!col.isValid() && cancelled) *cancelled = true; } } return col; } /****************************************************************************** * Return all collections which belong to a resource and which optionally * contain a specified mime type. */ Collection::List CollectionControlModel::allCollections(CalEvent::Type type) { const bool allTypes = (type == CalEvent::EMPTY); const QString mimeType = CalEvent::mimeType(type); AgentManager* agentManager = AgentManager::self(); Collection::List result; const QList colIds = instance()->collectionIds(); - for (Collection::Id id : colIds) + for (Collection::Id colId : colIds) { - Collection c(id); + Collection c(colId); AkonadiModel::instance()->refresh(c); // update with latest data if ((allTypes || c.contentMimeTypes().contains(mimeType)) && agentManager->instance(c.resource()).isValid()) result += c; } return result; } /****************************************************************************** * Return the enabled collections which contain a specified mime type. * If 'writable' is true, only writable collections are included. */ Collection::List CollectionControlModel::enabledCollections(CalEvent::Type type, bool writable) { const QString mimeType = CalEvent::mimeType(type); const QList colIds = instance()->collectionIds(); Collection::List result; - for (int i = 0, count = colIds.count(); i < count; ++i) + for (Collection::Id colId : colIds) { - Collection c(colIds[i]); + Collection c(colId); AkonadiModel::instance()->refresh(c); // update with latest data if (c.contentMimeTypes().contains(mimeType) && (!writable || ((c.rights() & writableRights) == writableRights))) result += c; } return result; } /****************************************************************************** * Return the collection ID for a given resource ID. */ Collection CollectionControlModel::collectionForResource(const QString& resourceId) { const QList colIds = instance()->collectionIds(); - for (Collection::Id id : colIds) + for (Collection::Id colId : colIds) { - Collection c(id); + const Collection c(colId); if (c.resource() == resourceId) return c; } return Collection(); } /****************************************************************************** * Return whether all enabled collections have been populated. */ -bool CollectionControlModel::isPopulated(Collection::Id colId) +bool CollectionControlModel::isPopulated(Collection::Id collectionId) { AkonadiModel* model = AkonadiModel::instance(); const QList colIds = instance()->collectionIds(); - for (int i = 0, count = colIds.count(); i < count; ++i) + for (Collection::Id colId : colIds) { - if ((colId == -1 || colId == colIds[i]) - && !model->data(model->collectionIndex(colIds[i]), AkonadiModel::IsPopulatedRole).toBool()) + if ((collectionId == -1 || collectionId == colId) + && !model->data(model->collectionIndex(colId), AkonadiModel::IsPopulatedRole).toBool()) { - Collection c(colIds[i]); + Collection c(colId); model->refresh(c); // update with latest data if (!c.hasAttribute() || c.attribute()->enabled() == CalEvent::EMPTY) return false; } } return true; } /****************************************************************************** * Wait for one or all enabled collections to be populated. * Reply = true if successful. */ bool CollectionControlModel::waitUntilPopulated(Collection::Id colId, int timeout) { qCDebug(KALARM_LOG) << "CollectionControlModel::waitUntilPopulated"; int result = 1; AkonadiModel* model = AkonadiModel::instance(); while (!model->isCollectionTreeFetched() || !isPopulated(colId)) { if (!mPopulatedCheckLoop) mPopulatedCheckLoop = new QEventLoop(this); if (timeout > 0) QTimer::singleShot(timeout * 1000, mPopulatedCheckLoop, &QEventLoop::quit); result = mPopulatedCheckLoop->exec(); } delete mPopulatedCheckLoop; mPopulatedCheckLoop = nullptr; return result; } /****************************************************************************** * Check for, and remove, any Akonadi resources which duplicate use of calendar * files/directories. */ void CollectionControlModel::removeDuplicateResources() { mAgentPaths.clear(); const AgentInstance::List agents = AgentManager::self()->instances(); for (const AgentInstance& agent : agents) { if (agent.type().mimeTypes().indexOf(matchMimeType) >= 0) { CollectionFetchJob* job = new CollectionFetchJob(Collection::root(), CollectionFetchJob::Recursive); job->fetchScope().setResource(agent.identifier()); connect(job, &CollectionFetchJob::result, instance(), &CollectionControlModel::collectionFetchResult); } } } /****************************************************************************** * Called when a CollectionFetchJob has completed. */ void CollectionControlModel::collectionFetchResult(KJob* j) { CollectionFetchJob* job = qobject_cast(j); if (j->error()) qCCritical(KALARM_LOG) << "CollectionControlModel::collectionFetchResult: CollectionFetchJob" << job->fetchScope().resource()<< "error: " << j->errorString(); else { AgentManager* agentManager = AgentManager::self(); const Collection::List collections = job->collections(); for (const Collection& c : collections) { if (c.contentMimeTypes().indexOf(matchMimeType) >= 0) { ResourceCol thisRes(job->fetchScope().resource(), c.id()); auto it = mAgentPaths.constFind(c.remoteId()); if (it != mAgentPaths.constEnd()) { // Remove the resource containing the higher numbered Collection // ID, which is likely to be the more recently created. - ResourceCol prevRes = it.value(); + const ResourceCol prevRes = it.value(); if (thisRes.collectionId > prevRes.collectionId) { qCWarning(KALARM_LOG) << "CollectionControlModel::collectionFetchResult: Removing duplicate resource" << thisRes.resourceId; agentManager->removeInstance(agentManager->instance(thisRes.resourceId)); continue; } qCWarning(KALARM_LOG) << "CollectionControlModel::collectionFetchResult: Removing duplicate resource" << prevRes.resourceId; agentManager->removeInstance(agentManager->instance(prevRes.resourceId)); } mAgentPaths[c.remoteId()] = thisRes; } } } } /****************************************************************************** * Called when the Akonadi server has stopped. Reset the model. */ void CollectionControlModel::reset() { delete mPopulatedCheckLoop; mPopulatedCheckLoop = nullptr; // Clear the collections list. This is required because addCollection() or // setCollections() don't work if the collections which they specify are // already in the list. setCollections(Collection::List()); } /****************************************************************************** * Exit from the populated event loop when a collection has been populated. */ void CollectionControlModel::collectionPopulated() { if (mPopulatedCheckLoop) mPopulatedCheckLoop->exit(1); } /****************************************************************************** * Return the data for a given role, for a specified item. */ QVariant CollectionControlModel::data(const QModelIndex& index, int role) const { return sourceModel()->data(mapToSource(index), role); } #include "collectionmodel.moc" // vim: et sw=4: diff --git a/src/commandoptions.cpp b/src/commandoptions.cpp index 8f96b9a5..0285c7d5 100644 --- a/src/commandoptions.cpp +++ b/src/commandoptions.cpp @@ -1,874 +1,876 @@ /* * commandoptions.cpp - extract command line options * 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 "commandoptions.h" #include "alarmtime.h" #include "kalarmapp.h" #include "kamail.h" #include "kalarm_debug.h" #include #include #include #include #include namespace { bool convInterval(const QString& timeParam, KARecurrence::Type&, int& timeInterval, bool allowMonthYear = false); } CommandOptions* CommandOptions::mFirstInstance = nullptr; CommandOptions::CommandOptions() - : mParser(nullptr), - mOptions(Num_Options, nullptr), - mCommand(NONE), - mEditActionSet(false), - mRecurrence(nullptr), - mRepeatCount(0), - mRepeatInterval(0), - mLateCancel(0), - mBgColour(Preferences::defaultBgColour()), - mFgColour(Preferences::defaultFgColour()), - mReminderMinutes(0), - mAudioVolume(-1), - mFromID(0), - mFlags(KAEvent::DEFAULT_FONT), - mDisableAll(false) + : mParser(nullptr) + , mOptions(Num_Options, nullptr) + , mCommand(NONE) + , mEditActionSet(false) + , mRecurrence(nullptr) + , mRepeatCount(0) + , mRepeatInterval(0) + , mLateCancel(0) + , mBgColour(Preferences::defaultBgColour()) + , mFgColour(Preferences::defaultFgColour()) + , mReminderMinutes(0) + , mAudioVolume(-1) + , mFromID(0) + , mFlags(KAEvent::DEFAULT_FONT) + , mDisableAll(false) { if (!mFirstInstance) mFirstInstance = this; } /****************************************************************************** * Set the command line options for the parser, and remove any arguments * following --exec or --exec-display (since the parser can't handle this). * Reply = command line arguments for parser. */ QStringList CommandOptions::setOptions(QCommandLineParser* parser, const QStringList& args) { mParser = parser; mOptions[ACK_CONFIRM] = new QCommandLineOption(QStringList{QStringLiteral("a"), QStringLiteral("ack-confirm")}, i18n("Prompt for confirmation when alarm is acknowledged")); mOptions[ATTACH] = new QCommandLineOption(QStringList{QStringLiteral("A"), QStringLiteral("attach")}, i18n("Attach file to email (repeat as needed)"), QStringLiteral("url")); mOptions[AUTO_CLOSE] = new QCommandLineOption(QStringLiteral("auto-close"), i18n("Auto-close alarm window after --late-cancel period")); mOptions[BCC] = new QCommandLineOption(QStringLiteral("bcc"), i18n("Blind copy email to self")); mOptions[BEEP] = new QCommandLineOption(QStringList{QStringLiteral("b"), QStringLiteral("beep")}, i18n("Beep when message is displayed")); mOptions[COLOUR] = new QCommandLineOption(QStringList{QStringLiteral("c"), QStringLiteral("colour"), QStringLiteral("color")}, i18n("Message background color (name or hex 0xRRGGBB)"), QStringLiteral("color")); mOptions[COLOURFG] = new QCommandLineOption(QStringList{QStringLiteral("C"), QStringLiteral("colourfg"), QStringLiteral("colorfg")}, i18n("Message foreground color (name or hex 0xRRGGBB)"), QStringLiteral("color")); mOptions[OptCANCEL_EVENT] = new QCommandLineOption(QStringLiteral("cancelEvent"), i18n("Cancel alarm with the specified event ID"), QStringLiteral("eventID")); mOptions[DISABLE] = new QCommandLineOption(QStringList{QStringLiteral("d"), QStringLiteral("disable")}, i18n("Disable the alarm")); mOptions[DISABLE_ALL] = new QCommandLineOption(QStringLiteral("disable-all"), i18n("Disable monitoring of all alarms")); mOptions[EXEC] = new QCommandLineOption(QStringList{QStringLiteral("e"), QStringLiteral("exec")}, i18n("Execute a shell command line"), QStringLiteral("commandLine")); mOptions[EXEC_DISPLAY] = new QCommandLineOption(QStringList{QStringLiteral("E"), QStringLiteral("exec-display")}, i18n("Command line to generate alarm message text"), QStringLiteral("commandLine")); mOptions[OptEDIT] = new QCommandLineOption(QStringLiteral("edit"), i18n("Display the alarm edit dialog to edit the specified alarm"), QStringLiteral("eventID")); mOptions[EDIT_NEW_DISPLAY] = new QCommandLineOption(QStringLiteral("edit-new-display"), i18n("Display the alarm edit dialog to edit a new display alarm")); mOptions[EDIT_NEW_COMMAND] = new QCommandLineOption(QStringLiteral("edit-new-command"), i18n("Display the alarm edit dialog to edit a new command alarm")); mOptions[EDIT_NEW_EMAIL] = new QCommandLineOption(QStringLiteral("edit-new-email"), i18n("Display the alarm edit dialog to edit a new email alarm")); mOptions[EDIT_NEW_AUDIO] = new QCommandLineOption(QStringLiteral("edit-new-audio"), i18n("Display the alarm edit dialog to edit a new audio alarm")); mOptions[OptEDIT_NEW_PRESET] = new QCommandLineOption(QStringLiteral("edit-new-preset"), i18n("Display the alarm edit dialog, preset with a template"), QStringLiteral("templateName")); mOptions[FILE] = new QCommandLineOption(QStringList{QStringLiteral("f"), QStringLiteral("file")}, i18n("File to display"), QStringLiteral("url")); mOptions[FROM_ID] = new QCommandLineOption(QStringList{QStringLiteral("F"), QStringLiteral("from-id")}, i18n("KMail identity to use as sender of email"), QStringLiteral("ID")); mOptions[INTERVAL] = new QCommandLineOption(QStringList{QStringLiteral("i"), QStringLiteral("interval")}, i18n("Interval between alarm repetitions"), QStringLiteral("period")); mOptions[KORGANIZER] = new QCommandLineOption(QStringList{QStringLiteral("k"), QStringLiteral("korganizer")}, i18n("Show alarm as an event in KOrganizer")); mOptions[LATE_CANCEL] = new QCommandLineOption(QStringList{QStringLiteral("l"), QStringLiteral("late-cancel")}, i18n("Cancel alarm if more than 'period' late when triggered"), QStringLiteral("period"), QStringLiteral("1")); mOptions[OptLIST] = new QCommandLineOption(QStringLiteral("list"), i18n("Output list of scheduled alarms to stdout")); mOptions[LOGIN] = new QCommandLineOption(QStringList{QStringLiteral("L"), QStringLiteral("login")}, i18n("Repeat alarm at every login")); mOptions[MAIL] = new QCommandLineOption(QStringList{QStringLiteral("m"), QStringLiteral("mail")}, i18n("Send an email to the given address (repeat as needed)"), QStringLiteral("address")); mOptions[PLAY] = new QCommandLineOption(QStringList{QStringLiteral("p"), QStringLiteral("play")}, i18n("Audio file to play once"), QStringLiteral("url")); mOptions[PLAY_REPEAT] = new QCommandLineOption(QStringList{QStringLiteral("P"), QStringLiteral("play-repeat")}, i18n("Audio file to play repeatedly"), QStringLiteral("url")); mOptions[RECURRENCE] = new QCommandLineOption(QStringLiteral("recurrence"), i18n("Specify alarm recurrence using iCalendar syntax"), QStringLiteral("spec")); mOptions[REMINDER] = new QCommandLineOption(QStringList{QStringLiteral("R"), QStringLiteral("reminder")}, i18n("Display reminder before or after alarm"), QStringLiteral("period")); mOptions[REMINDER_ONCE] = new QCommandLineOption(QStringLiteral("reminder-once"), i18n("Display reminder once, before or after first alarm recurrence"), QStringLiteral("period")); mOptions[REPEAT] = new QCommandLineOption(QStringList{QStringLiteral("r"), QStringLiteral("repeat")}, i18n("Number of times to repeat alarm (including initial occasion)"), QStringLiteral("count")); mOptions[SPEAK] = new QCommandLineOption(QStringList{QStringLiteral("s"), QStringLiteral("speak")}, i18n("Speak the message when it is displayed")); mOptions[SUBJECT] = new QCommandLineOption(QStringList{QStringLiteral("S"), QStringLiteral("subject")}, i18n("Email subject line"), QStringLiteral("text")); #ifndef NDEBUG mOptions[TEST_SET_TIME] = new QCommandLineOption(QStringLiteral("test-set-time"), i18n("Simulate system time [[[yyyy-]mm-]dd-]hh:mm [TZ] (debug mode)"), QStringLiteral("time")); #endif mOptions[TIME] = new QCommandLineOption(QStringList{QStringLiteral("t"), QStringLiteral("time")}, i18n("Trigger alarm at time [[[yyyy-]mm-]dd-]hh:mm [TZ], or date yyyy-mm-dd [TZ]"), QStringLiteral("time")); mOptions[OptTRAY] = new QCommandLineOption(QStringLiteral("tray"), i18n("Display system tray icon")); mOptions[OptTRIGGER_EVENT] = new QCommandLineOption(QStringLiteral("triggerEvent"), i18n("Trigger alarm with the specified event ID"), QStringLiteral("eventID")); mOptions[UNTIL] = new QCommandLineOption(QStringList{QStringLiteral("u"), QStringLiteral("until")}, i18n("Repeat until time [[[yyyy-]mm-]dd-]hh:mm [TZ], or date yyyy-mm-dd [TZ]"), QStringLiteral("time")); mOptions[VOLUME] = new QCommandLineOption(QStringList{QStringLiteral("V"), QStringLiteral("volume")}, i18n("Volume to play audio file"), QStringLiteral("percent")); for (int i = 0; i < mOptions.size(); ++i) { - if (!mOptions[i]) + if (!mOptions.at(i)) qCCritical(KALARM_LOG) << "CommandOptions::setOptions: Command option" << i << "not initialised"; else - mParser->addOption(*(mOptions[i])); + mParser->addOption(*(mOptions.at(i))); } mParser->addVersionOption(); mParser->addHelpOption(); mParser->addPositionalArgument(QStringLiteral("message"), i18n("Message text to display"), QStringLiteral("[message]")); // Check for any options which eat up all following arguments. mNonExecArguments.clear(); for (int i = 0; i < args.size(); ++i) { const QString arg = args[i]; if (arg == QStringLiteral("--nofork")) continue; // Ignore debugging option mNonExecArguments << arg; if (arg == optionName(EXEC) || arg == optionName(EXEC, true) || arg == optionName(EXEC_DISPLAY) || arg == optionName(EXEC_DISPLAY, true)) { // All following arguments (including ones beginning with '-') // belong to this option. QCommandLineParser can't handle this, so // remove them from the command line. ++i; // leave the first argument, which is the command to be executed while (++i < args.size()) mExecArguments << args[i]; } } return mNonExecArguments; } void CommandOptions::parse() { if (!mParser->parse(mNonExecArguments)) { setError(mParser->errorText()); return; } if (mParser->isSet(QStringLiteral("help"))) { mCommand = EXIT; mError = mParser->helpText(); return; } if (mParser->isSet(QStringLiteral("version"))) { mCommand = EXIT; mError = QCoreApplication::applicationName() + QStringLiteral(" ") + QCoreApplication::applicationVersion(); return; } } void CommandOptions::process() { if (mCommand == CMD_ERROR || mCommand == EXIT) return; #ifndef NDEBUG - if (mParser->isSet(*mOptions[TEST_SET_TIME])) + if (mParser->isSet(*mOptions.at(TEST_SET_TIME))) { - const QString time = mParser->value(*mOptions[TEST_SET_TIME]); + const QString time = mParser->value(*mOptions.at(TEST_SET_TIME)); if (!AlarmTime::convertTimeString(time.toLatin1(), mSimulationTime, KADateTime::realCurrentLocalDateTime(), true)) setErrorParameter(TEST_SET_TIME); } #endif if (checkCommand(OptTRAY, TRAY)) { } if (checkCommand(OptLIST, LIST)) { if (!mParser->positionalArguments().empty()) setErrorParameter(OptLIST); } if (checkCommand(OptTRIGGER_EVENT, TRIGGER_EVENT) || checkCommand(OptCANCEL_EVENT, CANCEL_EVENT) || checkCommand(OptEDIT, EDIT)) { // Fetch the event ID. This can optionally include a prefix of the // resource ID followed by a colon delimiter. - mEventId = EventId(mParser->value(*mOptions[mCommandOpt])); + mEventId = EventId(mParser->value(*mOptions.at(mCommandOpt))); } if (checkCommand(OptEDIT_NEW_PRESET, EDIT_NEW_PRESET)) { - mTemplateName = mParser->value(*mOptions[mCommandOpt]); + mTemplateName = mParser->value(*mOptions.at(mCommandOpt)); } if (checkCommand(FILE, NEW)) { mEditType = EditAlarmDlg::DISPLAY; mEditAction = KAEvent::FILE; mEditActionSet = true; - mText = mParser->value(*mOptions[mCommandOpt]); + mText = mParser->value(*mOptions.at(mCommandOpt)); } if (checkCommand(EXEC_DISPLAY, NEW)) { mEditType = EditAlarmDlg::DISPLAY; mEditAction = KAEvent::COMMAND; mEditActionSet = true; mFlags |= KAEvent::DISPLAY_COMMAND; - mText = mParser->value(*mOptions[mCommandOpt]) + QStringLiteral(" ") + mExecArguments.join(QLatin1Char(' ')); + mText = mParser->value(*mOptions.at(mCommandOpt)) + QStringLiteral(" ") + mExecArguments.join(QLatin1Char(' ')); } if (checkCommand(EXEC, NEW)) { mEditType = EditAlarmDlg::COMMAND; mEditAction = KAEvent::COMMAND; mEditActionSet = true; - mText = mParser->value(*mOptions[mCommandOpt]) + QStringLiteral(" ") + mExecArguments.join(QLatin1Char(' ')); + mText = mParser->value(*mOptions.at(mCommandOpt)) + QStringLiteral(" ") + mExecArguments.join(QLatin1Char(' ')); } if (checkCommand(MAIL, NEW)) { mEditType = EditAlarmDlg::EMAIL; mEditAction = KAEvent::EMAIL; mEditActionSet = true; } if (checkCommand(EDIT_NEW_DISPLAY, EDIT_NEW, EditAlarmDlg::DISPLAY)) { mEditType = EditAlarmDlg::DISPLAY; if (!mEditActionSet || (mEditAction != KAEvent::COMMAND && mEditAction != KAEvent::FILE)) { mEditAction = KAEvent::MESSAGE; mEditActionSet = true; } const QStringList args = mParser->positionalArguments(); if (!args.empty()) mText = args[0]; } if (checkCommand(EDIT_NEW_COMMAND, EDIT_NEW)) { mEditType = EditAlarmDlg::COMMAND; mEditAction = KAEvent::COMMAND; mEditActionSet = true; } if (checkCommand(EDIT_NEW_EMAIL, EDIT_NEW, EditAlarmDlg::EMAIL)) { mEditType = EditAlarmDlg::EMAIL; mEditAction = KAEvent::EMAIL; mEditActionSet = true; } if (checkCommand(EDIT_NEW_AUDIO, EDIT_NEW, EditAlarmDlg::AUDIO)) { mEditType = EditAlarmDlg::AUDIO; mEditAction = KAEvent::AUDIO; mEditActionSet = true; } if (mError.isEmpty() && mCommand == NONE) { if (mParser->positionalArguments().empty()) { if (checkCommand(PLAY, NEW) || checkCommand(PLAY_REPEAT, NEW)) { mEditType = EditAlarmDlg::AUDIO; mEditAction = KAEvent::AUDIO; mEditActionSet = true; } } else { qCDebug(KALARM_LOG) << "CommandOptions::process: Message"; mCommand = NEW; mCommandOpt = Opt_Message; mEditType = EditAlarmDlg::DISPLAY; mEditAction = KAEvent::MESSAGE; mEditActionSet = true; mText = arg(0); } } if (mEditActionSet && mEditAction == KAEvent::EMAIL) { - if (mParser->isSet(*mOptions[SUBJECT])) - mSubject = mParser->value(*mOptions[SUBJECT]); - if (mParser->isSet(*mOptions[FROM_ID])) - mFromID = Identities::identityUoid(mParser->value(*mOptions[FROM_ID])); - QStringList params = mParser->values(*mOptions[MAIL]); - for (int i = 0, count = params.count(); i < count; ++i) + if (mParser->isSet(*mOptions.at(SUBJECT))) + mSubject = mParser->value(*mOptions.at(SUBJECT)); + if (mParser->isSet(*mOptions.at(FROM_ID))) + mFromID = Identities::identityUoid(mParser->value(*mOptions.at(FROM_ID))); + const QStringList mailParams = mParser->values(*mOptions.at(MAIL)); + for (const QString& addr : mailParams) { - QString addr = params[i]; - if (!KAMail::checkAddress(addr)) + QString a(addr); + if (!KAMail::checkAddress(a)) setError(xi18nc("@info:shell", "%1: invalid email address", optionName(MAIL))); KCalendarCore::Person person(QString(), addr); mAddressees += person; } - params = mParser->values(*mOptions[ATTACH]); - for (int i = 0, count = params.count(); i < count; ++i) - mAttachments += params[i]; + const QStringList attParams = mParser->values(*mOptions.at(ATTACH)); + for (const QString& att : attParams) + mAttachments += att; const QStringList args = mParser->positionalArguments(); if (!args.empty()) mText = args[0]; } - if (mParser->isSet(*mOptions[DISABLE_ALL])) + if (mParser->isSet(*mOptions.at(DISABLE_ALL))) { if (mCommand == TRIGGER_EVENT || mCommand == LIST) setErrorIncompatible(DISABLE_ALL, mCommandOpt); mDisableAll = true; } // Check that other options are only specified for the // correct main command options. checkEditType(EditAlarmDlg::DISPLAY, COLOUR); checkEditType(EditAlarmDlg::DISPLAY, COLOURFG); checkEditType(EditAlarmDlg::DISPLAY, EditAlarmDlg::AUDIO, PLAY); checkEditType(EditAlarmDlg::DISPLAY, EditAlarmDlg::AUDIO, PLAY_REPEAT); checkEditType(EditAlarmDlg::DISPLAY, EditAlarmDlg::AUDIO, VOLUME); checkEditType(EditAlarmDlg::DISPLAY, SPEAK); checkEditType(EditAlarmDlg::DISPLAY, BEEP); checkEditType(EditAlarmDlg::DISPLAY, REMINDER); checkEditType(EditAlarmDlg::DISPLAY, REMINDER_ONCE); checkEditType(EditAlarmDlg::DISPLAY, ACK_CONFIRM); checkEditType(EditAlarmDlg::DISPLAY, AUTO_CLOSE); checkEditType(EditAlarmDlg::EMAIL, SUBJECT); checkEditType(EditAlarmDlg::EMAIL, FROM_ID); checkEditType(EditAlarmDlg::EMAIL, ATTACH); checkEditType(EditAlarmDlg::EMAIL, BCC); switch (mCommand) { case EDIT_NEW: - if (mParser->isSet(*mOptions[DISABLE])) + if (mParser->isSet(*mOptions.at(DISABLE))) setErrorIncompatible(DISABLE, mCommandOpt); // Fall through to NEW Q_FALLTHROUGH(); case NEW: { // Display a message or file, execute a command, or send an email - if (mParser->isSet(*mOptions[COLOUR])) + if (mParser->isSet(*mOptions.at(COLOUR))) { // Background colour is specified - QString colourText = mParser->value(*mOptions[COLOUR]); - if (colourText[0] == QLatin1Char('0') && colourText[1].toLower() == QLatin1Char('x')) + QString colourText = mParser->value(*mOptions.at(COLOUR)); + if (colourText.at(0) == QLatin1Char('0') + && colourText.at(1).toLower() == QLatin1Char('x')) colourText.replace(0, 2, QStringLiteral("#")); mBgColour.setNamedColor(colourText); if (!mBgColour.isValid()) setErrorParameter(COLOUR); } - if (mParser->isSet(*mOptions[COLOURFG])) + if (mParser->isSet(*mOptions.at(COLOURFG))) { // Foreground colour is specified - QString colourText = mParser->value(*mOptions[COLOURFG]); - if (colourText[0] == QLatin1Char('0') && colourText[1].toLower() == QLatin1Char('x')) + QString colourText = mParser->value(*mOptions.at(COLOURFG)); + if (colourText.at(0) == QLatin1Char('0') + && colourText.at(1).toLower() == QLatin1Char('x')) colourText.replace(0, 2, QStringLiteral("#")); mFgColour.setNamedColor(colourText); if (!mFgColour.isValid()) setErrorParameter(COLOURFG); } - if (mParser->isSet(*mOptions[TIME])) + if (mParser->isSet(*mOptions.at(TIME))) { - QByteArray dateTime = mParser->value(*mOptions[TIME]).toLocal8Bit(); + const QByteArray dateTime = mParser->value(*mOptions.at(TIME)).toLocal8Bit(); if (!AlarmTime::convertTimeString(dateTime, mAlarmTime)) setErrorParameter(TIME); } else mAlarmTime = KADateTime::currentLocalDateTime(); - bool haveRecurrence = mParser->isSet(*mOptions[RECURRENCE]); + const bool haveRecurrence = mParser->isSet(*mOptions.at(RECURRENCE)); if (haveRecurrence) { - if (mParser->isSet(*mOptions[LOGIN])) + if (mParser->isSet(*mOptions.at(LOGIN))) setErrorIncompatible(LOGIN, RECURRENCE); - else if (mParser->isSet(*mOptions[UNTIL])) + else if (mParser->isSet(*mOptions.at(UNTIL))) setErrorIncompatible(UNTIL, RECURRENCE); - QString rule = mParser->value(*mOptions[RECURRENCE]); + const QString rule = mParser->value(*mOptions.at(RECURRENCE)); mRecurrence = new KARecurrence; mRecurrence->set(rule); } - if (mParser->isSet(*mOptions[INTERVAL])) + if (mParser->isSet(*mOptions.at(INTERVAL))) { // Repeat count is specified int count = 0; KADateTime endTime; - if (mParser->isSet(*mOptions[LOGIN])) + if (mParser->isSet(*mOptions.at(LOGIN))) setErrorIncompatible(LOGIN, INTERVAL); bool ok; - if (mParser->isSet(*mOptions[REPEAT])) + if (mParser->isSet(*mOptions.at(REPEAT))) { - count = mParser->value(*mOptions[REPEAT]).toInt(&ok); + count = mParser->value(*mOptions.at(REPEAT)).toInt(&ok); if (!ok || !count || count < -1 || (count < 0 && haveRecurrence)) setErrorParameter(REPEAT); } else if (haveRecurrence) setErrorRequires(INTERVAL, REPEAT); - else if (mParser->isSet(*mOptions[UNTIL])) + else if (mParser->isSet(*mOptions.at(UNTIL))) { count = 0; - QByteArray dateTime = mParser->value(*mOptions[UNTIL]).toLocal8Bit(); + const QByteArray dateTime = mParser->value(*mOptions.at(UNTIL)).toLocal8Bit(); bool ok; - if (mParser->isSet(*mOptions[TIME])) + if (mParser->isSet(*mOptions.at(TIME))) ok = AlarmTime::convertTimeString(dateTime, endTime, mAlarmTime); else ok = AlarmTime::convertTimeString(dateTime, endTime); if (!ok) setErrorParameter(UNTIL); else if (mAlarmTime.isDateOnly() && !endTime.isDateOnly()) setError(xi18nc("@info:shell", "Invalid %1 parameter for date-only alarm", optionName(UNTIL))); if (!mAlarmTime.isDateOnly() && endTime.isDateOnly()) endTime.setTime(QTime(23,59,59)); if (endTime < mAlarmTime) setError(xi18nc("@info:shell", "%1 earlier than %2", optionName(UNTIL), optionName(TIME))); } else count = -1; // Get the recurrence interval int intervalOfType; KARecurrence::Type recurType; - if (!convInterval(mParser->value(*mOptions[INTERVAL]), recurType, intervalOfType, !haveRecurrence)) + if (!convInterval(mParser->value(*mOptions.at(INTERVAL)), recurType, intervalOfType, !haveRecurrence)) setErrorParameter(INTERVAL); else if (mAlarmTime.isDateOnly() && recurType == KARecurrence::MINUTELY) setError(xi18nc("@info:shell", "Invalid %1 parameter for date-only alarm", optionName(INTERVAL))); if (haveRecurrence) { if (mRecurrence) { // There is a also a recurrence specified, so set up a sub-repetition. // In this case, 'intervalOfType' is in minutes. mRepeatInterval = KCalendarCore::Duration(intervalOfType * 60); - KCalendarCore::Duration longestInterval = mRecurrence->longestInterval(); + const KCalendarCore::Duration longestInterval = mRecurrence->longestInterval(); if (mRepeatInterval * count > longestInterval) setError(xi18nc("@info:shell", "Invalid %1 and %2 parameters: repetition is longer than %3 interval", optionName(INTERVAL), optionName(REPEAT), optionName(RECURRENCE))); mRepeatCount = count; } } else { // There is no other recurrence specified, so convert the repetition // parameters into a KCal::Recurrence mRecurrence = new KARecurrence; mRecurrence->set(recurType, intervalOfType, count, mAlarmTime, endTime); } } else { - if (mParser->isSet(*mOptions[REPEAT])) + if (mParser->isSet(*mOptions.at(REPEAT))) setErrorRequires(REPEAT, INTERVAL); - else if (mParser->isSet(*mOptions[UNTIL])) + else if (mParser->isSet(*mOptions.at(UNTIL))) setErrorRequires(UNTIL, INTERVAL); } - bool audioRepeat = mParser->isSet(*mOptions[PLAY_REPEAT]); - if (audioRepeat || mParser->isSet(*mOptions[PLAY])) + const bool audioRepeat = mParser->isSet(*mOptions.at(PLAY_REPEAT)); + if (audioRepeat || mParser->isSet(*mOptions.at(PLAY))) { // Play a sound with the alarm - Option opt = audioRepeat ? PLAY_REPEAT : PLAY; - if (audioRepeat && mParser->isSet(*mOptions[PLAY])) + const Option opt = audioRepeat ? PLAY_REPEAT : PLAY; + if (audioRepeat && mParser->isSet(*mOptions.at(PLAY))) setErrorIncompatible(PLAY, PLAY_REPEAT); - if (mParser->isSet(*mOptions[BEEP])) + if (mParser->isSet(*mOptions.at(BEEP))) setErrorIncompatible(BEEP, opt); - else if (mParser->isSet(*mOptions[SPEAK])) + else if (mParser->isSet(*mOptions.at(SPEAK))) setErrorIncompatible(SPEAK, opt); - mAudioFile = mParser->value(*mOptions[audioRepeat ? PLAY_REPEAT : PLAY]); - if (mParser->isSet(*mOptions[VOLUME])) + mAudioFile = mParser->value(*mOptions.at(audioRepeat ? PLAY_REPEAT : PLAY)); + if (mParser->isSet(*mOptions.at(VOLUME))) { bool ok; - int volumepc = mParser->value(*mOptions[VOLUME]).toInt(&ok); + const int volumepc = mParser->value(*mOptions.at(VOLUME)).toInt(&ok); if (!ok || volumepc < 0 || volumepc > 100) setErrorParameter(VOLUME); mAudioVolume = static_cast(volumepc) / 100; } } - else if (mParser->isSet(*mOptions[VOLUME])) + else if (mParser->isSet(*mOptions.at(VOLUME))) setErrorRequires(VOLUME, PLAY, PLAY_REPEAT); - if (mParser->isSet(*mOptions[SPEAK])) + if (mParser->isSet(*mOptions.at(SPEAK))) { - if (mParser->isSet(*mOptions[BEEP])) + if (mParser->isSet(*mOptions.at(BEEP))) setErrorIncompatible(BEEP, SPEAK); else if (!KPIMTextEdit::TextToSpeech::self()->isReady()) setError(xi18nc("@info:shell", "%1 requires KAlarm to be compiled with QTextToSpeech support", optionName(SPEAK))); } - bool onceOnly = mParser->isSet(*mOptions[REMINDER_ONCE]); - if (mParser->isSet(*mOptions[REMINDER]) || onceOnly) + const bool onceOnly = mParser->isSet(*mOptions.at(REMINDER_ONCE)); + if (mParser->isSet(*mOptions.at(REMINDER)) || onceOnly) { // Issue a reminder alarm in advance of or after the main alarm - if (onceOnly && mParser->isSet(*mOptions[REMINDER])) + if (onceOnly && mParser->isSet(*mOptions.at(REMINDER))) setErrorIncompatible(REMINDER, REMINDER_ONCE); - Option opt = onceOnly ? REMINDER_ONCE : REMINDER; + const Option opt = onceOnly ? REMINDER_ONCE : REMINDER; KARecurrence::Type recurType; - QString optval = mParser->value(*mOptions[onceOnly ? REMINDER_ONCE : REMINDER]); - bool after = (optval[0] == QLatin1Char('+')); + QString optval = mParser->value(*mOptions.at(onceOnly ? REMINDER_ONCE : REMINDER)); + const bool after = (optval.at(0) == QLatin1Char('+')); if (after) optval.remove(0, 1); // it's a reminder after the main alarm if (!convInterval(optval, recurType, mReminderMinutes)) setErrorParameter(opt); else if (recurType == KARecurrence::MINUTELY && mAlarmTime.isDateOnly()) setError(xi18nc("@info:shell", "Invalid %1 parameter for date-only alarm", optionName(opt))); if (after) mReminderMinutes = -mReminderMinutes; if (onceOnly) mFlags |= KAEvent::REMINDER_ONCE; } - if (mParser->isSet(*mOptions[LATE_CANCEL])) + if (mParser->isSet(*mOptions.at(LATE_CANCEL))) { KARecurrence::Type recurType; - bool ok = convInterval(mParser->value(*mOptions[LATE_CANCEL]), recurType, mLateCancel); + const bool ok = convInterval(mParser->value(*mOptions.at(LATE_CANCEL)), recurType, mLateCancel); if (!ok) setErrorParameter(LATE_CANCEL); } - else if (mParser->isSet(*mOptions[AUTO_CLOSE])) + else if (mParser->isSet(*mOptions.at(AUTO_CLOSE))) setErrorRequires(AUTO_CLOSE, LATE_CANCEL); - if (mParser->isSet(*mOptions[ACK_CONFIRM])) + if (mParser->isSet(*mOptions.at(ACK_CONFIRM))) mFlags |= KAEvent::CONFIRM_ACK; - if (mParser->isSet(*mOptions[AUTO_CLOSE])) + if (mParser->isSet(*mOptions.at(AUTO_CLOSE))) mFlags |= KAEvent::AUTO_CLOSE; - if (mParser->isSet(*mOptions[BEEP])) + if (mParser->isSet(*mOptions.at(BEEP))) mFlags |= KAEvent::BEEP; - if (mParser->isSet(*mOptions[SPEAK])) + if (mParser->isSet(*mOptions.at(SPEAK))) mFlags |= KAEvent::SPEAK; - if (mParser->isSet(*mOptions[KORGANIZER])) + if (mParser->isSet(*mOptions.at(KORGANIZER))) mFlags |= KAEvent::COPY_KORGANIZER; - if (mParser->isSet(*mOptions[DISABLE])) + if (mParser->isSet(*mOptions.at(DISABLE))) mFlags |= KAEvent::DISABLED; if (audioRepeat) mFlags |= KAEvent::REPEAT_SOUND; - if (mParser->isSet(*mOptions[LOGIN])) + if (mParser->isSet(*mOptions.at(LOGIN))) mFlags |= KAEvent::REPEAT_AT_LOGIN; - if (mParser->isSet(*mOptions[BCC])) + if (mParser->isSet(*mOptions.at(BCC))) mFlags |= KAEvent::EMAIL_BCC; if (mAlarmTime.isDateOnly()) mFlags |= KAEvent::ANY_TIME; break; } case NONE: { // No arguments - run interactively & display the main window if (!mError.isEmpty()) break; qCDebug(KALARM_LOG) << "CommandOptions::process: Interactive"; QStringList errors; - if (mParser->isSet(*mOptions[ACK_CONFIRM])) + if (mParser->isSet(*mOptions.at(ACK_CONFIRM))) errors << optionName(ACK_CONFIRM); - if (mParser->isSet(*mOptions[ATTACH])) + if (mParser->isSet(*mOptions.at(ATTACH))) errors << optionName(ATTACH); - if (mParser->isSet(*mOptions[AUTO_CLOSE])) + if (mParser->isSet(*mOptions.at(AUTO_CLOSE))) errors << optionName(AUTO_CLOSE); - if (mParser->isSet(*mOptions[BCC])) + if (mParser->isSet(*mOptions.at(BCC))) errors << optionName(BCC); - if (mParser->isSet(*mOptions[BEEP])) + if (mParser->isSet(*mOptions.at(BEEP))) errors << optionName(BEEP); - if (mParser->isSet(*mOptions[COLOUR])) + if (mParser->isSet(*mOptions.at(COLOUR))) errors << optionName(COLOUR); - if (mParser->isSet(*mOptions[COLOURFG])) + if (mParser->isSet(*mOptions.at(COLOURFG))) errors << optionName(COLOURFG); - if (mParser->isSet(*mOptions[DISABLE])) + if (mParser->isSet(*mOptions.at(DISABLE))) errors << optionName(DISABLE); - if (mParser->isSet(*mOptions[FROM_ID])) + if (mParser->isSet(*mOptions.at(FROM_ID))) errors << optionName(FROM_ID); - if (mParser->isSet(*mOptions[KORGANIZER])) + if (mParser->isSet(*mOptions.at(KORGANIZER))) errors << optionName(KORGANIZER); - if (mParser->isSet(*mOptions[LATE_CANCEL])) + if (mParser->isSet(*mOptions.at(LATE_CANCEL))) errors << optionName(LATE_CANCEL); - if (mParser->isSet(*mOptions[LOGIN])) + if (mParser->isSet(*mOptions.at(LOGIN))) errors << optionName(LOGIN); - if (mParser->isSet(*mOptions[PLAY])) + if (mParser->isSet(*mOptions.at(PLAY))) errors << optionName(PLAY); - if (mParser->isSet(*mOptions[PLAY_REPEAT])) + if (mParser->isSet(*mOptions.at(PLAY_REPEAT))) errors << optionName(PLAY_REPEAT); - if (mParser->isSet(*mOptions[REMINDER])) + if (mParser->isSet(*mOptions.at(REMINDER))) errors << optionName(REMINDER); - if (mParser->isSet(*mOptions[REMINDER_ONCE])) + if (mParser->isSet(*mOptions.at(REMINDER_ONCE))) errors << optionName(REMINDER_ONCE); - if (mParser->isSet(*mOptions[SPEAK])) + if (mParser->isSet(*mOptions.at(SPEAK))) errors << optionName(SPEAK); - if (mParser->isSet(*mOptions[SUBJECT])) + if (mParser->isSet(*mOptions.at(SUBJECT))) errors << optionName(SUBJECT); - if (mParser->isSet(*mOptions[TIME])) + if (mParser->isSet(*mOptions.at(TIME))) errors << optionName(TIME); - if (mParser->isSet(*mOptions[VOLUME])) + if (mParser->isSet(*mOptions.at(VOLUME))) errors << optionName(VOLUME); if (!errors.isEmpty()) mError = errors.join(QLatin1Char(' ')) + i18nc("@info:shell", ": option(s) only valid with an appropriate action option or message"); break; } default: break; } if (!mError.isEmpty()) setError(mError); } void CommandOptions::setError(const QString& errmsg) { qCWarning(KALARM_LOG) << "CommandOptions::setError:" << errmsg; mCommand = CMD_ERROR; if (mError.isEmpty()) mError = errmsg + i18nc("@info:shell", "\nUse --help to get a list of available command line options.\n"); } void CommandOptions::printError(const QString& errmsg) { // Note: we can't use mArgs->usage() since that also quits any other // running 'instances' of the program. std::cerr << errmsg.toLocal8Bit().data() - << i18nc("@info:shell", "\nUse --help to get a list of available command line options.\n").toLocal8Bit().data(); + << i18nc("@info:shell", "\nUse --help to get a list of available command line options.\n").toLocal8Bit().data(); } /****************************************************************************** * Check if the given command option is specified, and if so set mCommand etc. * If another command option has also been detected, issue an error. * If 'allowedEditType' is set, supersede any previous specification of that * edit type with the given command option - this allows, e.g., --mail to be * used along with --edit-new-email so the user can specify addressees. */ bool CommandOptions::checkCommand(Option command, Command code, EditAlarmDlg::Type allowedEditType) { if (!mError.isEmpty() - || !mParser->isSet(*mOptions[command])) + || !mParser->isSet(*mOptions.at(command))) return false; if (mCommand != NONE && (allowedEditType == EditAlarmDlg::NO_TYPE || (allowedEditType != EditAlarmDlg::NO_TYPE && (mCommand != NEW || mEditType != allowedEditType)))) setErrorIncompatible(mCommandOpt, command); qCDebug(KALARM_LOG).nospace() << "CommandOptions::checkCommand: " << optionName(command); mCommand = code; mCommandOpt = command; return true; } // Set the error message to "--opt requires --opt2" or "--opt requires --opt2 or --opt3". void CommandOptions::setErrorRequires(Option opt, Option opt2, Option opt3) { if (opt3 == Num_Options) setError(xi18nc("@info:shell", "%1 requires %2", optionName(opt), optionName(opt2))); else setError(xi18nc("@info:shell", "%1 requires %2 or %3", optionName(opt), optionName(opt2), optionName(opt3))); } void CommandOptions::setErrorParameter(Option opt) { setError(xi18nc("@info:shell", "Invalid %1 parameter", optionName(opt))); } void CommandOptions::setErrorIncompatible(Option opt1, Option opt2) { setError(xi18nc("@info:shell", "%1 incompatible with %2", optionName(opt1), optionName(opt2))); } void CommandOptions::checkEditType(EditAlarmDlg::Type type1, EditAlarmDlg::Type type2, Option opt) { - if (mParser->isSet(*mOptions[opt]) && mCommand != NONE + if (mParser->isSet(*mOptions.at(opt)) && mCommand != NONE && ((mCommand != NEW && mCommand != EDIT_NEW) || (mEditType != type1 && (type2 == EditAlarmDlg::NO_TYPE || mEditType != type2)))) setErrorIncompatible(opt, mCommandOpt); } // Fetch one of the arguments (i.e. not belonging to any option). QString CommandOptions::arg(int n) { const QStringList args = mParser->positionalArguments(); return (n < args.size()) ? args[n] : QString(); } QString CommandOptions::optionName(Option opt, bool shortName) const { if (opt == Opt_Message) return QStringLiteral("message"); - const QStringList names = mOptions[opt]->names(); + const QStringList names = mOptions.at(opt)->names(); if (names.empty()) return QString(); - for (int i = 0; i < names.size(); ++i) + for (const QString& name : names) { - if (shortName && names[i].size() == 1) - return QStringLiteral("-") + names[i]; - else if (!shortName && names[i].size() > 1) - return QStringLiteral("--") + names[i]; + if (shortName && name.size() == 1) + return QStringLiteral("-") + name; + else if (!shortName && name.size() > 1) + return QStringLiteral("--") + name; } return QStringLiteral("-") + names[0]; } namespace { /****************************************************************************** * Convert a non-zero positive time interval command line parameter. * 'timeInterval' receives the count for the recurType. If 'allowMonthYear' is * false, weeks and days are converted to minutes. * Reply = true if successful. */ bool convInterval(const QString& timeParam, KARecurrence::Type& recurType, int& timeInterval, bool allowMonthYear) { QByteArray timeString = timeParam.toLocal8Bit(); // Get the recurrence interval bool ok = true; uint interval = 0; uint length = timeString.length(); - switch (timeString[length - 1]) + switch (timeString.at(length - 1)) { case 'Y': if (!allowMonthYear) ok = false; recurType = KARecurrence::ANNUAL_DATE; timeString = timeString.left(length - 1); break; case 'W': recurType = KARecurrence::WEEKLY; timeString = timeString.left(length - 1); break; case 'D': recurType = KARecurrence::DAILY; timeString = timeString.left(length - 1); break; case 'M': { int i = timeString.indexOf('H'); if (i < 0) { if (!allowMonthYear) ok = false; recurType = KARecurrence::MONTHLY_DAY; timeString = timeString.left(length - 1); } else { recurType = KARecurrence::MINUTELY; interval = timeString.left(i).toUInt(&ok) * 60; timeString = timeString.mid(i + 1, length - i - 2); } break; } default: // should be a digit recurType = KARecurrence::MINUTELY; break; } if (ok) interval += timeString.toUInt(&ok); if (!allowMonthYear) { // Convert time interval to minutes switch (recurType) { case KARecurrence::WEEKLY: interval *= 7; // fall through to DAILY Q_FALLTHROUGH(); case KARecurrence::DAILY: interval *= 24*60; recurType = KARecurrence::MINUTELY; break; default: break; } } timeInterval = static_cast(interval); return ok && interval; } } // vim: et sw=4: diff --git a/src/eventlistview.cpp b/src/eventlistview.cpp index 90c95fbc..1b63b1e8 100644 --- a/src/eventlistview.cpp +++ b/src/eventlistview.cpp @@ -1,239 +1,238 @@ /* * eventlistview.cpp - base class for widget showing list of alarms * Program: kalarm - * Copyright © 2007-2013 by David Jarvie + * Copyright © 2007-2019 David Jarvie * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kalarm.h" #include "eventlistview.h" #include "find.h" #include #include #include #include #include "kalarm_debug.h" EventListView::EventListView(QWidget* parent) - : QTreeView(parent), - mFind(nullptr), - mEditOnSingleClick(false) + : QTreeView(parent) + , mFind(nullptr) + , mEditOnSingleClick(false) { setRootIsDecorated(false); // don't show expander icons for child-less items setSortingEnabled(true); setAllColumnsShowFocus(true); setSelectionMode(ExtendedSelection); setSelectionBehavior(SelectRows); setTextElideMode(Qt::ElideRight); // Set default WhatsThis text to be displayed when no actual item is clicked on setWhatsThis(i18nc("@info:whatsthis", "List of scheduled alarms")); } /****************************************************************************** * Return the event referred to by an index. */ KAEvent EventListView::event(const QModelIndex& index) const { return itemModel()->event(index); } KAEvent EventListView::event(int row) const { return itemModel()->event(itemModel()->index(row, 0)); } /****************************************************************************** * Select one event and make it the current item. */ void EventListView::select(Akonadi::Item::Id eventId) { select(itemModel()->eventIndex(eventId)); } void EventListView::select(const QModelIndex& index, bool scrollToIndex) { selectionModel()->select(index, QItemSelectionModel::SelectCurrent | QItemSelectionModel::Rows); if (scrollToIndex) scrollTo(index); } void EventListView::clearSelection() { selectionModel()->clearSelection(); } /****************************************************************************** * Return the single selected item. * Reply = invalid if no items are selected, or if multiple items are selected. */ QModelIndex EventListView::selectedIndex() const { - QModelIndexList list = selectionModel()->selectedRows(); + const QModelIndexList list = selectionModel()->selectedRows(); if (list.count() != 1) return QModelIndex(); return list[0]; } /****************************************************************************** * Return the single selected event. * Reply = null if no items are selected, or if multiple items are selected. */ KAEvent EventListView::selectedEvent() const { - QModelIndexList list = selectionModel()->selectedRows(); + const QModelIndexList list = selectionModel()->selectedRows(); if (list.count() != 1) return KAEvent(); -qCDebug(KALARM_LOG)<<"SelectedEvent() count="<(list[0].model()); return model->event(list[0]); } /****************************************************************************** * Return the selected events. */ QVector EventListView::selectedEvents() const { QVector elist; - QModelIndexList ixlist = selectionModel()->selectedRows(); + const QModelIndexList ixlist = selectionModel()->selectedRows(); int count = ixlist.count(); if (count) { const ItemListModel* model = static_cast(ixlist[0].model()); elist.reserve(count); for (int i = 0; i < count; ++i) elist += model->event(ixlist[i]); } return elist; } /****************************************************************************** * Called when the Find action is selected. * Display the non-modal Find dialog. */ void EventListView::slotFind() { if (!mFind) { mFind = new Find(this); connect(mFind, &Find::active, this, &EventListView::findActive); } mFind->display(); } /****************************************************************************** * Called when the Find Next or Find Prev action is selected. */ void EventListView::findNext(bool forward) { if (mFind) mFind->findNext(forward); } /****************************************************************************** * Called when a ToolTip or WhatsThis event occurs. */ bool EventListView::viewportEvent(QEvent* e) { if (e->type() == QEvent::ToolTip && isActiveWindow()) { QHelpEvent* he = static_cast(e); - QModelIndex index = indexAt(he->pos()); + const QModelIndex index = indexAt(he->pos()); QVariant value = model()->data(index, Qt::ToolTipRole); if (value.canConvert()) { QString toolTip = value.toString(); int i = toolTip.indexOf(QLatin1Char('\n')); if (i < 0) { ItemListModel* m = qobject_cast(model()); if (!m || m->event(index).commandError() == KAEvent::CMD_NO_ERROR) { // Single line tooltip. Only display it if the text column // is truncated in the view display. value = model()->data(index, Qt::FontRole); - QFontMetrics fm(qvariant_cast(value).resolve(viewOptions().font)); - int textWidth = fm.boundingRect(toolTip).width() + 1; + const QFontMetrics fm(qvariant_cast(value).resolve(viewOptions().font)); + const int textWidth = fm.boundingRect(toolTip).width() + 1; const int margin = QApplication::style()->pixelMetric(QStyle::PM_FocusFrameHMargin) + 1; - int left = columnViewportPosition(index.column()) + margin; - int right = left + textWidth; + const int left = columnViewportPosition(index.column()) + margin; + const int right = left + textWidth; if (left >= 0 && right <= width() - 2*frameWidth()) toolTip.clear(); // prevent any tooltip showing } } QToolTip::showText(he->globalPos(), toolTip, this); return true; } } return QTreeView::viewportEvent(e); } /****************************************************************************** * Called when a context menu event is requested by mouse or key. */ void EventListView::contextMenuEvent(QContextMenuEvent* e) { Q_EMIT contextMenuRequested(e->globalPos()); } bool EventListDelegate::editorEvent(QEvent* e, QAbstractItemModel* model, const QStyleOptionViewItem&, const QModelIndex& index) { // Don't invoke the editor unless it's either a double click or, // if KDE is in single click mode and it's a left button release // with no other buttons pressed and no keyboard modifiers. switch (e->type()) { case QEvent::MouseButtonPress: case QEvent::MouseMove: return false; case QEvent::MouseButtonDblClick: break; case QEvent::MouseButtonRelease: { EventListView* view = static_cast(parent()); if (!view->editOnSingleClick() || !view->style()->styleHint(QStyle::SH_ItemView_ActivateItemOnSingleClick, nullptr, view)) return false; QMouseEvent* me = static_cast(e); if (me->button() != Qt::LeftButton || me->buttons() || me->modifiers() != Qt::NoModifier) return false; break; } default: break; } if (index.isValid()) { qCDebug(KALARM_LOG) << "EventListDelegate::editorEvent"; ItemListModel* itemModel = qobject_cast(model); if (!itemModel) qCCritical(KALARM_LOG) << "EventListDelegate::editorEvent: Invalid cast to ItemListModel*"; else { KAEvent event = itemModel->event(index); edit(&event, static_cast(parent())); return true; } } return false; // indicate that the event has not been handled } // vim: et sw=4: diff --git a/src/find.cpp b/src/find.cpp index 72345005..299b33a6 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) + : 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(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 = QApplication::style()->pixelMetric(QStyle::PM_DefaultChildMargin); grid->setContentsMargins(dcm, dcm, dcm, dcm); 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(); + const int rowCount = mListView->model()->rowCount(); for (int row = 0; row < rowCount; ++row) { - KAEvent viewEvent = mListView->event(row); + const 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); + const long options = mOptions & (KFind::WholeWordsOnly | KFind::CaseSensitive | KFind::RegularExpression); + const bool newFind = !mFind; + const 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(); + const 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 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(); + const 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?"); + const 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 b96677bc..3d043b1f 100644 --- a/src/functions.cpp +++ b/src/functions.cpp @@ -1,1879 +1,1878 @@ /* * functions.cpp - miscellaneous functions * Program: kalarm * Copyright © 2001-2019 David Jarvie * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "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 KCalendarCore; #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "kalarm_debug.h" using namespace Akonadi; namespace { bool refreshAlarmsQueued = false; struct UpdateStatusData { KAlarm::UpdateResult status; // status code and KOrganizer error message if any int warnErr; int warnKOrg; explicit UpdateStatusData(KAlarm::UpdateStatus s = KAlarm::UPDATE_OK) : status(s), warnErr(0), warnKOrg(0) {} // Set an error status and increment to number of errors to warn about void setError(KAlarm::UpdateStatus st, int errorCount = -1) { status.set(st); if (errorCount < 0) ++warnErr; else warnErr = errorCount; } // Update the error status with a KOrganizer related status void korgUpdate(const KAlarm::UpdateResult& result) { if (result.status != KAlarm::UPDATE_OK) { ++warnKOrg; if (result.status > status.status) status = result; } } }; const QLatin1String KMAIL_DBUS_SERVICE("org.kde.kmail"); //const QLatin1String KMAIL_DBUS_IFACE("org.kde.kmail.kmail"); //const QLatin1String KMAIL_DBUS_WINDOW_PATH("/kmail/kmail_mainwindow_1"); const QLatin1String KORG_DBUS_SERVICE("org.kde.korganizer"); const QLatin1String KORG_DBUS_IFACE("org.kde.korganizer.Korganizer"); // D-Bus object path of KOrganizer's notification interface #define KORG_DBUS_PATH "/Korganizer" #define KORG_DBUS_LOAD_PATH "/korganizer_PimApplication" //const QLatin1String KORG_DBUS_WINDOW_PATH("/korganizer/MainWindow_1"); const QLatin1String KORG_MIME_TYPE("application/x-vnd.akonadi.calendar.event"); const QLatin1String KORGANIZER_UID("korg-"); const QLatin1String ALARM_OPTS_FILE("alarmopts"); const char* DONT_SHOW_ERRORS_GROUP = "DontShowErrors"; void editNewTemplate(EditAlarmDlg::Type, const KAEvent* preset, QWidget* parent); void displayUpdateError(QWidget* parent, KAlarm::UpdateError, const UpdateStatusData&, bool showKOrgError = true); KAlarm::UpdateResult sendToKOrganizer(const KAEvent&); KAlarm::UpdateResult deleteFromKOrganizer(const QString& eventID); KAlarm::UpdateResult runKOrganizer(); QString uidKOrganizer(const QString& eventID); } namespace KAlarm { Private* Private::mInstance = nullptr; /****************************************************************************** * Display a main window with the specified event selected. */ MainWindow* displayMainWindowSelected(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)) + KAEvent& event = events[i]; + if (!cal->addEvent(event, msgParent, false, &collection)) { status.setError(UPDATE_ERROR); continue; } - if (allowKOrgUpdate && events[i].copyToKOrganizer()) + if (allowKOrgUpdate && event.copyToKOrganizer()) { - UpdateResult st = sendToKOrganizer(events[i]); // tell KOrganizer to show the event + UpdateResult st = sendToKOrganizer(event); // tell KOrganizer to show the event status.korgUpdate(st); } } if (status.warnErr == events.count()) status.status = UPDATE_FAILED; else if (!cal->save()) status.setError(SAVE_FAILED, events.count()); // everything failed } if (status.status != UPDATE_OK && msgParent) displayUpdateError(msgParent, ERR_ADD, status, showKOrgErr); return status.status; } /****************************************************************************** * Save the event in the archived calendar and adjust every main window instance. * The event's ID is changed to an archived ID if necessary. */ bool addArchivedEvent(KAEvent& event, 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); + const KAEvent* newEvent = cal->updateEvent(event); UpdateStatus status = UPDATE_OK; if (!newEvent) status = UPDATE_FAILED; else if (!cal->save()) status = SAVE_FAILED; if (status != UPDATE_OK) { if (msgParent) displayUpdateError(msgParent, ERR_TEMPLATE, UpdateStatusData(SAVE_FAILED)); return UpdateResult(status); } return UpdateResult(UPDATE_OK); } /****************************************************************************** * Delete alarms from the calendar file and from every main window instance. * If the events are archived, the events' IDs are changed to archived IDs if necessary. */ UpdateResult deleteEvent(KAEvent& event, bool archive, QWidget* msgParent, bool showKOrgErr) { QVector events(1, event); return deleteEvents(events, archive, msgParent, showKOrgErr); } UpdateResult deleteEvents(QVector& events, bool archive, QWidget* msgParent, bool showKOrgErr) { qCDebug(KALARM_LOG) << "deleteEvents:" << events.count(); if (events.isEmpty()) return UpdateResult(UPDATE_OK); UpdateStatusData status; AlarmCalendar* cal = AlarmCalendar::resources(); bool deleteWakeFromSuspendAlarm = false; - QString wakeFromSuspendId = checkRtcWakeConfig().value(0); + const QString wakeFromSuspendId = checkRtcWakeConfig().value(0); for (int i = 0, end = events.count(); i < end; ++i) { // Save the event details in the calendar file, and get the new event ID KAEvent* event = &events[i]; - QString id = event->id(); + const QString id = event->id(); // Delete the event from the calendar file if (event->category() != CalEvent::ARCHIVED) { if (event->copyToKOrganizer()) { // The event was shown in KOrganizer, so tell KOrganizer to // delete it. But ignore errors, because the user could have // manually deleted it from KOrganizer since it was set up. UpdateResult st = deleteFromKOrganizer(id); status.korgUpdate(st); } if (archive && event->toBeArchived()) { KAEvent ev(*event); addArchivedEvent(ev); // this changes the event ID to an archived ID } } if (!cal->deleteEvent(*event, false)) // don't save calendar after deleting status.setError(UPDATE_ERROR); if (id == wakeFromSuspendId) deleteWakeFromSuspendAlarm = true; // Remove "Don't show error messages again" for this alarm setDontShowErrors(EventId(*event)); } if (status.warnErr == events.count()) status.status = UPDATE_FAILED; else if (!cal->save()) // save the calendars now status.setError(SAVE_FAILED, events.count()); if (status.status != UPDATE_OK && msgParent) displayUpdateError(msgParent, ERR_DELETE, status, showKOrgErr); // Remove any wake-from-suspend scheduled for a deleted alarm if (deleteWakeFromSuspendAlarm && !wakeFromSuspendId.isEmpty()) cancelRtcWake(msgParent, wakeFromSuspendId); return status.status; } /****************************************************************************** * Delete templates from the calendar file and from every template list view. */ UpdateResult deleteTemplates(const KAEvent::List& events, QWidget* msgParent) { int count = events.count(); qCDebug(KALARM_LOG) << "deleteTemplates:" << count; if (!count) return UpdateResult(UPDATE_OK); UpdateStatusData status; AlarmCalendar* cal = AlarmCalendar::resources(); - for (int i = 0, end = count; i < end; ++i) + for (KAEvent* event : events) { // 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 + if (!cal->deleteEvent(*event, false)) // don't save calendar after deleting status.setError(UPDATE_ERROR); } if (status.warnErr == count) status.status = UPDATE_FAILED; else if (!cal->save()) // save the calendars now status.setError(SAVE_FAILED, count); if (status.status != UPDATE_OK && msgParent) displayUpdateError(msgParent, ERR_TEMPLATE, status); return status.status; } /****************************************************************************** * Delete an alarm from the display calendar. */ void deleteDisplayEvent(const QString& eventID) { qCDebug(KALARM_LOG) << "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); + const QString wakeFromSuspendId = checkRtcWakeConfig().value(0); for (int i = 0, end = events.count(); i < end; ++i) { KAEvent* event = &events[i]; if (event->category() == CalEvent::ACTIVE && enable != event->enabled()) { event->setEnabled(enable); if (!enable && event->id() == wakeFromSuspendId) deleteWakeFromSuspendAlarm = true; // Update the event in the calendar file - KAEvent* newev = cal->updateEvent(event); + const 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 (int64 seconds since epoch). * = empty list if none or expired. */ QStringList checkRtcWakeConfig(bool checkEventExists) { KConfigGroup config(KSharedConfig::openConfig(), "General"); const QStringList params = config.readEntry("RtcWake", QStringList()); if (params.count() == 3 && params[2].toLongLong() > KADateTime::currentUtcDateTime().toSecsSinceEpoch()) { 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); + const EventId id(*event); // Use AutoQPointer to guard against crash on application exit while // the dialogue is still open. It prevents double deletion (both on // deletion of parent, and on return from this function). AutoQPointer editDlg = EditAlarmDlg::create(false, event, false, parent, EditAlarmDlg::RES_USE_EVENT_ID); if (editDlg->exec() == QDialog::Accepted) { if (!AlarmCalendar::resources()->event(id)) { // Event has been deleted while the user was editing the alarm, // so treat it as a new alarm. PrivateNewAlarmDlg().accept(editDlg); return; } KAEvent newEvent; Collection calendar; bool changeDeferral = !editDlg->getEvent(newEvent, calendar); // Update the event in the displays and in the calendar file - Undo::Event undo(*event, calendar); + const 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); + const UpdateResult status = modifyEvent(*event, newEvent, editDlg); if (status.status != UPDATE_OK && status.status <= UPDATE_KORG_ERR) displayKOrgUpdateError(editDlg, ERR_MODIFY, status); } Undo::saveEdit(undo, newEvent); outputAlarmWarnings(editDlg, &newEvent); } } /****************************************************************************** * Display the alarm edit dialog to edit the alarm with the specified ID. * An error occurs if the alarm is not found, if there is more than one alarm * with the same ID, or if it is read-only or expired. */ bool editAlarmById(const EventId& id, QWidget* parent) { const QString eventID(id.eventId()); KAEvent* event = AlarmCalendar::resources()->event(id, true); if (!event) { if (id.collectionId() != -1) qCWarning(KALARM_LOG) << "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(); + const 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); + const 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); + const 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) + const bool includeCmdAlarms = ShellProcess::authorised(); + const KAEvent::List events = AlarmCalendar::resources()->events(CalEvent::TEMPLATE); + for (KAEvent* event : events) { - 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) + const KAEvent::List events = AlarmCalendar::resources()->events(CalEvent::ACTIVE); + for (KAEvent* event : events) { - 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 * = null string if success. */ QString runKMail() { QDBusReply reply = QDBusConnection::sessionBus().interface()->isServiceRegistered(KMAIL_DBUS_SERVICE); if (!reply.isValid() || !reply.value()) { // Program is not already running, so start it - QString errmsg; QDBusReply startReply = QDBusConnection::sessionBus().interface()->startService(KMAIL_DBUS_SERVICE); if (!startReply.isValid()) { - QString errmsg = startReply.error().message(); + const QString errmsg = startReply.error().message(); qCCritical(KALARM_LOG) << "Couldn't start KMail (" << errmsg << ")"; return xi18nc("@info", "Unable to start KMail(%1)", errmsg); } } return QString(); } /****************************************************************************** * The "Don't show again" option for error messages is personal to the user on a * particular computer. For example, he may want to inhibit error messages only * on his laptop. So the status is not stored in the alarm calendar, but in the * user's local KAlarm data directory. ******************************************************************************/ /****************************************************************************** * Return the Don't-show-again error message tags set for a specified alarm ID. */ QStringList dontShowErrors(const EventId& eventId) { if (eventId.isEmpty()) return QStringList(); KConfig config(QStandardPaths::writableLocation(QStandardPaths::DataLocation) + QLatin1Char('/') + ALARM_OPTS_FILE); KConfigGroup group(&config, DONT_SHOW_ERRORS_GROUP); const QString id = QStringLiteral("%1:%2").arg(eventId.collectionId()).arg(eventId.eventId()); return group.readEntry(id, QStringList()); } /****************************************************************************** * Check whether the specified Don't-show-again error message tag is set for an * alarm ID. */ bool dontShowErrors(const EventId& eventId, const QString& tag) { if (tag.isEmpty()) return false; - QStringList tags = dontShowErrors(eventId); + const QStringList tags = dontShowErrors(eventId); return tags.indexOf(tag) >= 0; } /****************************************************************************** * Reset the Don't-show-again error message tags for an alarm ID. * If 'tags' is empty, the config entry is deleted. */ void setDontShowErrors(const EventId& eventId, const QStringList& tags) { if (eventId.isEmpty()) return; KConfig config(QStandardPaths::writableLocation(QStandardPaths::DataLocation) + QLatin1Char('/') + ALARM_OPTS_FILE); KConfigGroup group(&config, DONT_SHOW_ERRORS_GROUP); const QString id = QStringLiteral("%1:%2").arg(eventId.collectionId()).arg(eventId.eventId()); if (tags.isEmpty()) group.deleteEntry(id); else group.writeEntry(id, tags); group.sync(); } /****************************************************************************** * Set the specified Don't-show-again error message tag for an alarm ID. * Existing tags are unaffected. */ void setDontShowErrors(const EventId& eventId, const QString& tag) { if (eventId.isEmpty() || tag.isEmpty()) return; KConfig config(QStandardPaths::writableLocation(QStandardPaths::DataLocation) + QLatin1Char('/') + ALARM_OPTS_FILE); KConfigGroup group(&config, DONT_SHOW_ERRORS_GROUP); const QString id = QStringLiteral("%1:%2").arg(eventId.collectionId()).arg(eventId.eventId()); QStringList tags = group.readEntry(id, QStringList()); if (tags.indexOf(tag) < 0) { tags += tag; group.writeEntry(id, tags); group.sync(); } } /****************************************************************************** * 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); 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); 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:/+")); + const 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("showFileErrMessage: 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(); + const 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 + const 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 KCalendarCore::Event); event.updateKCalEvent(kcalEvent, KAEvent::UID_IGNORE); // Change the event ID to avoid duplicating the same unique ID as the original event const QString uid = uidKOrganizer(event.id()); kcalEvent->setUid(uid); kcalEvent->clearAlarms(); QString userEmail; switch (event.actionTypes()) { case KAEvent::ACT_DISPLAY: case KAEvent::ACT_COMMAND: case KAEvent::ACT_DISPLAY_COMMAND: kcalEvent->setSummary(event.cleanText()); userEmail = Preferences::emailAddress(); break; case KAEvent::ACT_EMAIL: { - QString from = event.emailFromId() - ? Identities::identityManager()->identityForUoid(event.emailFromId()).fullEmailAddr() - : Preferences::emailAddress(); + const QString from = event.emailFromId() + ? Identities::identityManager()->identityForUoid(event.emailFromId()).fullEmailAddr() + : Preferences::emailAddress(); AlarmText atext; atext.setEmail(event.emailAddresses(QStringLiteral(", ")), from, QString(), QString(), event.emailSubject(), QString()); kcalEvent->setSummary(atext.displayText()); userEmail = from; break; } case KAEvent::ACT_AUDIO: kcalEvent->setSummary(event.audioFile()); break; default: break; } const Person person(QString(), userEmail); kcalEvent->setOrganizer(person); kcalEvent->setDuration(Duration(Preferences::kOrgEventDuration() * 60, Duration::Seconds)); // Translate the event into string format ICalFormat format; format.setTimeZone(Preferences::timeSpecAsZone()); const QString iCal = format.toICalString(kcalEvent); // Send the event to KOrganizer KAlarm::UpdateResult status = runKOrganizer(); // start KOrganizer if it isn't already running, and create its D-Bus interface if (status != KAlarm::UPDATE_OK) return status; QDBusInterface korgInterface(KORG_DBUS_SERVICE, QStringLiteral(KORG_DBUS_PATH), KORG_DBUS_IFACE); const QList args{iCal}; QDBusReply reply = korgInterface.callWithArgumentList(QDBus::Block, QStringLiteral("addIncidence"), args); if (!reply.isValid()) { if (reply.error().type() == QDBusError::UnknownObject) { status = KAlarm::UPDATE_KORG_ERRSTART; qCCritical(KALARM_LOG) << "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, QString(), newID, true); // this auto-deletes when complete // Ignore errors return KAlarm::UpdateResult(KAlarm::UPDATE_OK); } /****************************************************************************** * Start KOrganizer if not already running, and create its D-Bus interface. */ KAlarm::UpdateResult runKOrganizer() { KAlarm::UpdateResult status; // If Kontact is running, there is a load() method which needs to be called to // load KOrganizer into Kontact. But if KOrganizer is running independently, // the load() method doesn't exist. This call starts korganizer if needed, too. QDBusInterface iface(KORG_DBUS_SERVICE, QStringLiteral(KORG_DBUS_LOAD_PATH), QStringLiteral("org.kde.PIMUniqueApplication")); QDBusReply reply = iface.call(QStringLiteral("load")); - if ((!reply.isValid() || !reply.value()) && iface.lastError().type() != QDBusError::UnknownMethod) + if ((!reply.isValid() || !reply.value()) + && iface.lastError().type() != QDBusError::UnknownMethod) { status.set(KAlarm::UPDATE_KORG_ERR, iface.lastError().message()); qCWarning(KALARM_LOG) << "Loading KOrganizer failed:" << status.message; return status; } return status; } /****************************************************************************** * Insert a KOrganizer string after the hyphen in the supplied event ID. */ QString uidKOrganizer(const QString& id) { if (id.startsWith(KORGANIZER_UID)) return id; QString result = id; return result.insert(0, KORGANIZER_UID); } } // namespace /****************************************************************************** * Case insensitive comparison for use by qSort(). */ bool caseInsensitiveLessThan(const QString& s1, const QString& s2) { return s1.toLower() < s2.toLower(); } // vim: et sw=4: diff --git a/src/itemlistmodel.cpp b/src/itemlistmodel.cpp index 7188fa66..a6c46d20 100644 --- a/src/itemlistmodel.cpp +++ b/src/itemlistmodel.cpp @@ -1,353 +1,332 @@ /* * itemlistmodel.cpp - Akonadi item models * Program: kalarm - * Copyright © 2007-2012 by David Jarvie + * Copyright © 2007-2019 David Jarvie * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "itemlistmodel.h" #include "collectionmodel.h" #include #include using namespace Akonadi; /*============================================================================= = Class: ItemListModel = Filter proxy model containing all items (alarms/templates) of specified mime = types in enabled collections. =============================================================================*/ ItemListModel::ItemListModel(CalEvent::Types allowed, QObject* parent) - : EntityMimeTypeFilterModel(parent), - mAllowedTypes(allowed), - mHaveEvents(false) + : EntityMimeTypeFilterModel(parent) + , mAllowedTypes(allowed) + , mHaveEvents(false) { KSelectionProxyModel* selectionModel = new KSelectionProxyModel(CollectionControlModel::instance()->selectionModel(), this); selectionModel->setSourceModel(AkonadiModel::instance()); selectionModel->setFilterBehavior(KSelectionProxyModel::ChildrenOfExactSelection); setSourceModel(selectionModel); addMimeTypeExclusionFilter(Collection::mimeType()); setHeaderGroup(EntityTreeModel::ItemListHeaders); if (allowed) { const QStringList mimeTypes = CalEvent::mimeTypes(allowed); for (const QString& mime : mimeTypes) addMimeTypeInclusionFilter(mime); } setHeaderGroup(EntityTreeModel::ItemListHeaders); setSortRole(AkonadiModel::SortRole); setDynamicSortFilter(true); connect(this, &ItemListModel::rowsInserted, this, &ItemListModel::slotRowsInserted); connect(this, &ItemListModel::rowsRemoved, this, &ItemListModel::slotRowsRemoved); connect(AkonadiModel::instance(), &AkonadiModel::collectionStatusChanged, this, &ItemListModel::collectionStatusChanged); } int ItemListModel::columnCount(const QModelIndex& /*parent*/) const { return AkonadiModel::ColumnCount; } /****************************************************************************** * Called when rows have been inserted into the model. */ void ItemListModel::slotRowsInserted() { if (!mHaveEvents && rowCount()) { mHaveEvents = true; Q_EMIT haveEventsStatus(true); } } /****************************************************************************** * Called when rows have been deleted from the model. */ void ItemListModel::slotRowsRemoved() { if (mHaveEvents && !rowCount()) { mHaveEvents = false; Q_EMIT haveEventsStatus(false); } } /****************************************************************************** * Called when a collection parameter or status has changed. * If the collection's enabled status has changed, re-filter the list to add or * remove its alarms. */ void ItemListModel::collectionStatusChanged(const Collection& collection, AkonadiModel::Change change, const QVariant&, bool inserted) { Q_UNUSED(inserted); if (!collection.isValid()) return; if (change == AkonadiModel::Enabled) { // Ensure that items for a newly enabled collection are always ordered // correctly. Note that invalidateFilter() is not adequate for this. invalidate(); } } bool ItemListModel::filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const { if (!EntityMimeTypeFilterModel::filterAcceptsRow(sourceRow, sourceParent)) return false; // Get the alarm type of the item - QModelIndex sourceIndex = sourceModel()->index(sourceRow, 0, sourceParent); - CalEvent::Type type = static_cast(sourceModel()->data(sourceIndex, AkonadiModel::StatusRole).toInt()); - Collection parent = sourceIndex.data(AkonadiModel::ParentCollectionRole).value(); + const QModelIndex sourceIndex = sourceModel()->index(sourceRow, 0, sourceParent); + const CalEvent::Type type = static_cast(sourceModel()->data(sourceIndex, AkonadiModel::StatusRole).toInt()); + const Collection parent = sourceIndex.data(AkonadiModel::ParentCollectionRole).value(); return CollectionControlModel::isEnabled(parent, type); } -#if 0 -QModelIndex ItemListModel::index(int row, int column, const QModelIndex& parent) const -{ - if (parent.isValid()) - return QModelIndex(); - return createIndex(row, column, mEvents[row]); -} - -bool ItemListModel::setData(const QModelIndex& ix, const QVariant&, int role) -{ - if (ix.isValid() && role == Qt::EditRole) - { -//??? update event - int row = ix.row(); - Q_EMIT dataChanged(index(row, 0), index(row, AkonadiModel::ColumnCount - 1)); - return true; - } - return false; -} -#endif - Qt::ItemFlags ItemListModel::flags(const QModelIndex& index) const { if (!index.isValid()) return Qt::ItemIsEnabled; return Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsEditable | Qt::ItemIsDragEnabled; } /****************************************************************************** * Return the index to a specified event. */ QModelIndex ItemListModel::eventIndex(Item::Id itemId) const { - QModelIndexList list = match(QModelIndex(), AkonadiModel::ItemIdRole, itemId, 1, Qt::MatchExactly | Qt::MatchRecursive); + const QModelIndexList list = match(QModelIndex(), AkonadiModel::ItemIdRole, itemId, 1, Qt::MatchExactly | Qt::MatchRecursive); if (list.isEmpty()) return QModelIndex(); return index(list[0].row(), 0, list[0].parent()); } /****************************************************************************** * Return the event in a specified row. */ KAEvent ItemListModel::event(int row) const { return event(index(row, 0)); } /****************************************************************************** * Return the event referred to by an index. */ KAEvent ItemListModel::event(const QModelIndex& index) const { return static_cast(sourceModel())->event(mapToSource(index)); } /****************************************************************************** * Check whether the model contains any events. */ bool ItemListModel::haveEvents() const { return rowCount(); } /*============================================================================= = Class: AlarmListModel = Filter proxy model containing all alarms (not templates) of specified mime = types in enabled collections. Equivalent to AlarmListFilterModel =============================================================================*/ AlarmListModel* AlarmListModel::mAllInstance = nullptr; AlarmListModel::AlarmListModel(QObject* parent) - : ItemListModel(CalEvent::ACTIVE | CalEvent::ARCHIVED, parent), - mFilterTypes(CalEvent::ACTIVE | CalEvent::ARCHIVED) + : ItemListModel(CalEvent::ACTIVE | CalEvent::ARCHIVED, parent) + , mFilterTypes(CalEvent::ACTIVE | CalEvent::ARCHIVED) { } AlarmListModel::~AlarmListModel() { if (this == mAllInstance) mAllInstance = nullptr; } AlarmListModel* AlarmListModel::all() { if (!mAllInstance) { mAllInstance = new AlarmListModel(AkonadiModel::instance()); mAllInstance->sort(TimeColumn, Qt::AscendingOrder); } return mAllInstance; } void AlarmListModel::setEventTypeFilter(CalEvent::Types types) { // Ensure that the filter isn't applied to the 'all' instance, and that // 'types' doesn't include any disallowed alarm types if (!types) types = includedTypes(); if (this != mAllInstance && types != mFilterTypes && (types & includedTypes()) == types) { mFilterTypes = types; invalidateFilter(); } } bool AlarmListModel::filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const { if (!ItemListModel::filterAcceptsRow(sourceRow, sourceParent)) return false; if (mFilterTypes == CalEvent::EMPTY) return false; - int type = sourceModel()->data(sourceModel()->index(sourceRow, 0, sourceParent), AkonadiModel::StatusRole).toInt(); + const int type = sourceModel()->data(sourceModel()->index(sourceRow, 0, sourceParent), AkonadiModel::StatusRole).toInt(); return static_cast(type) & mFilterTypes; } bool AlarmListModel::filterAcceptsColumn(int sourceCol, const QModelIndex&) const { return (sourceCol != AkonadiModel::TemplateNameColumn); } QVariant AlarmListModel::headerData(int section, Qt::Orientation orientation, int role) const { if (orientation == Qt::Horizontal) { if (section < 0 || section >= ColumnCount) return QVariant(); } return ItemListModel::headerData(section, orientation, role); } /*============================================================================= = Class: TemplateListModel = Filter proxy model containing all alarm templates for specified alarm types = in enabled collections. Equivalent to TemplateListFilterModel =============================================================================*/ TemplateListModel* TemplateListModel::mAllInstance = nullptr; TemplateListModel::TemplateListModel(QObject* parent) - : ItemListModel(CalEvent::TEMPLATE, parent), - mActionsEnabled(KAEvent::ACT_ALL), - mActionsFilter(KAEvent::ACT_ALL) + : ItemListModel(CalEvent::TEMPLATE, parent) + , mActionsEnabled(KAEvent::ACT_ALL) + , mActionsFilter(KAEvent::ACT_ALL) { } TemplateListModel::~TemplateListModel() { if (this == mAllInstance) mAllInstance = nullptr; } TemplateListModel* TemplateListModel::all() { if (!mAllInstance) { mAllInstance = new TemplateListModel(AkonadiModel::instance()); mAllInstance->sort(TemplateNameColumn, Qt::AscendingOrder); } return mAllInstance; } void TemplateListModel::setAlarmActionFilter(KAEvent::Actions types) { // Ensure that the filter isn't applied to the 'all' instance. if (this != mAllInstance && types != mActionsFilter) { mActionsFilter = types; invalidateFilter(); } } void TemplateListModel::setAlarmActionsEnabled(KAEvent::Actions types) { // Ensure that the setting isn't applied to the 'all' instance. if (this != mAllInstance && types != mActionsEnabled) { mActionsEnabled = types; invalidateFilter(); } } bool TemplateListModel::filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const { if (!ItemListModel::filterAcceptsRow(sourceRow, sourceParent)) return false; if (mActionsFilter == KAEvent::ACT_ALL) return true; - QModelIndex sourceIndex = sourceModel()->index(sourceRow, 0, sourceParent); - KAEvent::Actions actions = static_cast(sourceModel()->data(sourceIndex, AkonadiModel::AlarmActionsRole).toInt()); + const QModelIndex sourceIndex = sourceModel()->index(sourceRow, 0, sourceParent); + const KAEvent::Actions actions = static_cast(sourceModel()->data(sourceIndex, AkonadiModel::AlarmActionsRole).toInt()); return actions & mActionsFilter; } bool TemplateListModel::filterAcceptsColumn(int sourceCol, const QModelIndex&) const { return sourceCol == AkonadiModel::TemplateNameColumn || sourceCol == AkonadiModel::TypeColumn; } QVariant TemplateListModel::headerData(int section, Qt::Orientation orientation, int role) const { if (orientation == Qt::Horizontal) { switch (section) { case TypeColumn: section = AkonadiModel::TypeColumn; break; case TemplateNameColumn: section = AkonadiModel::TemplateNameColumn; break; default: return QVariant(); } } return ItemListModel::headerData(section, orientation, role); } Qt::ItemFlags TemplateListModel::flags(const QModelIndex& index) const { Qt::ItemFlags f = sourceModel()->flags(mapToSource(index)); if (mActionsEnabled == KAEvent::ACT_ALL) return f; - KAEvent::Actions actions = static_cast(ItemListModel::data(index, AkonadiModel::AlarmActionsRole).toInt()); + const KAEvent::Actions actions = static_cast(ItemListModel::data(index, AkonadiModel::AlarmActionsRole).toInt()); if (!(actions & mActionsEnabled)) f = static_cast(f & ~(Qt::ItemIsEnabled | Qt::ItemIsSelectable)); return f; } // vim: et sw=4: diff --git a/src/kalarmapp.cpp b/src/kalarmapp.cpp index 45532cea..0eb85248 100644 --- a/src/kalarmapp.cpp +++ b/src/kalarmapp.cpp @@ -1,2501 +1,2496 @@ /* * kalarmapp.cpp - the KAlarm application object * Program: kalarm - * Copyright © 2001-2018 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" #include "kalarmapp.h" #include "alarmcalendar.h" #include "alarmlistview.h" #include "alarmtime.h" #include "commandoptions.h" #include "dbushandler.h" #include "editdlgtypes.h" #include "collectionmodel.h" #include "functions.h" #include "kamail.h" #include "mainwindow.h" #include "messagebox.h" #include "messagewin.h" #include "kalarmmigrateapplication.h" #include "preferences.h" #include "prefdlg.h" #include "shellprocess.h" #include "startdaytimer.h" #include "traywindow.h" #include "kalarm_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include static const int AKONADI_TIMEOUT = 30; // timeout (seconds) for Akonadi collections to be populated /****************************************************************************** * Find the maximum number of seconds late which a late-cancel alarm is allowed * to be. This is calculated as the late cancel interval, plus a few seconds * leeway to cater for any timing irregularities. */ static inline int maxLateness(int lateCancel) { static const int LATENESS_LEEWAY = 5; int lc = (lateCancel >= 1) ? (lateCancel - 1)*60 : 0; return LATENESS_LEEWAY + lc; } KAlarmApp* KAlarmApp::mInstance = nullptr; int KAlarmApp::mActiveCount = 0; int KAlarmApp::mFatalError = 0; QString KAlarmApp::mFatalMessage; /****************************************************************************** * Construct the application. */ KAlarmApp::KAlarmApp(int& argc, char** argv) - : QApplication(argc, argv), - mInitialised(false), - mRedisplayAlarms(false), - mQuitting(false), - mReadOnly(false), - mLoginAlarmsDone(false), - mDBusHandler(new DBusHandler()), - mTrayWindow(nullptr), - mAlarmTimer(nullptr), - mArchivedPurgeDays(-1), // default to not purging - mPurgeDaysQueued(-1), - mPendingQuit(false), - mCancelRtcWake(false), - mProcessingQueue(false), - mAlarmsEnabled(true) + : QApplication(argc, argv) + , mInitialised(false) + , mRedisplayAlarms(false) + , mQuitting(false) + , mReadOnly(false) + , mLoginAlarmsDone(false) + , mDBusHandler(new DBusHandler()) + , mTrayWindow(nullptr) + , mAlarmTimer(nullptr) + , mArchivedPurgeDays(-1) // default to not purging + , mPurgeDaysQueued(-1) + , mPendingQuit(false) + , mCancelRtcWake(false) + , mProcessingQueue(false) + , mAlarmsEnabled(true) { qCDebug(KALARM_LOG) << "KAlarmApp:"; KAlarmMigrateApplication migrate; migrate.migrate(); #ifndef NDEBUG KAlarm::setTestModeConditions(); #endif setQuitOnLastWindowClosed(false); Preferences::self(); // read KAlarm configuration if (!Preferences::noAutoStart()) { // Strip out any "OnlyShowIn=KDE" list from kalarm.autostart.desktop Preferences::setNoAutoStart(false); // Enable kalarm.autostart.desktop to start KAlarm Preferences::setAutoStart(true); Preferences::self()->save(); } Preferences::connect(SIGNAL(startOfDayChanged(QTime)), this, SLOT(changeStartOfDay())); Preferences::connect(SIGNAL(workTimeChanged(QTime,QTime,QBitArray)), this, SLOT(slotWorkTimeChanged(QTime,QTime,QBitArray))); Preferences::connect(SIGNAL(holidaysChanged(KHolidays::HolidayRegion)), this, SLOT(slotHolidaysChanged(KHolidays::HolidayRegion))); Preferences::connect(SIGNAL(feb29TypeChanged(Feb29Type)), this, SLOT(slotFeb29TypeChanged(Feb29Type))); Preferences::connect(SIGNAL(showInSystemTrayChanged(bool)), this, SLOT(slotShowInSystemTrayChanged())); Preferences::connect(SIGNAL(archivedKeepDaysChanged(int)), this, SLOT(setArchivePurgeDays())); Preferences::connect(SIGNAL(messageFontChanged(QFont)), this, SLOT(slotMessageFontChanged(QFont))); slotFeb29TypeChanged(Preferences::defaultFeb29Type()); KAEvent::setStartOfDay(Preferences::startOfDay()); KAEvent::setWorkTime(Preferences::workDays(), Preferences::workDayStart(), Preferences::workDayEnd()); KAEvent::setHolidays(Preferences::holidays()); KAEvent::setDefaultFont(Preferences::messageFont()); if (initialise()) // initialise calendars and alarm timer { connect(AkonadiModel::instance(), &AkonadiModel::collectionAdded, this, &KAlarmApp::purgeNewArchivedDefault); connect(AkonadiModel::instance(), &Akonadi::EntityTreeModel::collectionTreeFetched, this, &KAlarmApp::checkWritableCalendar); connect(AkonadiModel::instance(), &AkonadiModel::migrationCompleted, this, &KAlarmApp::checkWritableCalendar); KConfigGroup config(KSharedConfig::openConfig(), "General"); mNoSystemTray = config.readEntry("NoSystemTray", false); mOldShowInSystemTray = wantShowInSystemTray(); DateTime::setStartOfDay(Preferences::startOfDay()); mPrefsArchivedColour = Preferences::archivedColour(); } // Check if KOrganizer is installed const QString korg = QStringLiteral("korganizer"); mKOrganizerEnabled = !QStandardPaths::findExecutable(korg).isEmpty(); if (!mKOrganizerEnabled) { qCDebug(KALARM_LOG) << "KAlarmApp: KOrganizer options disabled (KOrganizer not found)"; } // Check if the window manager can't handle keyboard focus transfer between windows mWindowFocusBroken = (KAlarm::currentDesktopIdentity() == KAlarm::Desktop::Unity); if (mWindowFocusBroken) { qCDebug(KALARM_LOG) << "KAlarmApp: Window keyboard focus broken"; } } /****************************************************************************** */ KAlarmApp::~KAlarmApp() { while (!mCommandProcesses.isEmpty()) { - ProcData* pd = mCommandProcesses[0]; + ProcData* pd = mCommandProcesses.at(0); mCommandProcesses.pop_front(); delete pd; } AlarmCalendar::terminateCalendars(); } /****************************************************************************** * Return the one and only KAlarmApp instance. * If it doesn't already exist, it is created first. */ KAlarmApp* KAlarmApp::create(int& argc, char** argv) { if (!mInstance) { mInstance = new KAlarmApp(argc, argv); if (mFatalError) mInstance->quitFatal(); } return mInstance; } /****************************************************************************** * (Re)initialise things which are tidied up/closed by quitIf(). * Reinitialisation can be necessary if session restoration finds nothing to * restore and starts quitting the application, but KAlarm then starts up again * before the application has exited. * Reply = true if calendars were initialised successfully, * false if they were already initialised, or if initialisation failed. */ bool KAlarmApp::initialise() { if (!mAlarmTimer) { mAlarmTimer = new QTimer(this); mAlarmTimer->setSingleShot(true); connect(mAlarmTimer, &QTimer::timeout, this, &KAlarmApp::checkNextDueAlarm); } if (!AlarmCalendar::resources()) { qCDebug(KALARM_LOG) << "KAlarmApp::initialise: initialising calendars"; if (AlarmCalendar::initialiseCalendars()) { connect(AlarmCalendar::resources(), &AlarmCalendar::earliestAlarmChanged, this, &KAlarmApp::checkNextDueAlarm); connect(AlarmCalendar::resources(), &AlarmCalendar::atLoginEventAdded, this, &KAlarmApp::atLoginEventAdded); return true; } } return false; } /****************************************************************************** * Restore the saved session if required. */ bool KAlarmApp::restoreSession() { if (!isSessionRestored()) return false; if (mFatalError) { quitFatal(); return false; } // Process is being restored by session management. qCDebug(KALARM_LOG) << "KAlarmApp::restoreSession: Restoring"; ++mActiveCount; // Create the session config object now. // This is necessary since if initCheck() below causes calendars to be updated, // the session config created after that points to an invalid file, resulting // in no windows being restored followed by a later crash. KConfigGui::sessionConfig(); // When KAlarm is session restored, automatically set start-at-login to true. Preferences::self()->load(); Preferences::setAutoStart(true); Preferences::setNoAutoStart(false); Preferences::setAskAutoStart(true); // cancel any start-at-login prompt suppression Preferences::self()->save(); if (!initCheck(true)) // open the calendar file (needed for main windows), don't process queue yet { --mActiveCount; quitIf(1, true); // error opening the main calendar - quit return false; } MainWindow* trayParent = nullptr; for (int i = 1; KMainWindow::canBeRestored(i); ++i) { const QString type = KMainWindow::classNameOfToplevel(i); if (type == QStringLiteral("MainWindow")) { MainWindow* win = MainWindow::create(true); win->restore(i, false); if (win->isHiddenTrayParent()) trayParent = win; else win->show(); } else if (type == QStringLiteral("MessageWin")) { MessageWin* win = new MessageWin; win->restore(i, false); if (win->isValid()) { if (AkonadiModel::instance()->isCollectionTreeFetched()) win->show(); } else delete win; } } // Try to display the system tray icon if it is configured to be shown if (trayParent || wantShowInSystemTray()) { if (!MainWindow::count()) qCWarning(KALARM_LOG) << "KAlarmApp::restoreSession: no main window to be restored!?"; else { displayTrayIcon(true, trayParent); // Occasionally for no obvious reason, the main main window is // shown when it should be hidden, so hide it just to be sure. if (trayParent) trayParent->hide(); } } --mActiveCount; if (quitIf(0)) // quit if no windows are open return false; // quitIf() can sometimes return, despite calling exit() startProcessQueue(); // start processing the execution queue return true; } /****************************************************************************** * Called for a unique QApplication when a new instance of the application is * started. * Reply: exit code (>= 0), or -1 to continue execution. * If exit code >= 0, 'outputText' holds text to output before terminating. */ void KAlarmApp::activateByDBus(const QStringList& args, const QString& workingDirectory) { activateInstance(args, workingDirectory, nullptr); } /****************************************************************************** * Called to start a new instance of the application. * Reply: exit code (>= 0), or -1 to continue execution. * If exit code >= 0, 'outputText' holds text to output before terminating. */ int KAlarmApp::activateInstance(const QStringList& args, const QString& workingDirectory, QString* outputText) { Q_UNUSED(workingDirectory) qCDebug(KALARM_LOG) << "KAlarmApp::activateInstance"; if (outputText) outputText->clear(); if (mFatalError) { quitFatal(); return 1; } // The D-Bus call to activate a subsequent instance of KAlarm may not supply // any arguments, but we need one. if (!args.isEmpty() && mActivateArg0.isEmpty()) mActivateArg0 = args[0]; QStringList fixedArgs(args); if (args.isEmpty() && !mActivateArg0.isEmpty()) fixedArgs << mActivateArg0; // Parse and interpret command line arguments. QCommandLineParser parser; KAboutData::applicationData().setupCommandLine(&parser); parser.setApplicationDescription(QApplication::applicationDisplayName()); CommandOptions* options = new CommandOptions; const QStringList newArgs = options->setOptions(&parser, fixedArgs); options->parse(); KAboutData::applicationData().processCommandLine(&parser); ++mActiveCount; int exitCode = 0; // default = success static bool firstInstance = true; bool dontRedisplay = false; CommandOptions::Command command = CommandOptions::NONE; bool processOptions = (!firstInstance || !isSessionRestored()); if (processOptions) { options->process(); #ifndef NDEBUG if (options->simulationTime().isValid()) KAlarm::setSimulatedSystemTime(options->simulationTime()); #endif command = options->command(); if (options->disableAll()) setAlarmsEnabled(false); // disable alarm monitoring // Handle options which exit with a terminal message, before // making the application a unique application, since a // unique application won't output to the terminal if another // instance is already running. switch (command) { case CommandOptions::CMD_ERROR: if (outputText) { *outputText = options->outputText(); delete options; return 1; } mReadOnly = true; // don't need write access to calendars exitCode = 1; break; case CommandOptions::EXIT: if (outputText) { *outputText = options->outputText(); delete options; return 0; } exitCode = -1; break; default: break; } } // Make this a unique application. KDBusService* s = new KDBusService(KDBusService::Unique, this); connect(this, &KAlarmApp::aboutToQuit, s, &KDBusService::deleteLater); connect(s, &KDBusService::activateRequested, this, &KAlarmApp::activateByDBus); if (processOptions) { switch (command) { case CommandOptions::TRIGGER_EVENT: case CommandOptions::CANCEL_EVENT: { // Display or delete the event with the specified event ID EventFunc function = (command == CommandOptions::TRIGGER_EVENT) ? EVENT_TRIGGER : EVENT_CANCEL; // Open the calendar, don't start processing execution queue yet, // and wait for the Akonadi collection to be populated. if (!initCheck(true, true, options->eventId().collectionId())) exitCode = 1; else { startProcessQueue(); // start processing the execution queue dontRedisplay = true; if (!handleEvent(options->eventId(), function, true)) { CommandOptions::printError(xi18nc("@info:shell", "%1: Event %2 not found, or not unique", QStringLiteral("--") + options->commandName(), options->eventId().eventId())); exitCode = 1; } } break; } case CommandOptions::LIST: // Output a list of scheduled alarms to stdout. // Open the calendar, don't start processing execution queue yet, // and wait for all Akonadi collections to be populated. mReadOnly = true; // don't need write access to calendars if (!initCheck(true, true)) exitCode = 1; else { dontRedisplay = true; - QStringList alarms = scheduledAlarmList(); - for (int i = 0, count = alarms.count(); i < count; ++i) - std::cout << alarms[i].toUtf8().constData() << std::endl; + const QStringList alarms = scheduledAlarmList(); + for (const QString& alarm : alarms) + std::cout << alarm.toUtf8().constData() << std::endl; } break; case CommandOptions::EDIT: // Edit a specified existing alarm. // Open the calendar and wait for the Akonadi collection to be populated. if (!initCheck(false, true, options->eventId().collectionId())) exitCode = 1; else if (!KAlarm::editAlarmById(options->eventId())) { CommandOptions::printError(xi18nc("@info:shell", "%1: Event %2 not found, or not editable", QStringLiteral("--") + options->commandName(), options->eventId().eventId())); exitCode = 1; } break; case CommandOptions::EDIT_NEW: { // Edit a new alarm, and optionally preset selected values if (!initCheck()) exitCode = 1; else { EditAlarmDlg* editDlg = EditAlarmDlg::create(false, options->editType(), MainWindow::mainMainWindow()); if (options->alarmTime().isValid()) editDlg->setTime(options->alarmTime()); if (options->recurrence()) editDlg->setRecurrence(*options->recurrence(), options->subRepeatInterval(), options->subRepeatCount()); else if (options->flags() & KAEvent::REPEAT_AT_LOGIN) editDlg->setRepeatAtLogin(); editDlg->setAction(options->editAction(), AlarmText(options->text())); if (options->lateCancel()) editDlg->setLateCancel(options->lateCancel()); if (options->flags() & KAEvent::COPY_KORGANIZER) editDlg->setShowInKOrganizer(true); switch (options->editType()) { case EditAlarmDlg::DISPLAY: { // EditAlarmDlg::create() always returns EditDisplayAlarmDlg for type = DISPLAY EditDisplayAlarmDlg* dlg = qobject_cast(editDlg); if (options->fgColour().isValid()) dlg->setFgColour(options->fgColour()); if (options->bgColour().isValid()) dlg->setBgColour(options->bgColour()); if (!options->audioFile().isEmpty() || options->flags() & (KAEvent::BEEP | KAEvent::SPEAK)) { KAEvent::Flags flags = options->flags(); Preferences::SoundType type = (flags & KAEvent::BEEP) ? Preferences::Sound_Beep : (flags & KAEvent::SPEAK) ? Preferences::Sound_Speak : Preferences::Sound_File; dlg->setAudio(type, options->audioFile(), options->audioVolume(), (flags & KAEvent::REPEAT_SOUND ? 0 : -1)); } if (options->reminderMinutes()) dlg->setReminder(options->reminderMinutes(), (options->flags() & KAEvent::REMINDER_ONCE)); if (options->flags() & KAEvent::CONFIRM_ACK) dlg->setConfirmAck(true); if (options->flags() & KAEvent::AUTO_CLOSE) dlg->setAutoClose(true); break; } case EditAlarmDlg::COMMAND: break; case EditAlarmDlg::EMAIL: { // EditAlarmDlg::create() always returns EditEmailAlarmDlg for type = EMAIL EditEmailAlarmDlg* dlg = qobject_cast(editDlg); if (options->fromID() || !options->addressees().isEmpty() || !options->subject().isEmpty() || !options->attachments().isEmpty()) dlg->setEmailFields(options->fromID(), options->addressees(), options->subject(), options->attachments()); if (options->flags() & KAEvent::EMAIL_BCC) dlg->setBcc(true); break; } case EditAlarmDlg::AUDIO: { // EditAlarmDlg::create() always returns EditAudioAlarmDlg for type = AUDIO EditAudioAlarmDlg* dlg = qobject_cast(editDlg); if (!options->audioFile().isEmpty() || options->audioVolume() >= 0) dlg->setAudio(options->audioFile(), options->audioVolume()); break; } case EditAlarmDlg::NO_TYPE: break; } // Execute the edit dialogue. Note that if no other instance of KAlarm is // running, this new instance will not exit after the dialogue is closed. // This is deliberate, since exiting would mean that KAlarm wouldn't // trigger the new alarm. KAlarm::execNewAlarmDlg(editDlg); } break; } case CommandOptions::EDIT_NEW_PRESET: // Edit a new alarm, preset with a template if (!initCheck()) exitCode = 1; else { // Execute the edit dialogue. Note that if no other instance of KAlarm is // running, this new instance will not exit after the dialogue is closed. // This is deliberate, since exiting would mean that KAlarm wouldn't // trigger the new alarm. KAlarm::editNewAlarm(options->templateName()); } break; case CommandOptions::NEW: // Display a message or file, execute a command, or send an email if (!initCheck() || !scheduleEvent(options->editAction(), options->text(), options->alarmTime(), options->lateCancel(), options->flags(), options->bgColour(), options->fgColour(), QFont(), options->audioFile(), options->audioVolume(), options->reminderMinutes(), (options->recurrence() ? *options->recurrence() : KARecurrence()), options->subRepeatInterval(), options->subRepeatCount(), options->fromID(), options->addressees(), options->subject(), options->attachments())) exitCode = 1; break; case CommandOptions::TRAY: // Display only the system tray icon if (Preferences::showInSystemTray() && QSystemTrayIcon::isSystemTrayAvailable()) { if (!initCheck() // open the calendar, start processing execution queue || !displayTrayIcon(true)) exitCode = 1; break; } // fall through to NONE Q_FALLTHROUGH(); case CommandOptions::NONE: // No arguments - run interactively & display the main window #ifndef NDEBUG if (options->simulationTime().isValid() && !firstInstance) break; // simulating time: don't open main window if already running #endif if (!initCheck()) exitCode = 1; else { if (mTrayWindow && mTrayWindow->assocMainWindow() && !mTrayWindow->assocMainWindow()->isVisible()) mTrayWindow->showAssocMainWindow(); else { MainWindow* win = MainWindow::create(); if (command == CommandOptions::TRAY) win->setWindowState(win->windowState() | Qt::WindowMinimized); win->show(); } } break; default: break; } } if (options != CommandOptions::firstInstance()) delete options; // If this is the first time through, redisplay any alarm message windows // from last time. if (firstInstance && !dontRedisplay && !exitCode) { /* First time through, so redisplay alarm message windows from last time. * But it is possible for session restoration in some circumstances to * not create any windows, in which case the alarm calendars will have * been deleted - if so, don't try to do anything. (This has been known * to happen under the Xfce desktop.) */ if (AlarmCalendar::resources()) { if (AkonadiModel::instance()->isCollectionTreeFetched()) { mRedisplayAlarms = false; MessageWin::redisplayAlarms(); } else mRedisplayAlarms = true; } } --mActiveCount; firstInstance = false; // Quit the application if this was the last/only running "instance" of the program. // Executing 'return' doesn't work very well since the program continues to // run if no windows were created. quitIf(exitCode >= 0 ? exitCode : 0); return -1; // continue executing the application instance } /****************************************************************************** * Quit the program, optionally only if there are no more "instances" running. * Reply = true if program exited. */ bool KAlarmApp::quitIf(int exitCode, bool force) { if (force) { // Quit regardless, except for message windows mQuitting = true; MainWindow::closeAll(); mQuitting = false; displayTrayIcon(false); if (MessageWin::instanceCount(true)) // ignore always-hidden windows (e.g. audio alarms) return false; } else if (mQuitting) return false; // MainWindow::closeAll() causes quitIf() to be called again else { // Quit only if there are no more "instances" running mPendingQuit = false; if (mActiveCount > 0 || MessageWin::instanceCount(true)) // ignore always-hidden windows (e.g. audio alarms) return false; int mwcount = MainWindow::count(); MainWindow* mw = mwcount ? MainWindow::firstWindow() : nullptr; if (mwcount > 1 || (mwcount && (!mw->isHidden() || !mw->isTrayParent()))) return false; // There are no windows left except perhaps a main window which is a hidden // tray icon parent, or an always-hidden message window. if (mTrayWindow) { // There is a system tray icon. // Don't exit unless the system tray doesn't seem to exist. if (checkSystemTray()) return false; } if (!mActionQueue.isEmpty() || !mCommandProcesses.isEmpty()) { // Don't quit yet if there are outstanding actions on the execution queue mPendingQuit = true; mPendingQuitCode = exitCode; return false; } } // This was the last/only running "instance" of the program, so exit completely. // NOTE: Everything which is terminated/deleted here must where applicable // be initialised in the initialise() method, in case KAlarm is // started again before application exit completes! qCDebug(KALARM_LOG) << "KAlarmApp::quitIf:" << exitCode << ": quitting"; MessageWin::stopAudio(true); if (mCancelRtcWake) { KAlarm::setRtcWakeTime(0, nullptr); KAlarm::deleteRtcWakeConfig(); } delete mAlarmTimer; // prevent checking for alarms after deleting calendars mAlarmTimer = nullptr; mInitialised = false; // prevent processQueue() from running AlarmCalendar::terminateCalendars(); exit(exitCode); return true; // sometimes we actually get to here, despite calling exit() } /****************************************************************************** * Called when the Quit menu item is selected. * Closes the system tray window and all main windows, but does not exit the * program if other windows are still open. */ void KAlarmApp::doQuit(QWidget* parent) { qCDebug(KALARM_LOG) << "KAlarmApp::doQuit"; if (KAMessageBox::warningCancelContinue(parent, i18nc("@info", "Quitting will disable alarms (once any alarm message windows are closed)."), QString(), KStandardGuiItem::quit(), KStandardGuiItem::cancel(), Preferences::QUIT_WARN ) != KMessageBox::Continue) return; if (!KAlarm::checkRtcWakeConfig(true).isEmpty()) { // A wake-on-suspend alarm is set if (KAMessageBox::warningCancelContinue(parent, i18nc("@info", "Quitting will cancel the scheduled Wake from Suspend."), QString(), KStandardGuiItem::quit() ) != KMessageBox::Continue) return; mCancelRtcWake = true; } if (!Preferences::autoStart()) { int option = KMessageBox::No; if (!Preferences::autoStartChangedByUser()) { option = KAMessageBox::questionYesNoCancel(parent, xi18nc("@info", "Do you want to start KAlarm at login?" "(Note that alarms will be disabled if KAlarm is not started.)"), QString(), KStandardGuiItem::yes(), KStandardGuiItem::no(), KStandardGuiItem::cancel(), Preferences::ASK_AUTO_START); } switch (option) { case KMessageBox::Yes: Preferences::setAutoStart(true); Preferences::setNoAutoStart(false); break; case KMessageBox::No: Preferences::setNoAutoStart(true); break; case KMessageBox::Cancel: default: return; } Preferences::self()->save(); } quitIf(0, true); } /****************************************************************************** * Display an error message for a fatal error. Prevent further actions since * the program state is unsafe. */ void KAlarmApp::displayFatalError(const QString& message) { if (!mFatalError) { mFatalError = 1; mFatalMessage = message; if (mInstance) QTimer::singleShot(0, mInstance, &KAlarmApp::quitFatal); } } /****************************************************************************** * Quit the program, once the fatal error message has been acknowledged. */ void KAlarmApp::quitFatal() { switch (mFatalError) { case 0: case 2: return; case 1: mFatalError = 2; KMessageBox::error(nullptr, mFatalMessage); // this is an application modal window mFatalError = 3; // fall through to '3' Q_FALLTHROUGH(); case 3: if (mInstance) mInstance->quitIf(1, true); break; } QTimer::singleShot(1000, this, &KAlarmApp::quitFatal); } /****************************************************************************** * Called by the alarm timer when the next alarm is due. * Also called when the execution queue has finished processing to check for the * next alarm. */ void KAlarmApp::checkNextDueAlarm() { if (!mAlarmsEnabled) return; // Find the first alarm due const KAEvent* nextEvent = AlarmCalendar::resources()->earliestAlarm(); if (!nextEvent) return; // there are no alarms pending const KADateTime nextDt = nextEvent->nextTrigger(KAEvent::ALL_TRIGGER).effectiveKDateTime(); const KADateTime now = KADateTime::currentDateTime(Preferences::timeSpec()); qint64 interval = now.msecsTo(nextDt); qCDebug(KALARM_LOG) << "KAlarmApp::checkNextDueAlarm: now:" << qPrintable(now.toString(QStringLiteral("%Y-%m-%d %H:%M %:Z"))) << ", next:" << qPrintable(nextDt.toString(QStringLiteral("%Y-%m-%d %H:%M %:Z"))) << ", due:" << interval; if (interval <= 0) { // Queue the alarm queueAlarmId(*nextEvent); qCDebug(KALARM_LOG) << "KAlarmApp::checkNextDueAlarm:" << nextEvent->id() << ": due now"; QTimer::singleShot(0, this, &KAlarmApp::processQueue); } else { // No alarm is due yet, so set timer to wake us when it's due. // Check for integer overflow before setting timer. +#pragma message("TODO: use hibernation wakeup signal") #ifndef HIBERNATION_SIGNAL /* TODO: REPLACE THIS CODE WHEN A SYSTEM NOTIFICATION SIGNAL BECOMES * AVAILABLE FOR WAKEUP FROM HIBERNATION. * Re-evaluate the next alarm time every minute, in case the * system clock jumps. The most common case when the clock jumps * is when a laptop wakes from hibernation. If timers were left to * run, they would trigger late by the length of time the system * was asleep. */ if (interval > 60000) // 1 minute interval = 60000; #endif ++interval; // ensure we don't trigger just before the minute boundary if (interval > INT_MAX) interval = INT_MAX; qCDebug(KALARM_LOG) << "KAlarmApp::checkNextDueAlarm:" << nextEvent->id() << "wait" << interval/1000 << "seconds"; mAlarmTimer->start(static_cast(interval)); } } /****************************************************************************** * Called by the alarm timer when the next alarm is due. * Also called when the execution queue has finished processing to check for the * next alarm. */ void KAlarmApp::queueAlarmId(const KAEvent& event) { EventId id(event); - for (int i = 0, end = mActionQueue.count(); i < end; ++i) + for (const ActionQEntry& entry : qAsConst(mActionQueue)) { - if (mActionQueue[i].function == EVENT_HANDLE && mActionQueue[i].eventId == id) + if (entry.function == EVENT_HANDLE && entry.eventId == id) return; // the alarm is already queued } mActionQueue.enqueue(ActionQEntry(EVENT_HANDLE, id)); } /****************************************************************************** * Start processing the execution queue. */ void KAlarmApp::startProcessQueue() { if (!mInitialised) { qCDebug(KALARM_LOG) << "KAlarmApp::startProcessQueue"; mInitialised = true; QTimer::singleShot(0, this, &KAlarmApp::processQueue); // process anything already queued } } /****************************************************************************** * The main processing loop for KAlarm. * All KAlarm operations involving opening or updating calendar files are called * from this loop to ensure that only one operation is active at any one time. * This precaution is necessary because KAlarm's activities are mostly * asynchronous, being in response to D-Bus calls from other programs or timer * events, any of which can be received in the middle of performing another * operation. If a calendar file is opened or updated while another calendar * operation is in progress, the program has been observed to hang, or the first * calendar call has failed with data loss - clearly unacceptable!! */ void KAlarmApp::processQueue() { if (mInitialised && !mProcessingQueue) { qCDebug(KALARM_LOG) << "KAlarmApp::processQueue"; mProcessingQueue = true; // Refresh alarms if that's been queued KAlarm::refreshAlarmsIfQueued(); if (!mLoginAlarmsDone) { // Queue all at-login alarms once only, at program start-up. // First, cancel any scheduled reminders or deferrals for them, // since these will be superseded by the new at-login trigger. - KAEvent::List events = AlarmCalendar::resources()->atLoginAlarms(); - for (int i = 0, end = events.count(); i < end; ++i) + const KAEvent::List events = AlarmCalendar::resources()->atLoginAlarms(); + for (KAEvent* const event : events) { - KAEvent event = *events[i]; - if (!cancelReminderAndDeferral(event)) + if (!cancelReminderAndDeferral(*event)) { if (mAlarmsEnabled) - queueAlarmId(event); + queueAlarmId(*event); } } mLoginAlarmsDone = true; } // Process queued events while (!mActionQueue.isEmpty()) { ActionQEntry& entry = mActionQueue.head(); if (entry.eventId.isEmpty()) { // It's a new alarm switch (entry.function) { case EVENT_TRIGGER: execAlarm(entry.event, entry.event.firstAlarm(), false); break; case EVENT_HANDLE: KAlarm::addEvent(entry.event, nullptr, nullptr, KAlarm::ALLOW_KORG_UPDATE | KAlarm::NO_RESOURCE_PROMPT); break; case EVENT_CANCEL: break; } } else handleEvent(entry.eventId, entry.function); mActionQueue.dequeue(); } // Purge the default archived alarms resource if it's time to do so if (mPurgeDaysQueued >= 0) { KAlarm::purgeArchive(mPurgeDaysQueued); mPurgeDaysQueued = -1; } // Now that the queue has been processed, quit if a quit was queued if (mPendingQuit) { if (quitIf(mPendingQuitCode)) return; // quitIf() can sometimes return, despite calling exit() } mProcessingQueue = false; // Schedule the application to be woken when the next alarm is due checkNextDueAlarm(); } } /****************************************************************************** * Called when a repeat-at-login alarm has been added externally. * Queues the alarm for triggering. * First, cancel any scheduled reminder or deferral for it, since these will be * superseded by the new at-login trigger. */ void KAlarmApp::atLoginEventAdded(const KAEvent& event) { KAEvent ev = event; if (!cancelReminderAndDeferral(ev)) { if (mAlarmsEnabled) { mActionQueue.enqueue(ActionQEntry(EVENT_HANDLE, EventId(ev))); if (mInitialised) QTimer::singleShot(0, this, &KAlarmApp::processQueue); } } } /****************************************************************************** * Called when the system tray main window is closed. */ void KAlarmApp::removeWindow(TrayWindow*) { mTrayWindow = nullptr; } /****************************************************************************** * Display or close the system tray icon. */ bool KAlarmApp::displayTrayIcon(bool show, MainWindow* parent) { qCDebug(KALARM_LOG) << "KAlarmApp::displayTrayIcon"; static bool creating = false; if (show) { if (!mTrayWindow && !creating) { if (!QSystemTrayIcon::isSystemTrayAvailable()) return false; if (!MainWindow::count()) { // We have to have at least one main window to act // as parent to the system tray icon (even if the // window is hidden). creating = true; // prevent main window constructor from creating an additional tray icon parent = MainWindow::create(); creating = false; } mTrayWindow = new TrayWindow(parent ? parent : MainWindow::firstWindow()); connect(mTrayWindow, &TrayWindow::deleted, this, &KAlarmApp::trayIconToggled); Q_EMIT trayIconToggled(); if (!checkSystemTray()) quitIf(0); // exit the application if there are no open windows } } else { delete mTrayWindow; mTrayWindow = nullptr; } return true; } /****************************************************************************** * Check whether the system tray icon has been housed in the system tray. */ bool KAlarmApp::checkSystemTray() { if (!mTrayWindow) return true; if (QSystemTrayIcon::isSystemTrayAvailable() == mNoSystemTray) { qCDebug(KALARM_LOG) << "KAlarmApp::checkSystemTray: changed ->" << mNoSystemTray; mNoSystemTray = !mNoSystemTray; // Store the new setting in the config file, so that if KAlarm exits it will // restart with the correct default. KConfigGroup config(KSharedConfig::openConfig(), "General"); config.writeEntry("NoSystemTray", mNoSystemTray); config.sync(); // Update other settings slotShowInSystemTrayChanged(); } return !mNoSystemTray; } /****************************************************************************** * Return the main window associated with the system tray icon. */ MainWindow* KAlarmApp::trayMainWindow() const { return mTrayWindow ? mTrayWindow->assocMainWindow() : nullptr; } /****************************************************************************** * Called when the show-in-system-tray preference setting has changed, to show * or hide the system tray icon. */ void KAlarmApp::slotShowInSystemTrayChanged() { bool newShowInSysTray = wantShowInSystemTray(); if (newShowInSysTray != mOldShowInSystemTray) { // The system tray run mode has changed ++mActiveCount; // prevent the application from quitting MainWindow* win = mTrayWindow ? mTrayWindow->assocMainWindow() : nullptr; delete mTrayWindow; // remove the system tray icon if it is currently shown mTrayWindow = nullptr; mOldShowInSystemTray = newShowInSysTray; if (newShowInSysTray) { // Show the system tray icon displayTrayIcon(true); } else { // Stop showing the system tray icon if (win && win->isHidden()) { if (MainWindow::count() > 1) delete win; else { win->setWindowState(win->windowState() | Qt::WindowMinimized); win->show(); } } } --mActiveCount; } } /****************************************************************************** * Called when the start-of-day time preference setting has changed. * Change alarm times for date-only alarms. */ void KAlarmApp::changeStartOfDay() { DateTime::setStartOfDay(Preferences::startOfDay()); KAEvent::setStartOfDay(Preferences::startOfDay()); AlarmCalendar::resources()->adjustStartOfDay(); } /****************************************************************************** * Called when the default alarm message font preference setting has changed. * Notify KAEvent. */ void KAlarmApp::slotMessageFontChanged(const QFont& font) { KAEvent::setDefaultFont(font); } /****************************************************************************** * Called when the working time preference settings have changed. * Notify KAEvent. */ void KAlarmApp::slotWorkTimeChanged(const QTime& start, const QTime& end, const QBitArray& days) { KAEvent::setWorkTime(days, start, end); } /****************************************************************************** * Called when the holiday region preference setting has changed. * Notify KAEvent. */ void KAlarmApp::slotHolidaysChanged(const KHolidays::HolidayRegion& holidays) { KAEvent::setHolidays(holidays); } /****************************************************************************** * Called when the date for February 29th recurrences has changed in the * preferences settings. */ void KAlarmApp::slotFeb29TypeChanged(Preferences::Feb29Type type) { KARecurrence::Feb29Type rtype; switch (type) { default: case Preferences::Feb29_None: rtype = KARecurrence::Feb29_None; break; case Preferences::Feb29_Feb28: rtype = KARecurrence::Feb29_Feb28; break; case Preferences::Feb29_Mar1: rtype = KARecurrence::Feb29_Mar1; break; } KARecurrence::setDefaultFeb29Type(rtype); } /****************************************************************************** * Return whether the program is configured to be running in the system tray. */ bool KAlarmApp::wantShowInSystemTray() const { return Preferences::showInSystemTray() && QSystemTrayIcon::isSystemTrayAvailable(); } /****************************************************************************** * Called when all calendars have been fetched at startup. * Check whether there are any writable active calendars, and if not, warn the * user. */ void KAlarmApp::checkWritableCalendar() { if (mReadOnly) return; // don't need write access to calendars bool treeFetched = AkonadiModel::instance()->isCollectionTreeFetched(); if (treeFetched && mRedisplayAlarms) { mRedisplayAlarms = false; MessageWin::redisplayAlarms(); } if (!treeFetched || !AkonadiModel::instance()->isMigrationCompleted()) return; static bool done = false; if (done) return; done = true; qCDebug(KALARM_LOG) << "KAlarmApp::checkWritableCalendar"; // Check for, and remove, any duplicate Akonadi resources, i.e. those which // use the same calendar file/directory. CollectionControlModel::removeDuplicateResources(); // Find whether there are any writable active alarm calendars bool active = !CollectionControlModel::enabledCollections(CalEvent::ACTIVE, true).isEmpty(); if (!active) { qCWarning(KALARM_LOG) << "KAlarmApp::checkWritableCalendar: No writable active calendar"; KAMessageBox::information(MainWindow::mainMainWindow(), xi18nc("@info", "Alarms cannot be created or updated, because no writable active alarm calendar is enabled." "To fix this, use View | Show Calendars to check or change calendar statuses."), QString(), QStringLiteral("noWritableCal")); } } /****************************************************************************** * Called when a new collection has been added, or when a collection has been * set as the standard collection for its type. * If it is the default archived calendar, purge its old alarms if necessary. */ void KAlarmApp::purgeNewArchivedDefault(const Akonadi::Collection& collection) { Akonadi::Collection col(collection); if (CollectionControlModel::isStandard(col, CalEvent::ARCHIVED)) { // Allow time (1 minute) for AkonadiModel to be populated with the // collection's events before purging it. qCDebug(KALARM_LOG) << "KAlarmApp::purgeNewArchivedDefault:" << collection.id() << ": standard archived..."; QTimer::singleShot(60000, this, &KAlarmApp::purgeAfterDelay); } } /****************************************************************************** * Called after a delay, after the default archived calendar has been added to * AkonadiModel. * Purge old alarms from it if necessary. */ void KAlarmApp::purgeAfterDelay() { if (mArchivedPurgeDays >= 0) purge(mArchivedPurgeDays); else setArchivePurgeDays(); } /****************************************************************************** * Called when the length of time to keep archived alarms changes in KAlarm's * preferences. * Set the number of days to keep archived alarms. * Alarms which are older are purged immediately, and at the start of each day. */ void KAlarmApp::setArchivePurgeDays() { int newDays = Preferences::archivedKeepDays(); if (newDays != mArchivedPurgeDays) { int oldDays = mArchivedPurgeDays; mArchivedPurgeDays = newDays; if (mArchivedPurgeDays <= 0) StartOfDayTimer::disconnect(this); if (mArchivedPurgeDays < 0) return; // keep indefinitely, so don't purge if (oldDays < 0 || mArchivedPurgeDays < oldDays) { // Alarms are now being kept for less long, so purge them purge(mArchivedPurgeDays); if (!mArchivedPurgeDays) return; // don't archive any alarms } // Start the purge timer to expire at the start of the next day // (using the user-defined start-of-day time). StartOfDayTimer::connect(this, SLOT(slotPurge())); } } /****************************************************************************** * Purge all archived events from the calendar whose end time is longer ago than * 'daysToKeep'. All events are deleted if 'daysToKeep' is zero. */ void KAlarmApp::purge(int daysToKeep) { if (mPurgeDaysQueued < 0 || daysToKeep < mPurgeDaysQueued) mPurgeDaysQueued = daysToKeep; // Do the purge once any other current operations are completed processQueue(); } /****************************************************************************** * Output a list of pending alarms, with their next scheduled occurrence. */ QStringList KAlarmApp::scheduledAlarmList() { - QVector events = KAlarm::getSortedActiveEvents(this); QStringList alarms; - for (int i = 0, count = events.count(); i < count; ++i) + const QVector events = KAlarm::getSortedActiveEvents(this); + for (const KAEvent& event : events) { - const KAEvent* event = &events[i]; - const KADateTime dateTime = event->nextTrigger(KAEvent::DISPLAY_TRIGGER).effectiveKDateTime().toLocalZone(); - Akonadi::Collection c(event->collectionId()); + const KADateTime dateTime = event.nextTrigger(KAEvent::DISPLAY_TRIGGER).effectiveKDateTime().toLocalZone(); + Akonadi::Collection c(event.collectionId()); AkonadiModel::instance()->refresh(c); QString text(c.resource() + QLatin1String(":")); - text += event->id() + QLatin1Char(' ') + text += event.id() + QLatin1Char(' ') + dateTime.toString(QStringLiteral("%Y%m%dT%H%M ")) - + AlarmText::summary(*event, 1); + + AlarmText::summary(event, 1); alarms << text; } return alarms; } /****************************************************************************** * Enable or disable alarm monitoring. */ void KAlarmApp::setAlarmsEnabled(bool enabled) { if (enabled != mAlarmsEnabled) { mAlarmsEnabled = enabled; Q_EMIT alarmEnabledToggled(enabled); if (!enabled) KAlarm::cancelRtcWake(nullptr); else if (!mProcessingQueue) checkNextDueAlarm(); } } /****************************************************************************** * Spread or collect alarm message and error message windows. */ void KAlarmApp::spreadWindows(bool spread) { spread = MessageWin::spread(spread); Q_EMIT spreadWindowsToggled(spread); } /****************************************************************************** * Called when the spread status of message windows changes. * Set the 'spread windows' action state. */ void KAlarmApp::setSpreadWindowsState(bool spread) { Q_EMIT spreadWindowsToggled(spread); } /****************************************************************************** * Check whether the window manager's handling of keyboard focus transfer * between application windows is broken. This is true for Ubuntu's Unity * desktop, where MessageWin windows steal keyboard focus from EditAlarmDlg * windows. */ bool KAlarmApp::windowFocusBroken() const { return mWindowFocusBroken; } /****************************************************************************** * Check whether window/keyboard focus currently needs to be fixed manually due * to the window manager not handling it correctly. This will occur if there are * both EditAlarmDlg and MessageWin windows currently active. */ bool KAlarmApp::needWindowFocusFix() const { return mWindowFocusBroken && MessageWin::instanceCount(true) && EditAlarmDlg::instanceCount(); } /****************************************************************************** * Called to schedule a new alarm, either in response to a DCOP notification or * to command line options. * Reply = true unless there was a parameter error or an error opening calendar file. */ bool KAlarmApp::scheduleEvent(KAEvent::SubAction action, const QString& text, const KADateTime& dateTime, int lateCancel, KAEvent::Flags flags, const QColor& bg, const QColor& fg, const QFont& font, const QString& audioFile, float audioVolume, int reminderMinutes, const KARecurrence& recurrence, const KCalendarCore::Duration& repeatInterval, int repeatCount, uint mailFromID, const KCalendarCore::Person::List& mailAddresses, const QString& mailSubject, const QStringList& mailAttachments) { qCDebug(KALARM_LOG) << "KAlarmApp::scheduleEvent:" << text; if (!dateTime.isValid()) return false; const KADateTime now = KADateTime::currentUtcDateTime(); if (lateCancel && dateTime < now.addSecs(-maxLateness(lateCancel))) return true; // alarm time was already archived too long ago KADateTime alarmTime = dateTime; // Round down to the nearest minute to avoid scheduling being messed up if (!dateTime.isDateOnly()) alarmTime.setTime(QTime(alarmTime.time().hour(), alarmTime.time().minute(), 0)); KAEvent event(alarmTime, text, bg, fg, font, action, lateCancel, flags, true); if (reminderMinutes) { bool onceOnly = flags & KAEvent::REMINDER_ONCE; event.setReminder(reminderMinutes, onceOnly); } if (!audioFile.isEmpty()) event.setAudioFile(audioFile, audioVolume, -1, 0, (flags & KAEvent::REPEAT_SOUND) ? 0 : -1); if (!mailAddresses.isEmpty()) event.setEmail(mailFromID, mailAddresses, mailSubject, mailAttachments); event.setRecurrence(recurrence); event.setFirstRecurrence(); event.setRepetition(Repetition(repeatInterval, repeatCount - 1)); event.endChanges(); if (alarmTime <= now) { // Alarm is due for display already. // First execute it once without adding it to the calendar file. if (!mInitialised) mActionQueue.enqueue(ActionQEntry(event, EVENT_TRIGGER)); else execAlarm(event, event.firstAlarm(), false); // If it's a recurring alarm, reschedule it for its next occurrence if (!event.recurs() || event.setNextOccurrence(now) == KAEvent::NO_OCCURRENCE) return true; // It has recurrences in the future } // Queue the alarm for insertion into the calendar file mActionQueue.enqueue(ActionQEntry(event)); if (mInitialised) QTimer::singleShot(0, this, &KAlarmApp::processQueue); return true; } /****************************************************************************** * Called in response to a D-Bus request to trigger or cancel an event. * Optionally display the event. Delete the event from the calendar file and * from every main window instance. */ bool KAlarmApp::dbusHandleEvent(const EventId& eventID, EventFunc function) { qCDebug(KALARM_LOG) << "KAlarmApp::dbusHandleEvent:" << eventID; mActionQueue.append(ActionQEntry(function, eventID)); if (mInitialised) QTimer::singleShot(0, this, &KAlarmApp::processQueue); return true; } /****************************************************************************** * Called in response to a D-Bus request to list all pending alarms. */ QString KAlarmApp::dbusList() { qCDebug(KALARM_LOG) << "KAlarmApp::dbusList"; return scheduledAlarmList().join(QStringLiteral("\n")) + QLatin1Char('\n'); } /****************************************************************************** * Either: * a) Display the event and then delete it if it has no outstanding repetitions. * b) Delete the event. * c) Reschedule the event for its next repetition. If none remain, delete it. * If the event is deleted, it is removed from the calendar file and from every * main window instance. * Reply = false if event ID not found, or if more than one event with the same * ID is found. */ bool KAlarmApp::handleEvent(const EventId& id, EventFunc function, bool checkDuplicates) { // Delete any expired wake-on-suspend config data KAlarm::checkRtcWakeConfig(); const QString eventID(id.eventId()); KAEvent* event = AlarmCalendar::resources()->event(id, checkDuplicates); if (!event) { if (id.collectionId() != -1) qCWarning(KALARM_LOG) << "KAlarmApp::handleEvent: Event ID not found, or duplicated:" << eventID; else qCWarning(KALARM_LOG) << "KAlarmApp::handleEvent: Event ID not found:" << eventID; return false; } switch (function) { case EVENT_CANCEL: qCDebug(KALARM_LOG) << "KAlarmApp::handleEvent:" << eventID << ", CANCEL"; KAlarm::deleteEvent(*event, true); break; case EVENT_TRIGGER: // handle it if it's due, else execute it regardless case EVENT_HANDLE: // handle it if it's due { const KADateTime now = KADateTime::currentUtcDateTime(); qCDebug(KALARM_LOG) << "KAlarmApp::handleEvent:" << eventID << "," << (function==EVENT_TRIGGER?"TRIGGER:":"HANDLE:") << qPrintable(now.qDateTime().toString(QStringLiteral("yyyy-MM-dd hh:mm"))) << "UTC"; bool updateCalAndDisplay = false; bool alarmToExecuteValid = false; KAAlarm alarmToExecute; bool restart = false; // Check all the alarms in turn. // Note that the main alarm is fetched before any other alarms. for (KAAlarm alarm = event->firstAlarm(); alarm.isValid(); alarm = (restart ? event->firstAlarm() : event->nextAlarm(alarm)), restart = false) { // Check if the alarm is due yet. const KADateTime nextDT = alarm.dateTime(true).effectiveKDateTime(); int secs = nextDT.secsTo(now); if (secs < 0) { // The alarm appears to be in the future. // Check if it's an invalid local time during a daylight // saving time shift, which has actually passed. if (alarm.dateTime().timeSpec() != KADateTime::LocalZone || nextDT > now.toTimeSpec(KADateTime::LocalZone)) { // This alarm is definitely not due yet qCDebug(KALARM_LOG) << "KAlarmApp::handleEvent: Alarm" << alarm.type() << "at" << nextDT.qDateTime() << ": not due"; continue; } } bool reschedule = false; bool rescheduleWork = false; if ((event->workTimeOnly() || event->holidaysExcluded()) && !alarm.deferred()) { // The alarm is restricted to working hours and/or non-holidays // (apart from deferrals). This needs to be re-evaluated every // time it triggers, since working hours could change. if (alarm.dateTime().isDateOnly()) { KADateTime dt(nextDT); dt.setDateOnly(true); reschedule = !event->isWorkingTime(dt); } else reschedule = !event->isWorkingTime(nextDT); rescheduleWork = reschedule; if (reschedule) qCDebug(KALARM_LOG) << "KAlarmApp::handleEvent: Alarm" << alarm.type() << "at" << nextDT.qDateTime() << ": not during working hours"; } if (!reschedule && alarm.repeatAtLogin()) { // Alarm is to be displayed at every login. qCDebug(KALARM_LOG) << "KAlarmApp::handleEvent: REPEAT_AT_LOGIN"; // Check if the main alarm is already being displayed. // (We don't want to display both at the same time.) if (alarmToExecute.isValid()) continue; // Set the time to display if it's a display alarm alarm.setTime(now); } if (!reschedule && event->lateCancel()) { // Alarm is due, and it is to be cancelled if too late. qCDebug(KALARM_LOG) << "KAlarmApp::handleEvent: LATE_CANCEL"; bool cancel = false; if (alarm.dateTime().isDateOnly()) { // The alarm has no time, so cancel it if its date is too far past int maxlate = event->lateCancel() / 1440; // maximum lateness in days KADateTime limit(DateTime(nextDT.addDays(maxlate + 1)).effectiveKDateTime()); if (now >= limit) { // It's too late to display the scheduled occurrence. // Find the last previous occurrence of the alarm. DateTime next; KAEvent::OccurType type = event->previousOccurrence(now, next, true); switch (type & ~KAEvent::OCCURRENCE_REPEAT) { case KAEvent::FIRST_OR_ONLY_OCCURRENCE: case KAEvent::RECURRENCE_DATE: case KAEvent::RECURRENCE_DATE_TIME: case KAEvent::LAST_RECURRENCE: limit.setDate(next.date().addDays(maxlate + 1)); if (now >= limit) { if (type == KAEvent::LAST_RECURRENCE || (type == KAEvent::FIRST_OR_ONLY_OCCURRENCE && !event->recurs())) cancel = true; // last occurrence (and there are no repetitions) else reschedule = true; } break; case KAEvent::NO_OCCURRENCE: default: reschedule = true; break; } } } else { // The alarm is timed. Allow it to be the permitted amount late before cancelling it. int maxlate = maxLateness(event->lateCancel()); if (secs > maxlate) { // It's over the maximum interval late. // Find the most recent occurrence of the alarm. DateTime next; - KAEvent::OccurType type = event->previousOccurrence(now, next, true); + const KAEvent::OccurType type = event->previousOccurrence(now, next, true); switch (type & ~KAEvent::OCCURRENCE_REPEAT) { case KAEvent::FIRST_OR_ONLY_OCCURRENCE: case KAEvent::RECURRENCE_DATE: case KAEvent::RECURRENCE_DATE_TIME: case KAEvent::LAST_RECURRENCE: if (next.effectiveKDateTime().secsTo(now) > maxlate) { if (type == KAEvent::LAST_RECURRENCE || (type == KAEvent::FIRST_OR_ONLY_OCCURRENCE && !event->recurs())) cancel = true; // last occurrence (and there are no repetitions) else reschedule = true; } break; case KAEvent::NO_OCCURRENCE: default: reschedule = true; break; } } } if (cancel) { // All recurrences are finished, so cancel the event event->setArchive(); if (cancelAlarm(*event, alarm.type(), false)) return true; // event has been deleted updateCalAndDisplay = true; continue; } } if (reschedule) { // The latest repetition was too long ago, so schedule the next one switch (rescheduleAlarm(*event, alarm, false, (rescheduleWork ? nextDT : KADateTime()))) { case 1: // A working-time-only alarm has been rescheduled and the // rescheduled time is already due. Start processing the // event again. alarmToExecuteValid = false; restart = true; break; case -1: return true; // event has been deleted default: break; } updateCalAndDisplay = true; continue; } if (!alarmToExecuteValid) { qCDebug(KALARM_LOG) << "KAlarmApp::handleEvent: Alarm" << alarm.type() << ": execute"; alarmToExecute = alarm; // note the alarm to be displayed alarmToExecuteValid = true; // only trigger one alarm for the event } else qCDebug(KALARM_LOG) << "KAlarmApp::handleEvent: Alarm" << alarm.type() << ": skip"; } // If there is an alarm to execute, do this last after rescheduling/cancelling // any others. This ensures that the updated event is only saved once to the calendar. if (alarmToExecute.isValid()) execAlarm(*event, alarmToExecute, true, !alarmToExecute.repeatAtLogin()); else { if (function == EVENT_TRIGGER) { // The alarm is to be executed regardless of whether it's due. // Only trigger one alarm from the event - we don't want multiple // identical messages, for example. - KAAlarm alarm = event->firstAlarm(); + const KAAlarm alarm = event->firstAlarm(); if (alarm.isValid()) execAlarm(*event, alarm, false); } if (updateCalAndDisplay) KAlarm::updateEvent(*event); // update the window lists and calendar file else if (function != EVENT_TRIGGER) { qCDebug(KALARM_LOG) << "KAlarmApp::handleEvent: No action"; } } break; } } return true; } /****************************************************************************** * Called when an alarm action has completed, to perform any post-alarm actions. */ void KAlarmApp::alarmCompleted(const KAEvent& event) { if (!event.postAction().isEmpty()) { // doShellCommand() will error if the user is not authorised to run // shell commands. - QString command = event.postAction(); + const QString command = event.postAction(); qCDebug(KALARM_LOG) << "KAlarmApp::alarmCompleted:" << event.id() << ":" << command; doShellCommand(command, event, nullptr, ProcData::POST_ACTION); } } /****************************************************************************** * Reschedule the alarm for its next recurrence after now. If none remain, * delete it. If the alarm is deleted and it is the last alarm for its event, * the event is removed from the calendar file and from every main window * instance. * If 'nextDt' is valid, the event is rescheduled for the next non-working * time occurrence after that. * Reply = 1 if 'nextDt' is valid and the rescheduled event is already due * = -1 if the event has been deleted * = 0 otherwise. */ int KAlarmApp::rescheduleAlarm(KAEvent& event, const KAAlarm& alarm, bool updateCalAndDisplay, const KADateTime& nextDt) { qCDebug(KALARM_LOG) << "KAlarmApp::rescheduleAlarm: Alarm type:" << alarm.type(); int reply = 0; bool update = false; event.startChanges(); if (alarm.repeatAtLogin()) { // Leave an alarm which repeats at every login until its main alarm triggers if (!event.reminderActive() && event.reminderMinutes() < 0) { // Executing an at-login alarm: first schedule the reminder // which occurs AFTER the main alarm. event.activateReminderAfter(KADateTime::currentUtcDateTime()); update = true; } } else if (alarm.isReminder() || alarm.deferred()) { // It's a reminder alarm or an extra deferred alarm, so delete it event.removeExpiredAlarm(alarm.type()); update = true; } else { // Reschedule the alarm for its next occurrence. bool cancelled = false; DateTime last = event.mainDateTime(false); // note this trigger time if (last != event.mainDateTime(true)) last = DateTime(); // but ignore sub-repetition triggers bool next = nextDt.isValid(); KADateTime next_dt = nextDt; const KADateTime now = KADateTime::currentUtcDateTime(); do { - KAEvent::OccurType type = event.setNextOccurrence(next ? next_dt : now); + const KAEvent::OccurType type = event.setNextOccurrence(next ? next_dt : now); switch (type) { case KAEvent::NO_OCCURRENCE: // All repetitions are finished, so cancel the event qCDebug(KALARM_LOG) << "KAlarmApp::rescheduleAlarm: No occurrence"; if (event.reminderMinutes() < 0 && last.isValid() && alarm.type() != KAAlarm::AT_LOGIN_ALARM && !event.mainExpired()) { // Set the reminder which is now due after the last main alarm trigger. // Note that at-login reminders are scheduled in execAlarm(). event.activateReminderAfter(last); updateCalAndDisplay = true; } if (cancelAlarm(event, alarm.type(), updateCalAndDisplay)) return -1; break; default: if (!(type & KAEvent::OCCURRENCE_REPEAT)) break; // Next occurrence is a repeat, so fall through to recurrence handling Q_FALLTHROUGH(); case KAEvent::RECURRENCE_DATE: case KAEvent::RECURRENCE_DATE_TIME: case KAEvent::LAST_RECURRENCE: // The event is due by now and repetitions still remain, so rewrite the event if (updateCalAndDisplay) update = true; break; case KAEvent::FIRST_OR_ONLY_OCCURRENCE: // The first occurrence is still due?!?, so don't do anything break; } if (cancelled) break; if (event.deferred()) { // Just in case there's also a deferred alarm, ensure it's removed event.removeExpiredAlarm(KAAlarm::DEFERRED_ALARM); update = true; } if (next) { // The alarm is restricted to working hours and/or non-holidays. // Check if the calculated next time is valid. next_dt = event.mainDateTime(true).effectiveKDateTime(); if (event.mainDateTime(false).isDateOnly()) { KADateTime dt(next_dt); dt.setDateOnly(true); next = !event.isWorkingTime(dt); } else next = !event.isWorkingTime(next_dt); } } while (next && next_dt <= now); reply = (!cancelled && next_dt.isValid() && (next_dt <= now)) ? 1 : 0; if (event.reminderMinutes() < 0 && last.isValid() && alarm.type() != KAAlarm::AT_LOGIN_ALARM) { // Set the reminder which is now due after the last main alarm trigger. // Note that at-login reminders are scheduled in execAlarm(). event.activateReminderAfter(last); } } event.endChanges(); if (update) KAlarm::updateEvent(event); // update the window lists and calendar file return reply; } /****************************************************************************** * Delete the alarm. If it is the last alarm for its event, the event is removed * from the calendar file and from every main window instance. * Reply = true if event has been deleted. */ bool KAlarmApp::cancelAlarm(KAEvent& event, KAAlarm::Type alarmType, bool updateCalAndDisplay) { qCDebug(KALARM_LOG) << "KAlarmApp::cancelAlarm"; if (alarmType == KAAlarm::MAIN_ALARM && !event.displaying() && event.toBeArchived()) { // The event is being deleted. Save it in the archived resources first. KAEvent ev(event); KAlarm::addArchivedEvent(ev); } event.removeExpiredAlarm(alarmType); if (!event.alarmCount()) { // If it's a command alarm being executed, mark it as deleted ProcData* pd = findCommandProcess(event.id()); if (pd) pd->eventDeleted = true; // Delete it KAlarm::deleteEvent(event, false); return true; } if (updateCalAndDisplay) KAlarm::updateEvent(event); // update the window lists and calendar file return false; } /****************************************************************************** * Cancel any reminder or deferred alarms in an repeat-at-login event. * This should be called when the event is first loaded. * If there are no more alarms left in the event, the event is removed from the * calendar file and from every main window instance. * Reply = true if event has been deleted. */ bool KAlarmApp::cancelReminderAndDeferral(KAEvent& event) { return cancelAlarm(event, KAAlarm::REMINDER_ALARM, false) || cancelAlarm(event, KAAlarm::DEFERRED_REMINDER_ALARM, false) || cancelAlarm(event, KAAlarm::DEFERRED_ALARM, true); } /****************************************************************************** * Execute an alarm by displaying its message or file, or executing its command. * Reply = ShellProcess instance if a command alarm * = MessageWin if an audio alarm * != 0 if successful * = -1 if execution has not completed * = 0 if the alarm is disabled, or if an error message was output. */ void* KAlarmApp::execAlarm(KAEvent& event, const KAAlarm& alarm, bool reschedule, bool allowDefer, bool noPreAction) { if (!mAlarmsEnabled || !event.enabled()) { // The event (or all events) is disabled qCDebug(KALARM_LOG) << "KAlarmApp::execAlarm:" << event.id() << ": disabled"; if (reschedule) rescheduleAlarm(event, alarm, true); return nullptr; } void* result = (void*)1; event.setArchive(); switch (alarm.action()) { case KAAlarm::COMMAND: if (!event.commandDisplay()) { // execCommandAlarm() will error if the user is not authorised // to run shell commands. result = execCommandAlarm(event, alarm); if (reschedule) rescheduleAlarm(event, alarm, true); break; } // fall through to MESSAGE Q_FALLTHROUGH(); case KAAlarm::MESSAGE: case KAAlarm::FILE: { // Display a message, file or command output, provided that the same event // isn't already being displayed MessageWin* win = MessageWin::findEvent(EventId(event)); // Find if we're changing a reminder message to the real message bool reminder = (alarm.type() & KAAlarm::REMINDER_ALARM); bool replaceReminder = !reminder && win && (win->alarmType() & KAAlarm::REMINDER_ALARM); if (!reminder && (!event.deferred() || (event.extraActionOptions() & KAEvent::ExecPreActOnDeferral)) && (replaceReminder || !win) && !noPreAction && !event.preAction().isEmpty()) { // It's not a reminder alarm, and it's not a deferred alarm unless the // pre-alarm action applies to deferred alarms, and there is no message // window (other than a reminder window) currently displayed for this // alarm, and we need to execute a command before displaying the new window. // // NOTE: The pre-action is not executed for a recurring alarm if an // alarm message window for a previous occurrence is still visible. // Check whether the command is already being executed for this alarm. - for (int i = 0, end = mCommandProcesses.count(); i < end; ++i) + for (const ProcData* pd : qAsConst(mCommandProcesses)) { - ProcData* pd = mCommandProcesses[i]; if (pd->event->id() == event.id() && (pd->flags & ProcData::PRE_ACTION)) { qCDebug(KALARM_LOG) << "KAlarmApp::execAlarm: Already executing pre-DISPLAY command"; return pd->process; // already executing - don't duplicate the action } } // doShellCommand() will error if the user is not authorised to run // shell commands. - QString command = event.preAction(); + const QString command = event.preAction(); qCDebug(KALARM_LOG) << "KAlarmApp::execAlarm: Pre-DISPLAY command:" << command; - int flags = (reschedule ? ProcData::RESCHEDULE : 0) | (allowDefer ? ProcData::ALLOW_DEFER : 0); + const int flags = (reschedule ? ProcData::RESCHEDULE : 0) | (allowDefer ? ProcData::ALLOW_DEFER : 0); if (doShellCommand(command, event, &alarm, (flags | ProcData::PRE_ACTION))) { AlarmCalendar::resources()->setAlarmPending(&event); return result; // display the message after the command completes } // Error executing command if (event.extraActionOptions() & KAEvent::CancelOnPreActError) { // Cancel the rest of the alarm execution qCDebug(KALARM_LOG) << "KAlarmApp::execAlarm:" << event.id() << ": pre-action failed: cancelled"; if (reschedule) rescheduleAlarm(event, alarm, true); return nullptr; } // Display the message even though it failed } if (!win) { // There isn't already a message for this event - int flags = (reschedule ? 0 : MessageWin::NO_RESCHEDULE) | (allowDefer ? 0 : MessageWin::NO_DEFER); + const int flags = (reschedule ? 0 : MessageWin::NO_RESCHEDULE) | (allowDefer ? 0 : MessageWin::NO_DEFER); (new MessageWin(&event, alarm, flags))->show(); } else if (replaceReminder) { // The caption needs to be changed from "Reminder" to "Message" win->cancelReminder(event, alarm); } else if (!win->hasDefer() && !alarm.repeatAtLogin()) { // It's a repeat-at-login message with no Defer button, // which has now reached its final trigger time and needs // to be replaced with a new message. win->showDefer(); win->showDateTime(event, alarm); } else { // Use the existing message window } if (win) { // Raise the existing message window and replay any sound win->repeat(alarm); // N.B. this reschedules the alarm } break; } case KAAlarm::EMAIL: { qCDebug(KALARM_LOG) << "KAlarmApp::execAlarm: EMAIL to:" << event.emailAddresses(QStringLiteral(",")); QStringList errmsgs; KAMail::JobData data(event, alarm, reschedule, (reschedule || allowDefer)); data.queued = true; int ans = KAMail::send(data, errmsgs); if (ans) { // The email has either been sent or failed - not queued if (ans < 0) result = nullptr; // failure data.queued = false; emailSent(data, errmsgs, (ans > 0)); } else { result = (void*)-1; // email has been queued } if (reschedule) rescheduleAlarm(event, alarm, true); break; } case KAAlarm::AUDIO: { // Play the sound, provided that the same event // isn't already playing MessageWin* win = MessageWin::findEvent(EventId(event)); if (!win) { // There isn't already a message for this event. - int flags = (reschedule ? 0 : MessageWin::NO_RESCHEDULE) | MessageWin::ALWAYS_HIDE; + const int flags = (reschedule ? 0 : MessageWin::NO_RESCHEDULE) | MessageWin::ALWAYS_HIDE; win = new MessageWin(&event, alarm, flags); } else { // There's an existing message window: replay the sound win->repeat(alarm); // N.B. this reschedules the alarm } return win; } default: return nullptr; } return result; } /****************************************************************************** * Called when sending an email has completed. */ void KAlarmApp::emailSent(KAMail::JobData& data, const QStringList& errmsgs, bool copyerr) { if (!errmsgs.isEmpty()) { // Some error occurred, although the email may have been sent successfully if (errmsgs.count() > 1) qCDebug(KALARM_LOG) << "KAlarmApp::emailSent:" << (copyerr ? "Copy error:" : "Failed:") << errmsgs[1]; MessageWin::showError(data.event, data.alarm.dateTime(), errmsgs); } else if (data.queued) Q_EMIT execAlarmSuccess(); } /****************************************************************************** * Execute the command specified in a command alarm. * To connect to the output ready signals of the process, specify a slot to be * called by supplying 'receiver' and 'slot' parameters. */ ShellProcess* KAlarmApp::execCommandAlarm(const KAEvent& event, const KAAlarm& alarm, const QObject* receiver, const char* slot) { // doShellCommand() will error if the user is not authorised to run // shell commands. - int flags = (event.commandXterm() ? ProcData::EXEC_IN_XTERM : 0) - | (event.commandDisplay() ? ProcData::DISP_OUTPUT : 0); - QString command = event.cleanText(); + const int flags = (event.commandXterm() ? ProcData::EXEC_IN_XTERM : 0) + | (event.commandDisplay() ? ProcData::DISP_OUTPUT : 0); + const QString command = event.cleanText(); if (event.commandScript()) { // Store the command script in a temporary file for execution qCDebug(KALARM_LOG) << "KAlarmApp::execCommandAlarm: Script"; - QString tmpfile = createTempScriptFile(command, false, event, alarm); + const QString tmpfile = createTempScriptFile(command, false, event, alarm); if (tmpfile.isEmpty()) { setEventCommandError(event, KAEvent::CMD_ERROR); return nullptr; } return doShellCommand(tmpfile, event, &alarm, (flags | ProcData::TEMP_FILE), receiver, slot); } else { qCDebug(KALARM_LOG) << "KAlarmApp::execCommandAlarm:" << command; return doShellCommand(command, event, &alarm, flags, receiver, slot); } } /****************************************************************************** * Execute a shell command line specified by an alarm. * If the PRE_ACTION bit of 'flags' is set, the alarm will be executed via * execAlarm() once the command completes, the execAlarm() parameters being * derived from the remaining bits in 'flags'. * 'flags' must contain the bit PRE_ACTION or POST_ACTION if and only if it is * a pre- or post-alarm action respectively. * To connect to the output ready signals of the process, specify a slot to be * called by supplying 'receiver' and 'slot' parameters. * * Note that if shell access is not authorised, the attempt to run the command * will be errored. */ ShellProcess* KAlarmApp::doShellCommand(const QString& command, const KAEvent& event, const KAAlarm* alarm, int flags, const QObject* receiver, const char* slot) { qCDebug(KALARM_LOG) << "KAlarmApp::doShellCommand:" << command << "," << event.id(); QIODevice::OpenMode mode = QIODevice::WriteOnly; QString cmd; QString tmpXtermFile; if (flags & ProcData::EXEC_IN_XTERM) { // Execute the command in a terminal window. cmd = composeXTermCommand(command, event, alarm, flags, tmpXtermFile); if (cmd.isEmpty()) { qCWarning(KALARM_LOG) << "KAlarmApp::doShellCommand: Command failed (no terminal selected)"; - QStringList errors; - errors << i18nc("@info", "Failed to execute command\n(no terminal selected for command alarms)"); + const QStringList errors{i18nc("@info", "Failed to execute command\n(no terminal selected for command alarms)")}; commandErrorMsg(nullptr, event, alarm, flags, errors); return nullptr; } } else { cmd = command; mode = QIODevice::ReadWrite; } ProcData* pd = nullptr; ShellProcess* proc = nullptr; if (!cmd.isEmpty()) { // Use ShellProcess, which automatically checks whether the user is // authorised to run shell commands. proc = new ShellProcess(cmd); proc->setEnv(QStringLiteral("KALARM_UID"), event.id(), true); proc->setOutputChannelMode(KProcess::MergedChannels); // combine stdout & stderr connect(proc, &ShellProcess::shellExited, this, &KAlarmApp::slotCommandExited); if ((flags & ProcData::DISP_OUTPUT) && receiver && slot) { connect(proc, SIGNAL(receivedStdout(ShellProcess*)), receiver, slot); connect(proc, SIGNAL(receivedStderr(ShellProcess*)), receiver, slot); } if (mode == QIODevice::ReadWrite && !event.logFile().isEmpty()) { // Output is to be appended to a log file. // Set up a logging process to write the command's output to. QString heading; if (alarm && alarm->dateTime().isValid()) { - QString dateTime = alarm->dateTime().formatLocale(); + const QString dateTime = alarm->dateTime().formatLocale(); heading.sprintf("\n******* KAlarm %s *******\n", dateTime.toLatin1().data()); } else heading = QStringLiteral("\n******* KAlarm *******\n"); QFile logfile(event.logFile()); if (logfile.open(QIODevice::Append | QIODevice::Text)) { QTextStream out(&logfile); out << heading; logfile.close(); } proc->setStandardOutputFile(event.logFile(), QIODevice::Append); } pd = new ProcData(proc, new KAEvent(event), (alarm ? new KAAlarm(*alarm) : nullptr), flags); if (flags & ProcData::TEMP_FILE) pd->tempFiles += command; if (!tmpXtermFile.isEmpty()) pd->tempFiles += tmpXtermFile; mCommandProcesses.append(pd); if (proc->start(mode)) return proc; } // Error executing command - report it qCWarning(KALARM_LOG) << "KAlarmApp::doShellCommand: Command failed to start"; commandErrorMsg(proc, event, alarm, flags); if (pd) { mCommandProcesses.removeAt(mCommandProcesses.indexOf(pd)); delete pd; } return nullptr; } /****************************************************************************** * Compose a command line to execute the given command in a terminal window. * 'tempScriptFile' receives the name of a temporary script file which is * invoked by the command line, if applicable. * Reply = command line, or empty string if error. */ QString KAlarmApp::composeXTermCommand(const QString& command, const KAEvent& event, const KAAlarm* alarm, int flags, QString& tempScriptFile) const { qCDebug(KALARM_LOG) << "KAlarmApp::composeXTermCommand:" << command << "," << event.id(); tempScriptFile.clear(); QString cmd = Preferences::cmdXTermCommand(); if (cmd.isEmpty()) return QString(); // no terminal application is configured cmd.replace(QLatin1String("%t"), KAboutData::applicationData().displayName()); // set the terminal window title if (cmd.indexOf(QLatin1String("%C")) >= 0) { // Execute the command from a temporary script file if (flags & ProcData::TEMP_FILE) cmd.replace(QLatin1String("%C"), command); // the command is already calling a temporary file else { tempScriptFile = createTempScriptFile(command, true, event, *alarm); if (tempScriptFile.isEmpty()) return QString(); cmd.replace(QLatin1String("%C"), tempScriptFile); // %C indicates where to insert the command } } else if (cmd.indexOf(QLatin1String("%W")) >= 0) { // Execute the command from a temporary script file, // with a sleep after the command is executed tempScriptFile = createTempScriptFile(command + QLatin1String("\nsleep 86400\n"), true, event, *alarm); if (tempScriptFile.isEmpty()) return QString(); cmd.replace(QLatin1String("%W"), tempScriptFile); // %w indicates where to insert the command } else if (cmd.indexOf(QLatin1String("%w")) >= 0) { // Append a sleep to the command. // Quote the command in case it contains characters such as [>|;]. - QString exec = KShell::quoteArg(command + QLatin1String("; sleep 86400")); + const QString exec = KShell::quoteArg(command + QLatin1String("; sleep 86400")); cmd.replace(QLatin1String("%w"), exec); // %w indicates where to insert the command string } else { // Set the command to execute. // Put it in quotes in case it contains characters such as [>|;]. - QString exec = KShell::quoteArg(command); + const QString exec = KShell::quoteArg(command); if (cmd.indexOf(QLatin1String("%c")) >= 0) cmd.replace(QLatin1String("%c"), exec); // %c indicates where to insert the command string else cmd.append(exec); // otherwise, simply append the command string } return cmd; } /****************************************************************************** * Create a temporary script file containing the specified command string. * Reply = path of temporary file, or null string if error. */ QString KAlarmApp::createTempScriptFile(const QString& command, bool insertShell, const KAEvent& event, const KAAlarm& alarm) const { QTemporaryFile tmpFile; tmpFile.setAutoRemove(false); // don't delete file when it is destructed if (!tmpFile.open()) qCCritical(KALARM_LOG) << "Unable to create a temporary script file"; else { tmpFile.setPermissions(QFile::ReadUser | QFile::WriteUser | QFile::ExeUser); QTextStream stream(&tmpFile); if (insertShell) stream << "#!" << ShellProcess::shellPath() << "\n"; stream << command; stream.flush(); if (tmpFile.error() != QFile::NoError) qCCritical(KALARM_LOG) << "Error" << tmpFile.errorString() << " writing to temporary script file"; else return tmpFile.fileName(); } - QStringList errmsgs(i18nc("@info", "Error creating temporary script file")); + const QStringList errmsgs(i18nc("@info", "Error creating temporary script file")); MessageWin::showError(event, alarm.dateTime(), errmsgs, QStringLiteral("Script")); return QString(); } /****************************************************************************** * Called when a command alarm's execution completes. */ void KAlarmApp::slotCommandExited(ShellProcess* proc) { qCDebug(KALARM_LOG) << "KAlarmApp::slotCommandExited"; // Find this command in the command list for (int i = 0, end = mCommandProcesses.count(); i < end; ++i) { - ProcData* pd = mCommandProcesses[i]; + ProcData* pd = mCommandProcesses.at(i); if (pd->process == proc) { // Found the command. Check its exit status. bool executeAlarm = pd->preAction(); - ShellProcess::Status status = proc->status(); + const ShellProcess::Status status = proc->status(); if (status == ShellProcess::SUCCESS && !proc->exitCode()) { qCDebug(KALARM_LOG) << "KAlarmApp::slotCommandExited:" << pd->event->id() << ": SUCCESS"; clearEventCommandError(*pd->event, pd->preAction() ? KAEvent::CMD_ERROR_PRE : pd->postAction() ? KAEvent::CMD_ERROR_POST : KAEvent::CMD_ERROR); } else { QString errmsg = proc->errorMessage(); if (status == ShellProcess::SUCCESS || status == ShellProcess::NOT_FOUND) qCWarning(KALARM_LOG) << "KAlarmApp::slotCommandExited:" << pd->event->id() << ":" << errmsg << "exit status =" << status << ", code =" << proc->exitCode(); else qCWarning(KALARM_LOG) << "KAlarmApp::slotCommandExited:" << pd->event->id() << ":" << errmsg << "exit status =" << status; if (pd->messageBoxParent) { // Close the existing informational KMessageBox for this process - QList dialogs = pd->messageBoxParent->findChildren(); + const QList dialogs = pd->messageBoxParent->findChildren(); if (!dialogs.isEmpty()) delete dialogs[0]; setEventCommandError(*pd->event, pd->preAction() ? KAEvent::CMD_ERROR_PRE : pd->postAction() ? KAEvent::CMD_ERROR_POST : KAEvent::CMD_ERROR); if (!pd->tempFile()) { errmsg += QLatin1Char('\n'); errmsg += proc->command(); } KAMessageBox::error(pd->messageBoxParent, errmsg); } else commandErrorMsg(proc, *pd->event, pd->alarm, pd->flags); if (executeAlarm && (pd->event->extraActionOptions() & KAEvent::CancelOnPreActError)) { qCDebug(KALARM_LOG) << "KAlarmApp::slotCommandExited:" << pd->event->id() << ": pre-action failed: cancelled"; if (pd->reschedule()) rescheduleAlarm(*pd->event, *pd->alarm, true); executeAlarm = false; } } if (pd->preAction()) AlarmCalendar::resources()->setAlarmPending(pd->event, false); if (executeAlarm) execAlarm(*pd->event, *pd->alarm, pd->reschedule(), pd->allowDefer(), true); mCommandProcesses.removeAt(i); delete pd; break; } } // If there are now no executing shell commands, quit if a quit was queued if (mPendingQuit && mCommandProcesses.isEmpty()) quitIf(mPendingQuitCode); } /****************************************************************************** * Output an error message for a shell command, and record the alarm's error status. */ void KAlarmApp::commandErrorMsg(const ShellProcess* proc, const KAEvent& event, const KAAlarm* alarm, int flags, const QStringList& errors) { KAEvent::CmdErrType cmderr; QString dontShowAgain; QStringList errmsgs = errors; if (flags & ProcData::PRE_ACTION) { if (event.extraActionOptions() & KAEvent::DontShowPreActError) return; // don't notify user of any errors for the alarm errmsgs += i18nc("@info", "Pre-alarm action:"); dontShowAgain = QStringLiteral("Pre"); cmderr = KAEvent::CMD_ERROR_PRE; } else if (flags & ProcData::POST_ACTION) { errmsgs += i18nc("@info", "Post-alarm action:"); dontShowAgain = QStringLiteral("Post"); cmderr = (event.commandError() == KAEvent::CMD_ERROR_PRE) ? KAEvent::CMD_ERROR_PRE_POST : KAEvent::CMD_ERROR_POST; } else { dontShowAgain = QStringLiteral("Exec"); cmderr = KAEvent::CMD_ERROR; } // Record the alarm's error status setEventCommandError(event, cmderr); // Display an error message if (proc) { errmsgs += proc->errorMessage(); if (!(flags & ProcData::TEMP_FILE)) errmsgs += proc->command(); dontShowAgain += QString::number(proc->status()); } MessageWin::showError(event, (alarm ? alarm->dateTime() : DateTime()), errmsgs, dontShowAgain); } /****************************************************************************** * Notes that an informational KMessageBox is displayed for this process. */ void KAlarmApp::commandMessage(ShellProcess* proc, QWidget* parent) { // Find this command in the command list - for (int i = 0, end = mCommandProcesses.count(); i < end; ++i) + for (ProcData* pd : qAsConst(mCommandProcesses)) { - ProcData* pd = mCommandProcesses[i]; if (pd->process == proc) { pd->messageBoxParent = parent; break; } } } /****************************************************************************** * If this is the first time through, open the calendar file, and start * processing the execution queue. */ bool KAlarmApp::initCheck(bool calendarOnly, bool waitForCollection, Akonadi::Collection::Id collectionId) { static bool firstTime = true; if (firstTime) qCDebug(KALARM_LOG) << "KAlarmApp::initCheck: first time"; if (initialise() || firstTime) { /* Need to open the display calendar now, since otherwise if display * alarms are immediately due, they will often be processed while * MessageWin::redisplayAlarms() is executing open() (but before open() * completes), which causes problems!! */ AlarmCalendar::displayCalendar()->open(); if (!AlarmCalendar::resources()->open()) return false; } if (firstTime) { setArchivePurgeDays(); // Warn the user if there are no writable active alarm calendars checkWritableCalendar(); firstTime = false; } if (!calendarOnly) startProcessQueue(); // start processing the execution queue if (waitForCollection) { // Wait for one or all Akonadi collections to be populated if (!CollectionControlModel::instance()->waitUntilPopulated(collectionId, AKONADI_TIMEOUT)) return false; } return true; } /****************************************************************************** * Called when an audio thread starts or stops. */ void KAlarmApp::notifyAudioPlaying(bool playing) { Q_EMIT audioPlaying(playing); } /****************************************************************************** * Stop audio play. */ void KAlarmApp::stopAudio() { MessageWin::stopAudio(); } /****************************************************************************** * Set the command error for the specified alarm. */ void KAlarmApp::setEventCommandError(const KAEvent& event, KAEvent::CmdErrType err) const { ProcData* pd = findCommandProcess(event.id()); if (pd && pd->eventDeleted) return; // the alarm has been deleted, so can't set error status if (err == KAEvent::CMD_ERROR_POST && event.commandError() == KAEvent::CMD_ERROR_PRE) err = KAEvent::CMD_ERROR_PRE_POST; event.setCommandError(err); KAEvent* ev = AlarmCalendar::resources()->event(EventId(event)); if (ev && ev->commandError() != err) ev->setCommandError(err); AkonadiModel::instance()->updateCommandError(event); } /****************************************************************************** * Clear the command error for the specified alarm. */ void KAlarmApp::clearEventCommandError(const KAEvent& event, KAEvent::CmdErrType err) const { ProcData* pd = findCommandProcess(event.id()); if (pd && pd->eventDeleted) return; // the alarm has been deleted, so can't set error status KAEvent::CmdErrType newerr = static_cast(event.commandError() & ~err); event.setCommandError(newerr); KAEvent* ev = AlarmCalendar::resources()->event(EventId(event)); if (ev) { newerr = static_cast(ev->commandError() & ~err); ev->setCommandError(newerr); } AkonadiModel::instance()->updateCommandError(event); } /****************************************************************************** * Find the currently executing command process for an event ID, if any. */ KAlarmApp::ProcData* KAlarmApp::findCommandProcess(const QString& eventId) const { - for (int i = 0, end = mCommandProcesses.count(); i < end; ++i) + for (ProcData* pd : qAsConst(mCommandProcesses)) { - ProcData* pd = mCommandProcesses[i]; if (pd->event->id() == eventId) return pd; } return nullptr; } KAlarmApp::ProcData::ProcData(ShellProcess* p, KAEvent* e, KAAlarm* a, int f) : process(p), event(e), alarm(a), messageBoxParent(nullptr), flags(f), eventDeleted(false) { } KAlarmApp::ProcData::~ProcData() { while (!tempFiles.isEmpty()) { // Delete the temporary file called by the XTerm command QFile f(tempFiles.first()); f.remove(); tempFiles.removeFirst(); } delete process; delete event; delete alarm; } // vim: et sw=4: diff --git a/src/kamail.cpp b/src/kamail.cpp index 20f91560..3349915e 100644 --- a/src/kamail.cpp +++ b/src/kamail.cpp @@ -1,971 +1,967 @@ /* * kamail.cpp - email functions * Program: kalarm * Copyright © 2002-2019 David Jarvie * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kalarm.h" //krazy:exclude=includes (kalarm.h must be first) #include "kamail.h" #include "functions.h" #include "kalarmapp.h" #include "mainwindow.h" #include "messagebox.h" #include "preferences.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "kalarm_debug.h" #include #include "kmailinterface.h" static const QLatin1String KMAIL_DBUS_SERVICE("org.kde.kmail"); //static const QLatin1String KMAIL_DBUS_PATH("/KMail"); namespace HeaderParsing { bool parseAddress( const char* & scursor, const char * const send, KMime::Types::Address & result, bool isCRLF=false ); } static void initHeaders(KMime::Message&, KAMail::JobData&); static KMime::Types::Mailbox::List parseAddresses(const QString& text, QString& invalidItem); static QString extractEmailAndNormalize(const QString& emailAddress); static QStringList extractEmailsAndNormalize(const QString& emailAddresses); static QByteArray autoDetectCharset(const QString& text); static const QTextCodec* codecForName(const QByteArray& str); QString KAMail::i18n_NeedFromEmailAddress() { return i18nc("@info", "A 'From' email address must be configured in order to execute email alarms."); } QString KAMail::i18n_sent_mail() { return i18nc("@info KMail folder name: this should be translated the same as in kmail", "sent-mail"); } KAMail* KAMail::mInstance = nullptr; // used only to enable signals/slots to work QQueue KAMail::mJobs; QQueue KAMail::mJobData; KAMail* KAMail::instance() { if (!mInstance) mInstance = new KAMail(); return mInstance; } /****************************************************************************** * Send the email message specified in an event. * Reply = 1 if the message was sent - 'errmsgs' may contain copy error messages. * = 0 if the message is queued for sending. * = -1 if the message was not sent - 'errmsgs' contains the error messages. */ int KAMail::send(JobData& jobdata, QStringList& errmsgs) { QString err; KIdentityManagement::Identity identity; jobdata.from = Preferences::emailAddress(); if (jobdata.event.emailFromId() && Preferences::emailFrom() == Preferences::MAIL_FROM_KMAIL) { identity = Identities::identityManager()->identityForUoid(jobdata.event.emailFromId()); if (identity.isNull()) { qCCritical(KALARM_LOG) << "KAMail::send: Identity" << jobdata.event.emailFromId() << "not found"; errmsgs = errors(xi18nc("@info", "Invalid 'From' email address.Email identity %1 not found", jobdata.event.emailFromId())); return -1; } if (identity.primaryEmailAddress().isEmpty()) { qCCritical(KALARM_LOG) << "KAMail::send: Identity" << identity.identityName() << "uoid" << identity.uoid() << ": no email address"; errmsgs = errors(xi18nc("@info", "Invalid 'From' email address.Email identity %1 has no email address", identity.identityName())); return -1; } jobdata.from = identity.fullEmailAddr(); } if (jobdata.from.isEmpty()) { switch (Preferences::emailFrom()) { case Preferences::MAIL_FROM_KMAIL: errmsgs = errors(xi18nc("@info", "No 'From' email address is configured (no default email identity found)" "Please set it in KMail or in the KAlarm Configuration dialog.")); break; case Preferences::MAIL_FROM_SYS_SETTINGS: errmsgs = errors(xi18nc("@info", "No 'From' email address is configured." "Please set a default address in KMail or KDE System Settings or in the KAlarm Configuration dialog.")); break; case Preferences::MAIL_FROM_ADDR: default: errmsgs = errors(xi18nc("@info", "No 'From' email address is configured." "Please set it in the KAlarm Configuration dialog.")); break; } return -1; } jobdata.bcc = (jobdata.event.emailBcc() ? Preferences::emailBccAddress() : QString()); qCDebug(KALARM_LOG) << "KAMail::send: To:" << jobdata.event.emailAddresses(QStringLiteral(",")) - << endl << "Subject:" << jobdata.event.emailSubject(); + << endl << "Subject:" << jobdata.event.emailSubject(); KMime::Message::Ptr message = KMime::Message::Ptr(new KMime::Message); MailTransport::TransportManager* manager = MailTransport::TransportManager::self(); MailTransport::Transport* transport = nullptr; if (Preferences::emailClient() == Preferences::sendmail) { qCDebug(KALARM_LOG) << "KAMail::send: Sending via sendmail"; - QStringList paths; - paths << QStringLiteral("/sbin") << QStringLiteral("/usr/sbin") << QStringLiteral("/usr/lib"); + const QStringList paths{QStringLiteral("/sbin"), QStringLiteral("/usr/sbin"), QStringLiteral("/usr/lib")}; QString command = QStandardPaths::findExecutable(QStringLiteral("sendmail"), paths); if (!command.isNull()) { command += QStringLiteral(" -f "); command += extractEmailAndNormalize(jobdata.from); command += QStringLiteral(" -oi -t "); initHeaders(*message, jobdata); } else { command = QStandardPaths::findExecutable(QStringLiteral("mail"), paths); if (command.isNull()) { qCCritical(KALARM_LOG) << "KAMail::send: sendmail not found"; errmsgs = errors(xi18nc("@info", "%1 not found", QStringLiteral("sendmail"))); // give up return -1; } command += QStringLiteral(" -s "); command += KShell::quoteArg(jobdata.event.emailSubject()); if (!jobdata.bcc.isEmpty()) { command += QStringLiteral(" -b "); command += extractEmailAndNormalize(jobdata.bcc); } command += QLatin1Char(' '); command += jobdata.event.emailPureAddresses(QStringLiteral(" ")); // locally provided, okay } // Add the body and attachments to the message. // (Sendmail requires attachments to have already been included in the message.) err = appendBodyAttachments(*message, jobdata); if (!err.isNull()) { qCCritical(KALARM_LOG) << "KAMail::send: Error compiling message:" << err; errmsgs = errors(err); return -1; } // Execute the send command FILE* fd = ::popen(command.toLocal8Bit().constData(), "w"); if (!fd) { qCCritical(KALARM_LOG) << "KAMail::send: Unable to open a pipe to " << command; errmsgs = errors(); return -1; } message->assemble(); QByteArray encoded = message->encodedContent(); fwrite(encoded.constData(), encoded.length(), 1, fd); pclose(fd); #ifdef KMAIL_SUPPORTED if (Preferences::emailCopyToKMail()) { // Create a copy of the sent email in KMail's 'sent-mail' folder, // or if there was a send error, in KMail's 'outbox' folder. err = addToKMailFolder(jobdata, "sent-mail", true); if (!err.isNull()) errmsgs += errors(err, COPY_ERROR); // not a fatal error - continue } #endif if (jobdata.allowNotify) notifyQueued(jobdata.event); return 1; } else { qCDebug(KALARM_LOG) << "KAMail::send: Sending via KDE"; const int transportId = identity.transport().isEmpty() ? -1 : identity.transport().toInt(); transport = manager->transportById(transportId, true); if (!transport) { qCCritical(KALARM_LOG) << "KAMail::send: No mail transport found for identity" << identity.identityName() << "uoid" << identity.uoid(); errmsgs = errors(xi18nc("@info", "No mail transport configured for email identity %1", identity.identityName())); return -1; } qCDebug(KALARM_LOG) << "KAMail::send: Using transport" << transport->name() << ", id=" << transport->id(); initHeaders(*message, jobdata); err = appendBodyAttachments(*message, jobdata); if (!err.isNull()) { qCCritical(KALARM_LOG) << "KAMail::send: Error compiling message:" << err; errmsgs = errors(err); return -1; } MailTransport::MessageQueueJob* mailjob = new MailTransport::MessageQueueJob(qApp); mailjob->setMessage(message); mailjob->transportAttribute().setTransportId(transport->id()); // MessageQueueJob email addresses must be pure, i.e. without display name. Note // that display names are included in the actual headers set up by initHeaders(). mailjob->addressAttribute().setFrom(extractEmailAndNormalize(jobdata.from)); mailjob->addressAttribute().setTo(extractEmailsAndNormalize(jobdata.event.emailAddresses(QStringLiteral(",")))); if (!jobdata.bcc.isEmpty()) mailjob->addressAttribute().setBcc(extractEmailsAndNormalize(jobdata.bcc)); MailTransport::SentBehaviourAttribute::SentBehaviour sentAction = (Preferences::emailClient() == Preferences::kmail || Preferences::emailCopyToKMail()) ? MailTransport::SentBehaviourAttribute::MoveToDefaultSentCollection : MailTransport::SentBehaviourAttribute::Delete; mailjob->sentBehaviourAttribute().setSentBehaviour(sentAction); mJobs.enqueue(mailjob); mJobData.enqueue(jobdata); if (mJobs.count() == 1) { // There are no jobs already active or queued, so send now connect(mailjob, &KJob::result, instance(), &KAMail::slotEmailSent); mailjob->start(); } } return 0; } /****************************************************************************** * Called when sending an email is complete. */ void KAMail::slotEmailSent(KJob* job) { bool copyerr = false; QStringList errmsgs; if (job->error()) { qCCritical(KALARM_LOG) << "KAMail::slotEmailSent: Failed:" << job->errorString(); errmsgs = errors(job->errorString(), SEND_ERROR); } JobData jobdata; if (mJobs.isEmpty() || mJobData.isEmpty() || job != mJobs.head()) { // The queue has been corrupted, so we can't locate the job's data qCCritical(KALARM_LOG) << "KAMail::slotEmailSent: Wrong job at head of queue: wiping queue"; mJobs.clear(); mJobData.clear(); if (!errmsgs.isEmpty()) theApp()->emailSent(jobdata, errmsgs); errmsgs.clear(); errmsgs += i18nc("@info", "Emails may not have been sent"); errmsgs += i18nc("@info", "Program error"); theApp()->emailSent(jobdata, errmsgs); return; } mJobs.dequeue(); jobdata = mJobData.dequeue(); if (jobdata.allowNotify) notifyQueued(jobdata.event); theApp()->emailSent(jobdata, errmsgs, copyerr); if (!mJobs.isEmpty()) { // Send the next queued email connect(mJobs.head(), &KJob::result, instance(), &KAMail::slotEmailSent); mJobs.head()->start(); } } /****************************************************************************** * Create the headers part of the email. */ void initHeaders(KMime::Message& message, KAMail::JobData& data) { KMime::Headers::Date* date = new KMime::Headers::Date; date->setDateTime(KADateTime::currentDateTime(Preferences::timeSpec()).qDateTime()); message.setHeader(date); KMime::Headers::From* from = new KMime::Headers::From; from->fromUnicodeString(data.from, autoDetectCharset(data.from)); message.setHeader(from); KMime::Headers::To* to = new KMime::Headers::To; const KCalendarCore::Person::List toList = data.event.emailAddressees(); - for (int i = 0, count = toList.count(); i < count; ++i) - to->addAddress(toList[i].email().toLatin1(), toList[i].name()); + for (const KCalendarCore::Person& who : toList) + to->addAddress(who.email().toLatin1(), who.name()); message.setHeader(to); if (!data.bcc.isEmpty()) { KMime::Headers::Bcc* bcc = new KMime::Headers::Bcc; bcc->fromUnicodeString(data.bcc, autoDetectCharset(data.bcc)); message.setHeader(bcc); } KMime::Headers::Subject* subject = new KMime::Headers::Subject; const QString str = data.event.emailSubject(); subject->fromUnicodeString(str, autoDetectCharset(str)); message.setHeader(subject); KMime::Headers::UserAgent* agent = new KMime::Headers::UserAgent; agent->fromUnicodeString(KAboutData::applicationData().displayName() + QLatin1String("/" KALARM_VERSION), "us-ascii"); message.setHeader(agent); KMime::Headers::MessageID* id = new KMime::Headers::MessageID; id->generate(data.from.mid(data.from.indexOf(QLatin1Char('@')) + 1).toLatin1()); message.setHeader(id); } /****************************************************************************** * Append the body and attachments to the email text. * Reply = reason for error * = empty string if successful. */ QString KAMail::appendBodyAttachments(KMime::Message& message, JobData& data) { - QStringList attachments = data.event.emailAttachments(); + const QStringList attachments = data.event.emailAttachments(); if (!attachments.count()) { // There are no attachments, so simply append the message body message.contentType()->setMimeType("text/plain"); message.contentType()->setCharset("utf-8"); message.fromUnicodeString(data.event.message()); auto encodings = KMime::encodingsForData(message.body()); encodings.removeAll(KMime::Headers::CE8Bit); // not handled by KMime - message.contentTransferEncoding()->setEncoding(encodings[0]); + message.contentTransferEncoding()->setEncoding(encodings.at(0)); message.assemble(); } else { // There are attachments, so the message must be in MIME format message.contentType()->setMimeType("multipart/mixed"); message.contentType()->setBoundary(KMime::multiPartBoundary()); if (!data.event.message().isEmpty()) { // There is a message body KMime::Content* content = new KMime::Content(); content->contentType()->setMimeType("text/plain"); content->contentType()->setCharset("utf-8"); content->fromUnicodeString(data.event.message()); auto encodings = KMime::encodingsForData(content->body()); encodings.removeAll(KMime::Headers::CE8Bit); // not handled by KMime - content->contentTransferEncoding()->setEncoding(encodings[0]); + content->contentTransferEncoding()->setEncoding(encodings.at(0)); content->assemble(); message.addContent(content); } // Append each attachment in turn - for (QStringList::Iterator at = attachments.begin(); at != attachments.end(); ++at) + for (const QString& att : attachments) { - QString attachment = QString::fromLatin1((*at).toLocal8Bit()); - QUrl url = QUrl::fromUserInput(attachment, QString(), QUrl::AssumeLocalFile); - QString attachError = xi18nc("@info", "Error attaching file: %1", attachment); + const QString attachment = QString::fromLatin1(att.toLocal8Bit()); + const QUrl url = QUrl::fromUserInput(attachment, QString(), QUrl::AssumeLocalFile); + const QString attachError = xi18nc("@info", "Error attaching file: %1", attachment); QByteArray contents; bool atterror = false; if (!url.isLocalFile()) { KIO::UDSEntry uds; auto statJob = KIO::stat(url, KIO::StatJob::SourceSide, 2); KJobWidgets::setWindow(statJob, MainWindow::mainMainWindow()); if (!statJob->exec()) { qCCritical(KALARM_LOG) << "KAMail::appendBodyAttachments: Not found:" << attachment; return xi18nc("@info", "Attachment not found: %1", attachment); } KFileItem fi(statJob->statResult(), url); if (fi.isDir() || !fi.isReadable()) { qCCritical(KALARM_LOG) << "KAMail::appendBodyAttachments: Not file/not readable:" << attachment; return attachError; } // Read the file contents auto downloadJob = KIO::storedGet(url); KJobWidgets::setWindow(downloadJob, MainWindow::mainMainWindow()); if (!downloadJob->exec()) { qCCritical(KALARM_LOG) << "KAMail::appendBodyAttachments: Load failure:" << attachment; return attachError; } contents = downloadJob->data(); if (static_cast(contents.size()) < fi.size()) { qCDebug(KALARM_LOG) << "KAMail::appendBodyAttachments: Read error:" << attachment; atterror = true; } } else { QFile f(url.toLocalFile()); if (!f.open(QIODevice::ReadOnly)) { qCCritical(KALARM_LOG) << "KAMail::appendBodyAttachments: Load failure:" << attachment; return attachError; } contents = f.readAll(); } QByteArray coded = KCodecs::base64Encode(contents); KMime::Content* content = new KMime::Content(); content->setBody(coded + "\n\n"); // Set the content type QMimeDatabase mimeDb; QString typeName = mimeDb.mimeTypeForUrl(url).name(); KMime::Headers::ContentType* ctype = new KMime::Headers::ContentType; ctype->fromUnicodeString(typeName, autoDetectCharset(typeName)); ctype->setName(attachment, "local"); content->setHeader(ctype); // Set the encoding KMime::Headers::ContentTransferEncoding* cte = new KMime::Headers::ContentTransferEncoding; cte->setEncoding(KMime::Headers::CEbase64); cte->setDecoded(false); content->setHeader(cte); content->assemble(); message.addContent(content); if (atterror) return attachError; } message.assemble(); } return QString(); } /****************************************************************************** * If any of the destination email addresses are non-local, display a * notification message saying that an email has been queued for sending. */ void KAMail::notifyQueued(const KAEvent& event) { KMime::Types::Address addr; const QString localhost = QStringLiteral("localhost"); const QString hostname = QHostInfo::localHostName(); - KCalendarCore::Person::List addresses = event.emailAddressees(); - for (int i = 0, end = addresses.count(); i < end; ++i) + const KCalendarCore::Person::List addresses = event.emailAddressees(); + for (const KCalendarCore::Person& address : addresses) { - QByteArray email = addresses[i].email().toLocal8Bit(); + const QByteArray email = address.email().toLocal8Bit(); const char* em = email.constData(); if (!email.isEmpty() && HeaderParsing::parseAddress(em, em + email.length(), addr)) { - QString domain = addr.mailboxList.at(0).addrSpec().domain; + const QString domain = addr.mailboxList.at(0).addrSpec().domain; if (!domain.isEmpty() && domain != localhost && domain != hostname) { KAMessageBox::information(MainWindow::mainMainWindow(), i18nc("@info", "An email has been queued to be sent"), QString(), Preferences::EMAIL_QUEUED_NOTIFY); return; } } } } /****************************************************************************** * Fetch the user's email address configured in KMail or KDE System Settings. */ QString KAMail::controlCentreAddress() { KEMailSettings e; return e.getSetting(KEMailSettings::EmailAddress); } /****************************************************************************** * Parse a list of email addresses, optionally containing display names, * entered by the user. * Reply = the invalid item if error, else empty string. */ QString KAMail::convertAddresses(const QString& items, KCalendarCore::Person::List& list) { list.clear(); QString invalidItem; const KMime::Types::Mailbox::List mailboxes = parseAddresses(items, invalidItem); if (!invalidItem.isEmpty()) return invalidItem; - for (int i = 0, count = mailboxes.count(); i < count; ++i) + for (const KMime::Types::Mailbox& mailbox : mailboxes) { - KCalendarCore::Person person(mailboxes[i].name(), mailboxes[i].addrSpec().asString()); + const KCalendarCore::Person person(mailbox.name(), mailbox.addrSpec().asString()); list += person; } return QString(); } /****************************************************************************** * Check the validity of an email address. * Because internal email addresses don't have to abide by the usual internet * email address rules, only some basic checks are made. * Reply = 1 if alright, 0 if empty, -1 if error. */ int KAMail::checkAddress(QString& address) { address = address.trimmed(); // Check that there are no list separator characters present if (address.indexOf(QLatin1Char(',')) >= 0 || address.indexOf(QLatin1Char(';')) >= 0) return -1; - int n = address.length(); + const int n = address.length(); if (!n) return 0; int start = 0; int end = n - 1; - if (address[end] == QLatin1Char('>')) + if (address.at(end) == QLatin1Char('>')) { // The email address is in <...> if ((start = address.indexOf(QLatin1Char('<'))) < 0) return -1; ++start; --end; } - int i = address.indexOf(QLatin1Char('@'), start); + const int i = address.indexOf(QLatin1Char('@'), start); if (i >= 0) { if (i == start || i == end) // check @ isn't the first or last character // || address.indexOf(QLatin1Char('@'), i + 1) >= 0) // check for multiple @ characters return -1; } /* else { // Allow the @ character to be missing if it's a local user if (!getpwnam(address.mid(start, end - start + 1).toLocal8Bit())) return false; } for (int i = start; i <= end; ++i) { - char ch = address[i].toLatin1(); + char ch = address.at(i).toLatin1(); if (ch == '.' || ch == '@' || ch == '-' || ch == '_' || (ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9')) continue; return false; }*/ return 1; } /****************************************************************************** * Convert a comma or semicolon delimited list of attachments into a * QStringList. The items are checked for validity. * Reply = the invalid item if error, else empty string. */ QString KAMail::convertAttachments(const QString& items, QStringList& list) { list.clear(); int length = items.length(); for (int next = 0; next < length; ) { // Find the first delimiter character (, or ;) int i = items.indexOf(QLatin1Char(','), next); if (i < 0) i = items.length(); int sc = items.indexOf(QLatin1Char(';'), next); if (sc < 0) sc = items.length(); if (sc < i) i = sc; QString item = items.mid(next, i - next).trimmed(); switch (checkAttachment(item)) { case 1: list += item; break; case 0: break; // empty attachment name case -1: default: return item; // error } next = i + 1; } return QString(); } /****************************************************************************** * Check for the existence of the attachment file. * If non-null, '*url' receives the QUrl of the attachment. * Reply = 1 if attachment exists * = 0 if null name * = -1 if doesn't exist. */ int KAMail::checkAttachment(QString& attachment, QUrl* url) { attachment = attachment.trimmed(); if (attachment.isEmpty()) { if (url) *url = QUrl(); return 0; } // Check that the file exists QUrl u = QUrl::fromUserInput(attachment, QString(), QUrl::AssumeLocalFile); u.setPath(QDir::cleanPath(u.path())); if (url) *url = u; return checkAttachment(u) ? 1 : -1; } /****************************************************************************** * Check for the existence of the attachment file. */ bool KAMail::checkAttachment(const QUrl& url) { auto statJob = KIO::stat(url); KJobWidgets::setWindow(statJob, MainWindow::mainMainWindow()); if (!statJob->exec()) return false; // doesn't exist KFileItem fi(statJob->statResult(), url); if (fi.isDir() || !fi.isReadable()) return false; return true; } /****************************************************************************** * Set the appropriate error messages for a given error string. */ QStringList KAMail::errors(const QString& err, ErrType prefix) { QString error1; switch (prefix) { case SEND_FAIL: error1 = i18nc("@info", "Failed to send email"); break; case SEND_ERROR: error1 = i18nc("@info", "Error sending email"); break; #ifdef KMAIL_SUPPORTED case COPY_ERROR: error1 = i18nc("@info", "Error copying sent email to KMail %1 folder", i18n_sent_mail()); break; #endif } if (err.isEmpty()) return QStringList(error1); - QStringList errs(QStringLiteral("%1:").arg(error1)); - errs += err; - return errs; + return QStringList{QStringLiteral("%1:").arg(error1), err}; } /****************************************************************************** * Get the body of an email from KMail, given its serial number. */ QString KAMail::getMailBody(quint32 serialNumber) { //TODO: Need to use Akonadi instead - QList args; - args << serialNumber << (int)0; + const QList args{serialNumber, (int)0}; QDBusInterface iface(KMAIL_DBUS_SERVICE, QString(), QStringLiteral("KMailIface")); - QDBusReply reply = iface.callWithArgumentList(QDBus::Block, QStringLiteral("getDecodedBodyPart"), args); + const QDBusReply reply = iface.callWithArgumentList(QDBus::Block, QStringLiteral("getDecodedBodyPart"), args); if (!reply.isValid()) { qCCritical(KALARM_LOG) << "KAMail::getMailBody: D-Bus call failed:" << reply.error().message(); return QString(); } return reply.value(); } /****************************************************************************** * Extract the pure addresses from given email addresses. */ QString extractEmailAndNormalize(const QString& emailAddress) { return KEmailAddress::extractEmailAddress(KEmailAddress::normalizeAddressesAndEncodeIdn(emailAddress)); } QStringList extractEmailsAndNormalize(const QString& emailAddresses) { const QStringList splitEmails(KEmailAddress::splitAddressList(emailAddresses)); QStringList normalizedEmail; for (const QString& email : splitEmails) { normalizedEmail << KEmailAddress::extractEmailAddress(KEmailAddress::normalizeAddressesAndEncodeIdn(email)); } return normalizedEmail; } //----------------------------------------------------------------------------- // Based on KMail KMMsgBase::autoDetectCharset(). QByteArray autoDetectCharset(const QString& text) { for (QByteArray encoding : {"us-ascii", "iso-8859-1", "locale", "utf-8"}) { if (encoding == "locale") encoding = QTextCodec::codecForLocale()->name().toLower(); if (text.isEmpty()) return encoding; if (encoding == "us-ascii") { if (KMime::isUsAscii(text)) return encoding; } else { const QTextCodec* codec = codecForName(encoding); if (!codec) qCDebug(KALARM_LOG) << "KAMail::autoDetectCharset: Something is wrong and I cannot get a codec. [" << encoding <<"]"; else { if (codec->canEncode(text)) return encoding; } } } return QByteArray(); } //----------------------------------------------------------------------------- // Based on KMail KMMsgBase::codecForName(). const QTextCodec* codecForName(const QByteArray& str) { if (str.isEmpty()) return nullptr; QByteArray codec = str.toLower(); return KCharsets::charsets()->codecForName(QLatin1String(codec)); } /****************************************************************************** * Parse a string containing multiple addresses, separated by comma or semicolon, * while retaining Unicode name parts. * Note that this only needs to parse strings input into KAlarm, so it only * needs to accept the common syntax for email addresses, not obsolete syntax. */ KMime::Types::Mailbox::List parseAddresses(const QString& text, QString& invalidItem) { KMime::Types::Mailbox::List list; int state = 0; int start = 0; // start of this item int endName = 0; // character after end of name int startAddr = 0; // start of address int endAddr = 0; // character after end of address char lastch = '\0'; bool ended = false; // found the end of the item for (int i = 0, count = text.length(); i <= count; ++i) { if (i == count) ended = true; else { - char ch = text[i].toLatin1(); + const char ch = text[i].toLatin1(); switch (state) { case 0: // looking for start of item if (ch == ' ' || ch == '\t') continue; start = i; state = (ch == '"') ? 10 : 1; break; case 1: // looking for start of address, or end of item switch (ch) { case '<': startAddr = i + 1; state = 2; break; case ',': case ';': ended = true; break; case ' ': break; default: endName = i + 1; break; } break; case 2: // looking for '>' at end of address if (ch == '>') { endAddr = i; state = 3; } break; case 3: // looking for item separator if (ch == ',' || ch == ';') ended = true; else if (ch != ' ') { invalidItem = text.mid(start); return KMime::Types::Mailbox::List(); } break; case 10: // looking for closing quote if (ch == '"' && lastch != '\\') { ++start; // remove opening quote from name endName = i; state = 11; } lastch = ch; break; case 11: // looking for '<' if (ch == '<') { startAddr = i + 1; state = 2; } break; } } if (ended) { // Found the end of the item - add it to the list if (!startAddr) { startAddr = start; endAddr = endName; endName = 0; } - QString addr = text.mid(startAddr, endAddr - startAddr); + const QString addr = text.mid(startAddr, endAddr - startAddr); KMime::Types::Mailbox mbox; mbox.fromUnicodeString(addr); if (mbox.address().isEmpty()) { invalidItem = text.mid(start, endAddr - start); return KMime::Types::Mailbox::List(); } if (endName) { int len = endName - start; QString name = text.mid(start, endName - start); - if (name[0] == QLatin1Char('"') && name[len - 1] == QLatin1Char('"')) + if (name.at(0) == QLatin1Char('"') && name.at(len - 1) == QLatin1Char('"')) name = name.mid(1, len - 2); mbox.setName(name); } list.append(mbox); endName = startAddr = endAddr = 0; start = i + 1; state = 0; ended = false; } } return list; } /*============================================================================= = HeaderParsing : modified and additional functions. = The following functions are modified from, or additional to, those in = libkmime kmime_header_parsing.cpp. =============================================================================*/ namespace HeaderParsing { using namespace KMime; using namespace KMime::Types; using namespace KMime::HeaderParsing; /****************************************************************************** * New function. * Allow a local user name to be specified as an email address. */ bool parseUserName( const char* & scursor, const char * const send, QString & result, bool isCRLF ) { QString maybeLocalPart; QString tmp; if ( scursor != send ) { // first, eat any whitespace eatCFWS( scursor, send, isCRLF ); char ch = *scursor++; switch ( ch ) { case '.': // dot case '@': case '"': // quoted-string return false; default: // atom scursor--; // re-set scursor to point to ch again tmp.clear(); if ( parseAtom( scursor, send, result, false /* no 8bit */ ) ) { if (getpwnam(result.toLocal8Bit().constData())) return true; } return false; // parseAtom can only fail if the first char is non-atext. } } return false; } /****************************************************************************** * Modified function. * Allow a local user name to be specified as an email address, and reinstate * the original scursor on error return. */ bool parseAddress( const char* & scursor, const char * const send, Address & result, bool isCRLF ) { // address := mailbox / group eatCFWS( scursor, send, isCRLF ); if ( scursor == send ) return false; // first try if it's a single mailbox: Mailbox maybeMailbox; const char * oldscursor = scursor; if ( parseMailbox( scursor, send, maybeMailbox, isCRLF ) ) { // yes, it is: result.displayName.clear(); result.mailboxList.append( maybeMailbox ); return true; } scursor = oldscursor; // KAlarm: Allow a local user name to be specified // no, it's not a single mailbox. Try if it's a local user name: QString maybeUserName; if ( parseUserName( scursor, send, maybeUserName, isCRLF ) ) { // yes, it is: maybeMailbox.setName( QString() ); AddrSpec addrSpec; addrSpec.localPart = maybeUserName; addrSpec.domain.clear(); maybeMailbox.setAddress( addrSpec ); result.displayName.clear(); result.mailboxList.append( maybeMailbox ); return true; } scursor = oldscursor; Address maybeAddress; // no, it's not a single mailbox. Try if it's a group: if ( !parseGroup( scursor, send, maybeAddress, isCRLF ) ) { scursor = oldscursor; // KAlarm: reinstate original scursor on error return return false; } result = maybeAddress; return true; } } // namespace HeaderParsing // vim: et sw=4: diff --git a/src/lib/kalocale.cpp b/src/lib/kalocale.cpp index 84371bce..759f52a4 100644 --- a/src/lib/kalocale.cpp +++ b/src/lib/kalocale.cpp @@ -1,92 +1,92 @@ /* * kalocale.cpp - miscellaneous locale functions * Program: kalarm - * Copyright © 2003-2018 by David Jarvie + * Copyright © 2003-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 "kalocale.h" #include "kalarm_debug.h" #include namespace { int firstDay = 0; int firstWorkDay = 0; int lastWorkDay = 0; uint workDays = 0; /****************************************************************************** * Return the first day of the week for the user's locale. * Reply = 1 (Mon) .. 7 (Sun). */ int localeFirstDayOfWeek() { static QLocale locale; if (!firstDay || locale != QLocale()) { Q_ASSERT(Qt::Monday == 1 && Qt::Sunday == 7); // all weekday numbering assumes this locale = QLocale(); firstDay = locale.firstDayOfWeek(); QList weekDays = locale.weekdays(); - for (int i = 0; i < weekDays.size(); ++i) - workDays |= 1 << (weekDays.at(i) - 1); + for (Qt::DayOfWeek weekDay : qAsConst(weekDays)) + workDays |= 1 << (weekDay - 1); std::sort(weekDays.begin(), weekDays.end()); int day = 0; - for (int i = 0; i < weekDays.size(); ++i) - if (++day < weekDays.at(i)) + for (Qt::DayOfWeek weekDay : qAsConst(weekDays)) + if (++day < weekDay) { lastWorkDay = (day == 1) ? weekDays.at(weekDays.size() - 1) : day - 1; - firstWorkDay = weekDays.at(i); + firstWorkDay = weekDay; break; } } return firstDay; } } namespace KAlarm { /*****************************************************************************/ int weekDay_to_localeDayInWeek(int weekDay) { return (weekDay + 7 - localeFirstDayOfWeek()) % 7; } /*****************************************************************************/ int localeDayInWeek_to_weekDay(int index) { return (index + localeFirstDayOfWeek() - 1) % 7 + 1; } /****************************************************************************** * Return the default work days in the week, as a bit mask. * Reply = bit 1 set for Monday ... bit 0x40 for Sunday. */ uint defaultWorkDays() { localeFirstDayOfWeek(); return workDays; } } // namespace KAlarm // vim: et sw=4: diff --git a/src/preferences.cpp b/src/preferences.cpp index e09c4b3e..beaa6f7b 100644 --- a/src/preferences.cpp +++ b/src/preferences.cpp @@ -1,508 +1,513 @@ /* * preferences.cpp - program preference settings * Program: kalarm - * Copyright © 2001-2018 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" #include "functions.h" #include "kamail.h" #include "messagebox.h" #include "preferences.h" #include #include #include #include #include #include #include #include "kalarm_debug.h" #include #include #include using namespace KHolidays; using namespace KAlarmCal; // Config file entry names static const char* GENERAL_SECTION = "General"; // Config file entry name for temporary use static const char* TEMP = "Temp"; static const QString AUTOSTART_FILE(QStringLiteral("kalarm.autostart.desktop")); // Values for EmailFrom entry static const QString FROM_SYS_SETTINGS(QStringLiteral("@SystemSettings")); static const QString FROM_KMAIL(QStringLiteral("@KMail")); // Config file entry names for notification messages const QLatin1String Preferences::QUIT_WARN("QuitWarn"); const QLatin1String Preferences::ASK_AUTO_START("AskAutoStart"); const QLatin1String Preferences::CONFIRM_ALARM_DELETION("ConfirmAlarmDeletion"); const QLatin1String Preferences::EMAIL_QUEUED_NOTIFY("EmailQueuedNotify"); const bool default_quitWarn = true; const bool default_emailQueuedNotify = false; const bool default_confirmAlarmDeletion = true; static QString translateXTermPath(const QString& cmdline, bool write); Preferences* Preferences::mInstance = nullptr; bool Preferences::mUsingDefaults = false; HolidayRegion* Preferences::mHolidays = nullptr; // always non-null after Preferences initialisation QString Preferences::mPreviousVersion; Preferences::Backend Preferences::mPreviousBackend; // Change tracking bool Preferences::mAutoStartChangedByUser = false; Preferences* Preferences::self() { if (!mInstance) { // Set the default button for the Quit warning message box to Cancel KAMessageBox::setContinueDefault(QUIT_WARN, KMessageBox::Cancel); KAMessageBox::setDefaultShouldBeShownContinue(QUIT_WARN, default_quitWarn); KAMessageBox::setDefaultShouldBeShownContinue(EMAIL_QUEUED_NOTIFY, default_emailQueuedNotify); KAMessageBox::setDefaultShouldBeShownContinue(CONFIRM_ALARM_DELETION, default_confirmAlarmDeletion); mInstance = new Preferences; } return mInstance; } Preferences::Preferences() { QObject::connect(this, &Preferences::base_StartOfDayChanged, this, &Preferences::startDayChange); QObject::connect(this, &Preferences::base_TimeZoneChanged, this, &Preferences::timeZoneChange); QObject::connect(this, &Preferences::base_HolidayRegionChanged, this, &Preferences::holidaysChange); QObject::connect(this, &Preferences::base_WorkTimeChanged, this, &Preferences::workTimeChange); load(); // Fetch the KAlarm version and backend which wrote the previous config file mPreviousVersion = version(); mPreviousBackend = backend(); // Update the KAlarm version in the config file, but don't call // writeConfig() here - leave it to be written only if the config file // is updated with other data. setVersion(QStringLiteral(KALARM_VERSION)); } /****************************************************************************** * Auto hiding of the system tray icon is only allowed on desktops which provide * GUI controls to show hidden icons. */ int Preferences::autoHideSystemTray() { if(noAutoHideSystemTrayDesktops().contains(KAlarm::currentDesktopIdentityName())) return 0; // never hide return self()->mBase_AutoHideSystemTray; } /****************************************************************************** * Auto hiding of the system tray icon is only allowed on desktops which provide * GUI controls to show hidden icons, so while KAlarm is running on such a * desktop, don't allow changes to the setting. */ void Preferences::setAutoHideSystemTray(int timeout) { if(noAutoHideSystemTrayDesktops().contains(KAlarm::currentDesktopIdentityName())) return; self()->setBase_AutoHideSystemTray(timeout); } void Preferences::setAskAutoStart(bool yes) { KAMessageBox::saveDontShowAgainYesNo(ASK_AUTO_START, !yes); } /****************************************************************************** * Set the NoAutoStart condition. * On KDE desktops, the "X-KDE-autostart-condition" entry in * kalarm.autostart.desktop references this to determine whether to autostart KAlarm. * On non-KDE desktops, the "X-KDE-autostart-condition" entry in * kalarm.autostart.desktop doesn't have any effect, so that KAlarm will be * autostarted even if it is set not to autostart. Adding a "Hidden" entry to, * and removing the "OnlyShowIn=KDE" entry from, a user-modifiable copy of the * file fixes this. */ void Preferences::setNoAutoStart(bool yes) { // Find the existing kalarm.autostart.desktop file, and whether it's writable. bool existingRO = true; // whether the existing file is read-only QString autostartFile; const QStringList autostartDirs = QStandardPaths::standardLocations(QStandardPaths::GenericConfigLocation); for (const QString& dir : autostartDirs) { const QString file = dir + QStringLiteral("/autostart/") + AUTOSTART_FILE; if (QFile::exists(file)) { QFileInfo info(file); if (info.isReadable()) { autostartFile = file; existingRO = !info.isWritable(); break; } } } // If the existing file isn't writable, find the path to create a writable copy QString autostartFileRW = autostartFile; if (existingRO) { autostartFileRW = QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + QStringLiteral("/autostart/") + AUTOSTART_FILE; if (autostartFileRW.isEmpty()) { qCWarning(KALARM_LOG) << "Preferences::setNoAutoStart: No writable autostart file path"; return; } if (QFile::exists(autostartFileRW)) { QFileInfo info(autostartFileRW); if (!info.isReadable() || !info.isWritable()) { qCWarning(KALARM_LOG) << "Preferences::setNoAutoStart: Autostart file is not read/write:" << autostartFileRW; return; } } } // Read the existing file and remove any "Hidden=" and "OnlyShowIn=" entries bool update = false; QStringList lines; { QFile file(autostartFile); if (!file.open(QIODevice::ReadOnly)) { qCWarning(KALARM_LOG) << "Preferences::setNoAutoStart: Error reading autostart file:" << autostartFile; return; } QTextStream stream(&file); stream.setCodec("UTF-8"); stream.setAutoDetectUnicode(true); lines = stream.readAll().split(QLatin1Char('\n')); for (int i = 0; i < lines.size(); ++i) { - const QString line = lines[i].trimmed(); + const QString line = lines.at(i).trimmed(); if (line.isEmpty()) { lines.removeAt(i); --i; } else if (line.startsWith(QStringLiteral("Hidden=")) || line.startsWith(QStringLiteral("OnlyShowIn="))) { lines.removeAt(i); update = true; --i; } } } if (yes) { // Add a "Hidden" entry to the local kalarm.autostart.desktop file, to // prevent autostart from happening. lines += QStringLiteral("Hidden=true"); update = true; } if (update) { // Write the updated file QFile file(autostartFileRW); if (!file.open(QIODevice::WriteOnly)) { qCWarning(KALARM_LOG) << "Preferences::setNoAutoStart: Error writing autostart file:" << autostartFileRW; return; } QTextStream stream(&file); stream.setCodec("UTF-8"); stream << lines.join(QLatin1Char('\n')) << "\n"; qCDebug(KALARM_LOG) << "Preferences::setNoAutoStart: Written" << autostartFileRW; } self()->setBase_NoAutoStart(yes); } /****************************************************************************** * Get the user's time zone, or if none has been chosen, the system time zone. * Reply = time zone, or invalid to use the local time zone. */ KADateTime::Spec Preferences::timeSpec() { const QByteArray zoneId = self()->mBase_TimeZone.toLatin1(); return zoneId.isEmpty() ? KADateTime::LocalZone : KADateTime::Spec(QTimeZone(zoneId)); } QTimeZone Preferences::timeSpecAsZone() { const QByteArray zoneId = self()->mBase_TimeZone.toLatin1(); return zoneId.isEmpty() ? QTimeZone::systemTimeZone() : QTimeZone(zoneId); } void Preferences::setTimeSpec(const KADateTime::Spec& spec) { self()->setBase_TimeZone(spec.type() == KADateTime::TimeZone ? QString::fromLatin1(spec.timeZone().id()) : QString()); } void Preferences::timeZoneChange(const QString& zone) { Q_UNUSED(zone); Q_EMIT mInstance->timeZoneChanged(timeSpec()); } const HolidayRegion& Preferences::holidays() { QString regionCode = self()->mBase_HolidayRegion; if (!mHolidays || mHolidays->regionCode() != regionCode) { delete mHolidays; mHolidays = new HolidayRegion(regionCode); } return *mHolidays; } void Preferences::setHolidayRegion(const QString& regionCode) { self()->setBase_HolidayRegion(regionCode); } void Preferences::holidaysChange(const QString& regionCode) { Q_UNUSED(regionCode); Q_EMIT mInstance->holidaysChanged(holidays()); } void Preferences::setStartOfDay(const QTime& t) { if (t != self()->mBase_StartOfDay.time()) { self()->setBase_StartOfDay(QDateTime(QDate(1900,1,1), t)); Q_EMIT mInstance->startOfDayChanged(t); } } // Called when the start of day value has changed in the config file void Preferences::startDayChange(const QDateTime& dt) { Q_EMIT mInstance->startOfDayChanged(dt.time()); } QBitArray Preferences::workDays() { unsigned days = self()->base_WorkDays(); QBitArray dayBits(7); for (int i = 0; i < 7; ++i) dayBits.setBit(i, days & (1 << i)); return dayBits; } void Preferences::setWorkDays(const QBitArray& dayBits) { + if (dayBits.size() != 7) + { + qCWarning(KALARM_LOG) << "Preferences::setWorkDays: Error! 'dayBits' parameter must have 7 elements: actual size" << dayBits.size(); + return; + } unsigned days = 0; for (int i = 0; i < 7; ++i) if (dayBits.testBit(i)) days |= 1 << i; self()->setBase_WorkDays(days); } void Preferences::workTimeChange(const QDateTime& start, const QDateTime& end, int days) { QBitArray dayBits(7); for (int i = 0; i < 7; ++i) if (days & (1 << i)) dayBits.setBit(i); Q_EMIT mInstance->workTimeChanged(start.time(), end.time(), dayBits); } Preferences::MailFrom Preferences::emailFrom() { - QString from = self()->mBase_EmailFrom; + const QString from = self()->mBase_EmailFrom; if (from == FROM_KMAIL) return MAIL_FROM_KMAIL; if (from == FROM_SYS_SETTINGS) return MAIL_FROM_SYS_SETTINGS; return MAIL_FROM_ADDR; } /****************************************************************************** * Get user's default 'From' email address. */ QString Preferences::emailAddress() { - QString from = self()->mBase_EmailFrom; + const QString from = self()->mBase_EmailFrom; if (from == FROM_KMAIL) return Identities::identityManager()->defaultIdentity().fullEmailAddr(); if (from == FROM_SYS_SETTINGS) return KAMail::controlCentreAddress(); return from; } void Preferences::setEmailAddress(Preferences::MailFrom from, const QString& address) { QString out; switch (from) { case MAIL_FROM_KMAIL: out = FROM_KMAIL; break; case MAIL_FROM_SYS_SETTINGS: out = FROM_SYS_SETTINGS; break; case MAIL_FROM_ADDR: out = address; break; default: return; } self()->setBase_EmailFrom(out); } Preferences::MailFrom Preferences::emailBccFrom() { - QString from = self()->mBase_EmailBccAddress; + const QString from = self()->mBase_EmailBccAddress; if (from == FROM_SYS_SETTINGS) return MAIL_FROM_SYS_SETTINGS; return MAIL_FROM_ADDR; } QString Preferences::emailBccAddress() { - QString from = self()->mBase_EmailBccAddress; + const QString from = self()->mBase_EmailBccAddress; if (from == FROM_SYS_SETTINGS) return KAMail::controlCentreAddress(); return from; } bool Preferences::emailBccUseSystemSettings() { return self()->mBase_EmailBccAddress == FROM_SYS_SETTINGS; } void Preferences::setEmailBccAddress(bool useSystemSettings, const QString& address) { QString out; if (useSystemSettings) out = FROM_SYS_SETTINGS; else out = address; self()->setBase_EmailBccAddress(out); } QString Preferences::cmdXTermCommand() { return translateXTermPath(self()->mBase_CmdXTermCommand, false); } void Preferences::setCmdXTermCommand(const QString& cmd) { self()->setBase_CmdXTermCommand(translateXTermPath(cmd, true)); } void Preferences::connect(const char* signal, const QObject* receiver, const char* member) { QObject::connect(self(), signal, receiver, member); } /****************************************************************************** * Called to allow or suppress output of the specified message dialog, where the * dialog has a checkbox to turn notification off. */ void Preferences::setNotify(const QString& messageID, bool notify) { KAMessageBox::saveDontShowAgainContinue(messageID, !notify); } /****************************************************************************** * Return whether the specified message dialog is output, where the dialog has * a checkbox to turn notification off. * Reply = false if message has been suppressed (by preferences or by selecting * "don't ask again") * = true in all other cases. */ bool Preferences::notifying(const QString& messageID) { return KAMessageBox::shouldBeShownContinue(messageID); } /****************************************************************************** * Translate an X terminal command path to/from config file format. * Note that only a home directory specification at the start of the path is * translated, so there's no need to worry about missing out some of the * executable's path due to quotes etc. * N.B. Calling KConfig::read/writePathEntry() on the entire command line * causes a crash on some systems, so it's necessary to extract the * executable path first before processing. */ QString translateXTermPath(const QString& cmdline, bool write) { QString params; QString cmd = cmdline; if (cmdline.isEmpty()) return cmdline; // Strip any leading quote - QChar quote = cmdline[0]; - char q = quote.toLatin1(); - bool quoted = (q == '"' || q == '\''); + const QChar quote = cmdline[0]; + const char q = quote.toLatin1(); + const bool quoted = (q == '"' || q == '\''); if (quoted) cmd = cmdline.mid(1); // Split the command at the first non-escaped space for (int i = 0, count = cmd.length(); i < count; ++i) { - switch (cmd[i].toLatin1()) + switch (cmd.at(i).toLatin1()) { case '\\': ++i; continue; case '"': case '\'': - if (cmd[i] != quote) + if (cmd.at(i) != quote) continue; // fall through to ' ' Q_FALLTHROUGH(); case ' ': params = cmd.mid(i); cmd = cmd.left(i); break; default: continue; } break; } // Translate any home directory specification at the start of the // executable's path. KConfigGroup group(KSharedConfig::openConfig(), GENERAL_SECTION); if (write) { group.writePathEntry(TEMP, cmd); cmd = group.readEntry(TEMP, QString()); } else { group.writeEntry(TEMP, cmd); cmd = group.readPathEntry(TEMP, QString()); } group.deleteEntry(TEMP); if (quoted) return quote + cmd + params; else return cmd + params; } // vim: et sw=4: diff --git a/src/recurrenceedit.cpp b/src/recurrenceedit.cpp index 0930fe3b..b709113d 100644 --- a/src/recurrenceedit.cpp +++ b/src/recurrenceedit.cpp @@ -1,1742 +1,1744 @@ /* * recurrenceedit.cpp - widget to edit the event's recurrence definition * Program: kalarm * Copyright © 2002-2019 David Jarvie * * Based originally on KOrganizer module koeditorrecurrence.cpp, * Copyright (c) 2000,2001 Cornelius Schumacher * * 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 "recurrenceedit.h" #include "recurrenceedit_p.h" #include "alarmtimewidget.h" #include "checkbox.h" #include "combobox.h" #include "kalarmapp.h" #include "kalocale.h" #include "preferences.h" #include "radiobutton.h" #include "repetitionbutton.h" #include "spinbox.h" #include "timeedit.h" #include "timespinbox.h" #include "buttongroup.h" #include #include #include using namespace KCalendarCore; #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "kalarm_debug.h" class ListWidget : public QListWidget { public: explicit ListWidget(QWidget* parent) : QListWidget(parent) {} QSize sizeHint() const override { return minimumSizeHint(); } }; // Collect these widget labels together to ensure consistent wording and // translations across different modules. QString RecurrenceEdit::i18n_combo_NoRecur() { return i18nc("@item:inlistbox Recurrence type", "No Recurrence"); } QString RecurrenceEdit::i18n_combo_AtLogin() { return i18nc("@item:inlistbox Recurrence type", "At Login"); } QString RecurrenceEdit::i18n_combo_HourlyMinutely() { return i18nc("@item:inlistbox Recurrence type", "Hourly/Minutely"); } QString RecurrenceEdit::i18n_combo_Daily() { return i18nc("@item:inlistbox Recurrence type", "Daily"); } QString RecurrenceEdit::i18n_combo_Weekly() { return i18nc("@item:inlistbox Recurrence type", "Weekly"); } QString RecurrenceEdit::i18n_combo_Monthly() { return i18nc("@item:inlistbox Recurrence type", "Monthly"); } QString RecurrenceEdit::i18n_combo_Yearly() { return i18nc("@item:inlistbox Recurrence type", "Yearly"); } RecurrenceEdit::RecurrenceEdit(bool readOnly, QWidget* parent) - : QFrame(parent), - mRule(nullptr), - mRuleButtonType(INVALID_RECUR), - mDailyShown(false), - mWeeklyShown(false), - mMonthlyShown(false), - mYearlyShown(false), - mNoEmitTypeChanged(true), - mReadOnly(readOnly) + : QFrame(parent) + , mRule(nullptr) + , mRuleButtonType(INVALID_RECUR) + , mDailyShown(false) + , mWeeklyShown(false) + , mMonthlyShown(false) + , mYearlyShown(false) + , mNoEmitTypeChanged(true) + , mReadOnly(readOnly) { qCDebug(KALARM_LOG) << "RecurrenceEdit:"; QVBoxLayout* topLayout = new QVBoxLayout(this); topLayout->setContentsMargins(0, 0, 0, 0); topLayout->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); /* Create the recurrence rule Group box which holds the recurrence period * selection buttons, and the weekly, monthly and yearly recurrence rule * frames which specify options individual to each of these distinct * sections of the recurrence rule. Each frame is made visible by the * selection of its corresponding radio button. */ QGroupBox* recurGroup = new QGroupBox(i18nc("@title:group", "Recurrence Rule"), this); topLayout->addWidget(recurGroup); QHBoxLayout* hlayout = new QHBoxLayout(recurGroup); int dcm = style()->pixelMetric(QStyle::PM_DefaultChildMargin); hlayout->setContentsMargins(dcm, dcm, dcm, dcm); hlayout->setSpacing(style()->pixelMetric(QStyle::PM_DefaultChildMargin)); // use margin spacing due to vertical divider line // Recurrence period radio buttons QVBoxLayout* vlayout = new QVBoxLayout(); vlayout->setSpacing(0); vlayout->setContentsMargins(0, 0, 0, 0); hlayout->addLayout(vlayout); mRuleButtonGroup = new ButtonGroup(recurGroup); connect(mRuleButtonGroup, &ButtonGroup::buttonSet, this, &RecurrenceEdit::periodClicked); connect(mRuleButtonGroup, &ButtonGroup::buttonSet, this, &RecurrenceEdit::contentsChanged); mNoneButton = new RadioButton(i18n_combo_NoRecur(), recurGroup); mNoneButton->setFixedSize(mNoneButton->sizeHint()); mNoneButton->setReadOnly(mReadOnly); mNoneButton->setWhatsThis(i18nc("@info:whatsthis", "Do not repeat the alarm")); mRuleButtonGroup->addButton(mNoneButton); vlayout->addWidget(mNoneButton); mAtLoginButton = new RadioButton(i18n_combo_AtLogin(), recurGroup); mAtLoginButton->setFixedSize(mAtLoginButton->sizeHint()); mAtLoginButton->setReadOnly(mReadOnly); mAtLoginButton->setWhatsThis(xi18nc("@info:whatsthis", "Trigger the alarm at the specified date/time and at every login until then." "Note that it will also be triggered any time KAlarm is restarted.")); mRuleButtonGroup->addButton(mAtLoginButton); vlayout->addWidget(mAtLoginButton); mSubDailyButton = new RadioButton(i18n_combo_HourlyMinutely(), recurGroup); mSubDailyButton->setFixedSize(mSubDailyButton->sizeHint()); mSubDailyButton->setReadOnly(mReadOnly); mSubDailyButton->setWhatsThis(i18nc("@info:whatsthis", "Repeat the alarm at hourly/minutely intervals")); mRuleButtonGroup->addButton(mSubDailyButton); vlayout->addWidget(mSubDailyButton); mDailyButton = new RadioButton(i18n_combo_Daily(), recurGroup); mDailyButton->setFixedSize(mDailyButton->sizeHint()); mDailyButton->setReadOnly(mReadOnly); mDailyButton->setWhatsThis(i18nc("@info:whatsthis", "Repeat the alarm at daily intervals")); mRuleButtonGroup->addButton(mDailyButton); vlayout->addWidget(mDailyButton); mWeeklyButton = new RadioButton(i18n_combo_Weekly(), recurGroup); mWeeklyButton->setFixedSize(mWeeklyButton->sizeHint()); mWeeklyButton->setReadOnly(mReadOnly); mWeeklyButton->setWhatsThis(i18nc("@info:whatsthis", "Repeat the alarm at weekly intervals")); mRuleButtonGroup->addButton(mWeeklyButton); vlayout->addWidget(mWeeklyButton); mMonthlyButton = new RadioButton(i18n_combo_Monthly(), recurGroup); mMonthlyButton->setFixedSize(mMonthlyButton->sizeHint()); mMonthlyButton->setReadOnly(mReadOnly); mMonthlyButton->setWhatsThis(i18nc("@info:whatsthis", "Repeat the alarm at monthly intervals")); mRuleButtonGroup->addButton(mMonthlyButton); vlayout->addWidget(mMonthlyButton); mYearlyButton = new RadioButton(i18n_combo_Yearly(), recurGroup); mYearlyButton->setFixedSize(mYearlyButton->sizeHint()); mYearlyButton->setReadOnly(mReadOnly); mYearlyButton->setWhatsThis(i18nc("@info:whatsthis", "Repeat the alarm at annual intervals")); mRuleButtonGroup->addButton(mYearlyButton); vlayout->addWidget(mYearlyButton); vlayout->addStretch(); // top-adjust the interval radio buttons // Sub-repetition button mSubRepetition = new RepetitionButton(i18nc("@action:button", "Sub-Repetition"), true, recurGroup); mSubRepetition->setFixedSize(mSubRepetition->sizeHint()); mSubRepetition->setReadOnly(mReadOnly); mSubRepetition->setWhatsThis(i18nc("@info:whatsthis", "Set up a repetition within the recurrence, to trigger the alarm multiple times each time the recurrence is due.")); connect(mSubRepetition, &RepetitionButton::needsInitialisation, this, &RecurrenceEdit::repeatNeedsInitialisation); connect(mSubRepetition, &RepetitionButton::changed, this, &RecurrenceEdit::frequencyChanged); connect(mSubRepetition, &RepetitionButton::changed, this, &RecurrenceEdit::contentsChanged); vlayout->addSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); vlayout->addWidget(mSubRepetition); // Vertical divider line vlayout = new QVBoxLayout(); vlayout->setContentsMargins(0, 0, 0, 0); hlayout->addLayout(vlayout); QFrame* divider = new QFrame(recurGroup); divider->setFrameStyle(QFrame::VLine | QFrame::Sunken); vlayout->addWidget(divider, 1); // Rule definition stack mRuleStack = new QStackedWidget(recurGroup); hlayout->addWidget(mRuleStack); hlayout->addStretch(1); mNoRule = new NoRule(mRuleStack); mSubDailyRule = new SubDailyRule(mReadOnly, mRuleStack); mDailyRule = new DailyRule(mReadOnly, mRuleStack); mWeeklyRule = new WeeklyRule(mReadOnly, mRuleStack); mMonthlyRule = new MonthlyRule(mReadOnly, mRuleStack); mYearlyRule = new YearlyRule(mReadOnly, mRuleStack); connect(mSubDailyRule, &SubDailyRule::frequencyChanged, this, &RecurrenceEdit::frequencyChanged); connect(mDailyRule, &DailyRule::frequencyChanged, this, &RecurrenceEdit::frequencyChanged); connect(mWeeklyRule, &WeeklyRule::frequencyChanged, this, &RecurrenceEdit::frequencyChanged); connect(mMonthlyRule, &MonthlyRule::frequencyChanged, this, &RecurrenceEdit::frequencyChanged); connect(mYearlyRule, &YearlyRule::frequencyChanged, this, &RecurrenceEdit::frequencyChanged); connect(mSubDailyRule, &SubDailyRule::changed, this, &RecurrenceEdit::contentsChanged); connect(mDailyRule, &DailyRule::changed, this, &RecurrenceEdit::contentsChanged); connect(mWeeklyRule, &WeeklyRule::changed, this, &RecurrenceEdit::contentsChanged); connect(mMonthlyRule, &MonthlyRule::changed, this, &RecurrenceEdit::contentsChanged); connect(mYearlyRule, &YearlyRule::changed, this, &RecurrenceEdit::contentsChanged); mRuleStack->addWidget(mNoRule); mRuleStack->addWidget(mSubDailyRule); mRuleStack->addWidget(mDailyRule); mRuleStack->addWidget(mWeeklyRule); mRuleStack->addWidget(mMonthlyRule); mRuleStack->addWidget(mYearlyRule); hlayout->addSpacing(style()->pixelMetric(QStyle::PM_DefaultChildMargin)); // Create the recurrence range group which contains the controls // which specify how long the recurrence is to last. mRangeButtonBox = new QGroupBox(i18nc("@title:group", "Recurrence End"), this); topLayout->addWidget(mRangeButtonBox); mRangeButtonGroup = new ButtonGroup(mRangeButtonBox); connect(mRangeButtonGroup, &ButtonGroup::buttonSet, this, &RecurrenceEdit::rangeTypeClicked); connect(mRangeButtonGroup, &ButtonGroup::buttonSet, this, &RecurrenceEdit::contentsChanged); vlayout = new QVBoxLayout(mRangeButtonBox); vlayout->setContentsMargins(dcm, dcm, dcm, dcm); vlayout->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); mNoEndDateButton = new RadioButton(i18nc("@option:radio", "No end"), mRangeButtonBox); mNoEndDateButton->setFixedSize(mNoEndDateButton->sizeHint()); mNoEndDateButton->setReadOnly(mReadOnly); mNoEndDateButton->setWhatsThis(i18nc("@info:whatsthis", "Repeat the alarm indefinitely")); mRangeButtonGroup->addButton(mNoEndDateButton); vlayout->addWidget(mNoEndDateButton, 1, Qt::AlignLeft); QSize size = mNoEndDateButton->size(); hlayout = new QHBoxLayout(); hlayout->setContentsMargins(0, 0, 0, 0); vlayout->addLayout(hlayout); mRepeatCountButton = new RadioButton(i18nc("@option:radio", "End after:"), mRangeButtonBox); mRepeatCountButton->setReadOnly(mReadOnly); mRepeatCountButton->setWhatsThis(i18nc("@info:whatsthis", "Repeat the alarm for the number of times specified")); mRangeButtonGroup->addButton(mRepeatCountButton); mRepeatCountEntry = new SpinBox(1, 9999, mRangeButtonBox); mRepeatCountEntry->setFixedSize(mRepeatCountEntry->sizeHint()); mRepeatCountEntry->setSingleShiftStep(10); mRepeatCountEntry->setSelectOnStep(false); mRepeatCountEntry->setReadOnly(mReadOnly); mRepeatCountEntry->setWhatsThis(i18nc("@info:whatsthis", "Enter the total number of times to trigger the alarm")); connect(mRepeatCountEntry, static_cast(&SpinBox::valueChanged), this, &RecurrenceEdit::repeatCountChanged); connect(mRepeatCountEntry, static_cast(&SpinBox::valueChanged), this, &RecurrenceEdit::contentsChanged); mRepeatCountButton->setFocusWidget(mRepeatCountEntry); mRepeatCountLabel = new QLabel(i18nc("@label", "occurrence(s)"), mRangeButtonBox); mRepeatCountLabel->setFixedSize(mRepeatCountLabel->sizeHint()); hlayout->addWidget(mRepeatCountButton); hlayout->addSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); hlayout->addWidget(mRepeatCountEntry); hlayout->addWidget(mRepeatCountLabel); hlayout->addStretch(); size = size.expandedTo(mRepeatCountButton->sizeHint()); hlayout = new QHBoxLayout(); hlayout->setContentsMargins(0, 0, 0, 0); vlayout->addLayout(hlayout); mEndDateButton = new RadioButton(i18nc("@option:radio", "End by:"), mRangeButtonBox); mEndDateButton->setReadOnly(mReadOnly); mEndDateButton->setWhatsThis( xi18nc("@info:whatsthis", "Repeat the alarm until the date/time specified." "This applies to the main recurrence only. It does not limit any sub-repetition which will occur regardless after the last main recurrence.")); mRangeButtonGroup->addButton(mEndDateButton); mEndDateEdit = new KDateComboBox(mRangeButtonBox); mEndDateEdit->setOptions(mReadOnly ? KDateComboBox::Options{} : KDateComboBox::EditDate | KDateComboBox::SelectDate | KDateComboBox::DatePicker); static const QString tzText = i18nc("@info", "This uses the same time zone as the start time."); mEndDateEdit->setWhatsThis(xi18nc("@info:whatsthis", "Enter the last date to repeat the alarm.%1", tzText)); connect(mEndDateEdit, &KDateComboBox::dateEdited, this, &RecurrenceEdit::contentsChanged); mEndDateButton->setFocusWidget(mEndDateEdit); mEndTimeEdit = new TimeEdit(mRangeButtonBox); mEndTimeEdit->setFixedSize(mEndTimeEdit->sizeHint()); mEndTimeEdit->setReadOnly(mReadOnly); mEndTimeEdit->setWhatsThis(xi18nc("@info:whatsthis", "Enter the last time to repeat the alarm.%1%2", tzText, TimeSpinBox::shiftWhatsThis())); connect(mEndTimeEdit, &TimeEdit::valueChanged, this, &RecurrenceEdit::contentsChanged); mEndAnyTimeCheckBox = new CheckBox(i18nc("@option:check", "Any time"), mRangeButtonBox); mEndAnyTimeCheckBox->setFixedSize(mEndAnyTimeCheckBox->sizeHint()); mEndAnyTimeCheckBox->setReadOnly(mReadOnly); mEndAnyTimeCheckBox->setWhatsThis(i18nc("@info:whatsthis", "Stop repeating the alarm after your first login on or after the specified end date")); connect(mEndAnyTimeCheckBox, &CheckBox::toggled, this, &RecurrenceEdit::slotAnyTimeToggled); connect(mEndAnyTimeCheckBox, &CheckBox::toggled, this, &RecurrenceEdit::contentsChanged); hlayout->addWidget(mEndDateButton); hlayout->addSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); hlayout->addWidget(mEndDateEdit); hlayout->addWidget(mEndTimeEdit); hlayout->addWidget(mEndAnyTimeCheckBox); hlayout->addStretch(); size = size.expandedTo(mEndDateButton->sizeHint()); // Line up the widgets to the right of the radio buttons mRepeatCountButton->setFixedSize(size); mEndDateButton->setFixedSize(size); // Create the exceptions group which specifies dates to be excluded // from the recurrence. mExceptionGroup = new QGroupBox(i18nc("@title:group", "Exceptions"), this); topLayout->addWidget(mExceptionGroup); topLayout->setStretchFactor(mExceptionGroup, 2); hlayout = new QHBoxLayout(mExceptionGroup); hlayout->setContentsMargins(dcm, dcm, dcm, dcm); hlayout->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); vlayout = new QVBoxLayout(); vlayout->setContentsMargins(0, 0, 0, 0); hlayout->addLayout(vlayout); mExceptionDateList = new ListWidget(mExceptionGroup); mExceptionDateList->setWhatsThis(i18nc("@info:whatsthis", "The list of exceptions, i.e. dates/times excluded from the recurrence")); connect(mExceptionDateList, &QListWidget::currentRowChanged, this, &RecurrenceEdit::enableExceptionButtons); vlayout->addWidget(mExceptionDateList); if (mReadOnly) { mExceptionDateEdit = nullptr; mChangeExceptionButton = nullptr; mDeleteExceptionButton = nullptr; } else { vlayout = new QVBoxLayout(); vlayout->setContentsMargins(0, 0, 0, 0); hlayout->addLayout(vlayout); mExceptionDateEdit = new KDateComboBox(mExceptionGroup); mExceptionDateEdit->setOptions(mReadOnly ? KDateComboBox::Options{} : KDateComboBox::EditDate | KDateComboBox::SelectDate | KDateComboBox::DatePicker); mExceptionDateEdit->setDate(KADateTime::currentLocalDate()); mExceptionDateEdit->setWhatsThis(i18nc("@info:whatsthis", "Enter a date to insert in the exceptions list. " "Use in conjunction with the Add or Change button below.")); vlayout->addWidget(mExceptionDateEdit, 0, Qt::AlignLeft); hlayout = new QHBoxLayout(); hlayout->setContentsMargins(0, 0, 0, 0); vlayout->addLayout(hlayout); QPushButton* button = new QPushButton(i18nc("@action:button", "Add"), mExceptionGroup); button->setWhatsThis(i18nc("@info:whatsthis", "Add the date entered above to the exceptions list")); connect(button, &QPushButton::clicked, this, &RecurrenceEdit::addException); hlayout->addWidget(button); mChangeExceptionButton = new QPushButton(i18nc("@action:button", "Change"), mExceptionGroup); mChangeExceptionButton->setWhatsThis(i18nc("@info:whatsthis", "Replace the currently highlighted item in the exceptions list with the date entered above")); connect(mChangeExceptionButton, &QPushButton::clicked, this, &RecurrenceEdit::changeException); hlayout->addWidget(mChangeExceptionButton); mDeleteExceptionButton = new QPushButton(i18nc("@action:button", "Delete"), mExceptionGroup); mDeleteExceptionButton->setWhatsThis(i18nc("@info:whatsthis", "Remove the currently highlighted item from the exceptions list")); connect(mDeleteExceptionButton, &QPushButton::clicked, this, &RecurrenceEdit::deleteException); hlayout->addWidget(mDeleteExceptionButton); } vlayout->addStretch(); mExcludeHolidays = new CheckBox(i18nc("@option:check", "Exclude holidays"), mExceptionGroup); mExcludeHolidays->setReadOnly(mReadOnly); mExcludeHolidays->setWhatsThis(xi18nc("@info:whatsthis", "Do not trigger the alarm on holidays." "You can specify your holiday region in the Configuration dialog.")); connect(mExcludeHolidays, &CheckBox::toggled, this, &RecurrenceEdit::contentsChanged); vlayout->addWidget(mExcludeHolidays); mWorkTimeOnly = new CheckBox(i18nc("@option:check", "Only during working time"), mExceptionGroup); mWorkTimeOnly->setReadOnly(mReadOnly); mWorkTimeOnly->setWhatsThis(xi18nc("@info:whatsthis", "Only execute the alarm during working hours, on working days." "You can specify working days and hours in the Configuration dialog.")); connect(mWorkTimeOnly, &CheckBox::toggled, this, &RecurrenceEdit::contentsChanged); vlayout->addWidget(mWorkTimeOnly); topLayout->addStretch(); mNoEmitTypeChanged = false; } /****************************************************************************** * Show or hide the exception controls. */ void RecurrenceEdit::showMoreOptions(bool more) { if (more) mExceptionGroup->show(); else mExceptionGroup->hide(); updateGeometry(); } /****************************************************************************** * Verify the consistency of the entered data. * Reply = widget to receive focus on error, or 0 if no error. */ QWidget* RecurrenceEdit::checkData(const KADateTime& startDateTime, QString& errorMessage) const { if (mAtLoginButton->isChecked()) return nullptr; const_cast(this)->mCurrStartDateTime = startDateTime; if (mEndDateButton->isChecked()) { // N.B. End date/time takes the same time spec as start date/time QWidget* errWidget = nullptr; bool noTime = !mEndTimeEdit->isEnabled(); QDate endDate = mEndDateEdit->date(); if (endDate < startDateTime.date()) errWidget = mEndDateEdit; else if (!noTime && KADateTime(endDate, mEndTimeEdit->time(), startDateTime.timeSpec()) < startDateTime) errWidget = mEndTimeEdit; if (errWidget) { errorMessage = noTime ? i18nc("@info", "End date is earlier than start date") : i18nc("@info", "End date/time is earlier than start date/time"); return errWidget; } } if (!mRule) return nullptr; return mRule->validate(errorMessage); } /****************************************************************************** * Called when a recurrence period radio button is clicked. */ void RecurrenceEdit::periodClicked(QAbstractButton* button) { - RepeatType oldType = mRuleButtonType; - bool none = (button == mNoneButton); - bool atLogin = (button == mAtLoginButton); - bool subdaily = (button == mSubDailyButton); + const RepeatType oldType = mRuleButtonType; + const bool none = (button == mNoneButton); + const bool atLogin = (button == mAtLoginButton); + const bool subdaily = (button == mSubDailyButton); if (none) { mRule = nullptr; mRuleButtonType = NO_RECUR; } else if (atLogin) { mRule = nullptr; mRuleButtonType = AT_LOGIN; mEndDateButton->setChecked(true); } else if (subdaily) { mRule = mSubDailyRule; mRuleButtonType = SUBDAILY; } else if (button == mDailyButton) { mRule = mDailyRule; mRuleButtonType = DAILY; mDailyShown = true; } else if (button == mWeeklyButton) { mRule = mWeeklyRule; mRuleButtonType = WEEKLY; mWeeklyShown = true; } else if (button == mMonthlyButton) { mRule = mMonthlyRule; mRuleButtonType = MONTHLY; mMonthlyShown = true; } else if (button == mYearlyButton) { mRule = mYearlyRule; mRuleButtonType = ANNUAL; mYearlyShown = true; } else return; if (mRuleButtonType != oldType) { mRuleStack->setCurrentWidget(mRule ? mRule : mNoRule); if (oldType == NO_RECUR || none) mRangeButtonBox->setEnabled(!none); mExceptionGroup->setEnabled(!(none || atLogin)); mEndAnyTimeCheckBox->setEnabled(atLogin); if (!none) { mNoEndDateButton->setEnabled(!atLogin); mRepeatCountButton->setEnabled(!atLogin); } rangeTypeClicked(); mSubRepetition->setEnabled(!(none || atLogin)); if (!mNoEmitTypeChanged) Q_EMIT typeChanged(mRuleButtonType); } } void RecurrenceEdit::slotAnyTimeToggled(bool on) { QAbstractButton* button = mRuleButtonGroup->checkedButton(); mEndTimeEdit->setEnabled((button == mAtLoginButton && !on) || (button == mSubDailyButton && mEndDateButton->isChecked())); } /****************************************************************************** * Called when a recurrence range type radio button is clicked. */ void RecurrenceEdit::rangeTypeClicked() { - bool endDate = mEndDateButton->isChecked(); + const bool endDate = mEndDateButton->isChecked(); mEndDateEdit->setEnabled(endDate); mEndTimeEdit->setEnabled(endDate && ((mAtLoginButton->isChecked() && !mEndAnyTimeCheckBox->isChecked()) || mSubDailyButton->isChecked())); - bool repeatCount = mRepeatCountButton->isChecked(); + const bool repeatCount = mRepeatCountButton->isChecked(); mRepeatCountEntry->setEnabled(repeatCount); mRepeatCountLabel->setEnabled(repeatCount); } void RecurrenceEdit::showEvent(QShowEvent*) { if (mRule) mRule->setFrequencyFocus(); else mRuleButtonGroup->checkedButton()->setFocus(); Q_EMIT shown(); } /****************************************************************************** * Return the sub-repetition interval and count within the recurrence, i.e. the * number of repetitions after the main recurrence. */ Repetition RecurrenceEdit::subRepetition() const { return (mRuleButtonType >= SUBDAILY) ? mSubRepetition->repetition() : Repetition(); } /****************************************************************************** * Called when the Sub-Repetition button has been pressed to display the * sub-repetition dialog. * Alarm repetition has the following restrictions: * 1) Not allowed for a repeat-at-login alarm * 2) For a date-only alarm, the repeat interval must be a whole number of days. * 3) The overall repeat duration must be less than the recurrence interval. */ void RecurrenceEdit::setSubRepetition(int reminderMinutes, bool dateOnly) { int maxDuration; switch (mRuleButtonType) { case RecurrenceEdit::NO_RECUR: case RecurrenceEdit::AT_LOGIN: // alarm repeat not allowed maxDuration = 0; break; default: // repeat duration must be less than recurrence interval { KAEvent event; updateEvent(event, false); maxDuration = event.longestRecurrenceInterval().asSeconds()/60 - reminderMinutes - 1; break; } } mSubRepetition->initialise(mSubRepetition->repetition(), dateOnly, maxDuration); mSubRepetition->setEnabled(mRuleButtonType >= SUBDAILY && maxDuration); } /****************************************************************************** * Activate the sub-repetition dialog. */ void RecurrenceEdit::activateSubRepetition() { mSubRepetition->activate(); } /****************************************************************************** * Called when the value of the repeat count field changes, to reset the * minimum value to 1 if the value was 0. */ void RecurrenceEdit::repeatCountChanged(int value) { if (value > 0 && mRepeatCountEntry->minimum() == 0) mRepeatCountEntry->setMinimum(1); } /****************************************************************************** * Add the date entered in the exception date edit control to the list of * exception dates. */ void RecurrenceEdit::addException() { if (!mExceptionDateEdit || !mExceptionDateEdit->date().isValid()) return; - QDate date = mExceptionDateEdit->date(); + const QDate date = mExceptionDateEdit->date(); DateList::Iterator it; int index = 0; bool insert = true; for (it = mExceptionDates.begin(); it != mExceptionDates.end(); ++index, ++it) { if (date <= *it) { insert = (date != *it); break; } } if (insert) { mExceptionDates.insert(it, date); mExceptionDateList->insertItem(index, new QListWidgetItem(QLocale().toString(date, QLocale::LongFormat))); Q_EMIT contentsChanged(); } mExceptionDateList->setCurrentItem(mExceptionDateList->item(index)); enableExceptionButtons(); } /****************************************************************************** * Change the currently highlighted exception date to that entered in the * exception date edit control. */ void RecurrenceEdit::changeException() { if (!mExceptionDateEdit || !mExceptionDateEdit->date().isValid()) return; QListWidgetItem* item = mExceptionDateList->currentItem(); if (item && item->isSelected()) { - int index = mExceptionDateList->row(item); - QDate olddate = mExceptionDates[index]; - QDate newdate = mExceptionDateEdit->date(); + const int index = mExceptionDateList->row(item); + const QDate olddate = mExceptionDates.at(index); + const QDate newdate = mExceptionDateEdit->date(); if (newdate != olddate) { mExceptionDates.removeAt(index); mExceptionDateList->takeItem(index); Q_EMIT contentsChanged(); addException(); } } } /****************************************************************************** * Delete the currently highlighted exception date. */ void RecurrenceEdit::deleteException() { QListWidgetItem* item = mExceptionDateList->currentItem(); if (item && item->isSelected()) { - int index = mExceptionDateList->row(item); + const int index = mExceptionDateList->row(item); mExceptionDates.removeAt(index); mExceptionDateList->takeItem(index); Q_EMIT contentsChanged(); enableExceptionButtons(); } } /****************************************************************************** * Enable/disable the exception group buttons according to whether any item is * selected in the exceptions listbox. */ void RecurrenceEdit::enableExceptionButtons() { QListWidgetItem* item = mExceptionDateList->currentItem(); - bool enable = item; + const bool enable = item; if (mDeleteExceptionButton) mDeleteExceptionButton->setEnabled(enable); if (mChangeExceptionButton) mChangeExceptionButton->setEnabled(enable); // Prevent the exceptions list box receiving keyboard focus is it's empty mExceptionDateList->setFocusPolicy(mExceptionDateList->count() ? Qt::WheelFocus : Qt::NoFocus); } /****************************************************************************** * Notify this instance of a change in the alarm start date. */ void RecurrenceEdit::setStartDate(const QDate& start, const QDate& today) { if (!mReadOnly) { setRuleDefaults(start); if (start < today) { mEndDateEdit->setMinimumDate(today); if (mExceptionDateEdit) mExceptionDateEdit->setMinimumDate(today); } else { const QString startString = i18nc("@info", "Date cannot be earlier than start date"); mEndDateEdit->setMinimumDate(start, startString); if (mExceptionDateEdit) mExceptionDateEdit->setMinimumDate(start, startString); } } } /****************************************************************************** * Specify the default recurrence end date. */ void RecurrenceEdit::setDefaultEndDate(const QDate& end) { if (!mEndDateButton->isChecked()) mEndDateEdit->setDate(end); } void RecurrenceEdit::setEndDateTime(const KADateTime& end) { const KADateTime edt = end.toTimeSpec(mCurrStartDateTime.timeSpec()); mEndDateEdit->setDate(edt.date()); mEndTimeEdit->setValue(edt.time()); mEndTimeEdit->setEnabled(!end.isDateOnly()); mEndAnyTimeCheckBox->setChecked(end.isDateOnly()); } KADateTime RecurrenceEdit::endDateTime() const { if (mRuleButtonGroup->checkedButton() == mAtLoginButton && mEndAnyTimeCheckBox->isChecked()) return KADateTime(mEndDateEdit->date(), mCurrStartDateTime.timeSpec()); return KADateTime(mEndDateEdit->date(), mEndTimeEdit->time(), mCurrStartDateTime.timeSpec()); } /****************************************************************************** * Set all controls to their default values. */ void RecurrenceEdit::setDefaults(const KADateTime& from) { mCurrStartDateTime = from; - QDate fromDate = from.date(); + const QDate fromDate = from.date(); mNoEndDateButton->setChecked(true); mSubDailyRule->setFrequency(1); mDailyRule->setFrequency(1); mWeeklyRule->setFrequency(1); mMonthlyRule->setFrequency(1); mYearlyRule->setFrequency(1); setRuleDefaults(fromDate); mMonthlyRule->setType(MonthYearRule::DATE); // date in month mYearlyRule->setType(MonthYearRule::DATE); // date in year mEndDateEdit->setDate(fromDate); mNoEmitTypeChanged = true; RadioButton* button; switch (Preferences::defaultRecurPeriod()) { case Preferences::Recur_Login: button = mAtLoginButton; break; case Preferences::Recur_Yearly: button = mYearlyButton; break; case Preferences::Recur_Monthly: button = mMonthlyButton; break; case Preferences::Recur_Weekly: button = mWeeklyButton; break; case Preferences::Recur_Daily: button = mDailyButton; break; case Preferences::Recur_SubDaily: button = mSubDailyButton; break; case Preferences::Recur_None: default: button = mNoneButton; break; } button->setChecked(true); mNoEmitTypeChanged = false; rangeTypeClicked(); enableExceptionButtons(); saveState(); } /****************************************************************************** * Set the controls for weekly, monthly and yearly rules which have not so far * been shown, to their default values, depending on the recurrence start date. */ void RecurrenceEdit::setRuleDefaults(const QDate& fromDate) { - int day = fromDate.day(); - int dayOfWeek = fromDate.dayOfWeek(); - int month = fromDate.month(); + const int day = fromDate.day(); + const int dayOfWeek = fromDate.dayOfWeek(); + const int month = fromDate.month(); if (!mDailyShown) mDailyRule->setDays(true); if (!mWeeklyShown) mWeeklyRule->setDay(dayOfWeek); if (!mMonthlyShown) mMonthlyRule->setDefaultValues(day, dayOfWeek); if (!mYearlyShown) mYearlyRule->setDefaultValues(day, dayOfWeek, month); } /****************************************************************************** * Initialise the recurrence to select repeat-at-login. * This function and set() are mutually exclusive: call one or the other, not both. */ void RecurrenceEdit::setRepeatAtLogin() { mAtLoginButton->setChecked(true); mEndDateButton->setChecked(true); } /****************************************************************************** * Set the state of all controls to reflect the data in the specified event. */ void RecurrenceEdit::set(const KAEvent& event) { setDefaults(event.mainDateTime().kDateTime()); if (event.repeatAtLogin()) { mAtLoginButton->setChecked(true); mEndDateButton->setChecked(true); return; } mNoneButton->setChecked(true); KARecurrence* recurrence = event.recurrence(); if (!recurrence) return; - KARecurrence::Type rtype = recurrence->type(); + const KARecurrence::Type rtype = recurrence->type(); switch (rtype) { case KARecurrence::MINUTELY: mSubDailyButton->setChecked(true); break; case KARecurrence::DAILY: { mDailyButton->setChecked(true); - QBitArray rDays = recurrence->days(); - bool set = false; - for (int i = 0; i < 7 && !set; ++i) - set = rDays.testBit(i); - if (set) + const QBitArray rDays = recurrence->days(); + if (rDays.count(true)) mDailyRule->setDays(rDays); else mDailyRule->setDays(true); break; } case KARecurrence::WEEKLY: { mWeeklyButton->setChecked(true); - QBitArray rDays = recurrence->days(); + const QBitArray rDays = recurrence->days(); mWeeklyRule->setDays(rDays); break; } case KARecurrence::MONTHLY_POS: // on nth (Tuesday) of the month { - QList posns = recurrence->monthPositions(); + const QList posns = recurrence->monthPositions(); int i = posns.first().pos(); if (!i) { // It's every (Tuesday) of the month. Convert to a weekly recurrence // (but ignoring any non-every xxxDay positions). mWeeklyButton->setChecked(true); mWeeklyRule->setFrequency(recurrence->frequency()); QBitArray rDays(7); - for (int i = 0, end = posns.count(); i < end; ++i) + for (const RecurrenceRule::WDayPos& posn : posns) { - if (!posns[i].pos()) - rDays.setBit(posns[i].day() - 1, 1); + if (!posn.pos()) + rDays.setBit(posn.day() - 1, 1); } mWeeklyRule->setDays(rDays); break; } mMonthlyButton->setChecked(true); mMonthlyRule->setPosition(i, posns.first().day()); break; } case KARecurrence::MONTHLY_DAY: // on nth day of the month { mMonthlyButton->setChecked(true); - QList rmd = recurrence->monthDays(); - int day = (rmd.isEmpty()) ? event.mainDateTime().date().day() : rmd.first(); + const QList rmd = recurrence->monthDays(); + const int day = (rmd.isEmpty()) ? event.mainDateTime().date().day() : rmd.first(); mMonthlyRule->setDate(day); break; } case KARecurrence::ANNUAL_DATE: // on the nth day of (months...) in the year case KARecurrence::ANNUAL_POS: // on the nth (Tuesday) of (months...) in the year { if (rtype == KARecurrence::ANNUAL_DATE) { mYearlyButton->setChecked(true); const QList rmd = recurrence->monthDays(); - int day = (rmd.isEmpty()) ? event.mainDateTime().date().day() : rmd.first(); + const int day = (rmd.isEmpty()) ? event.mainDateTime().date().day() : rmd.first(); mYearlyRule->setDate(day); mYearlyRule->setFeb29Type(recurrence->feb29Type()); } else if (rtype == KARecurrence::ANNUAL_POS) { mYearlyButton->setChecked(true); - QList posns = recurrence->yearPositions(); + const QList posns = recurrence->yearPositions(); mYearlyRule->setPosition(posns.first().pos(), posns.first().day()); } mYearlyRule->setMonths(recurrence->yearMonths()); break; } default: return; } mRule->setFrequency(recurrence->frequency()); // Get range information KADateTime endtime = mCurrStartDateTime; - int duration = recurrence->duration(); + const int duration = recurrence->duration(); if (duration == -1) mNoEndDateButton->setChecked(true); else if (duration) { mRepeatCountButton->setChecked(true); mRepeatCountEntry->setValue(duration); } else { mEndDateButton->setChecked(true); endtime = recurrence->endDateTime(); mEndTimeEdit->setValue(endtime.time()); } mEndDateEdit->setDate(endtime.date()); // Get exception information mExceptionDates = event.recurrence()->exDates(); std::sort(mExceptionDates.begin(), mExceptionDates.end()); mExceptionDateList->clear(); - for (int i = 0, iend = mExceptionDates.count(); i < iend; ++i) - new QListWidgetItem(QLocale().toString(mExceptionDates[i], QLocale::LongFormat), mExceptionDateList); + for (const QDate& exceptionDate : mExceptionDates) + new QListWidgetItem(QLocale().toString(exceptionDate, QLocale::LongFormat), mExceptionDateList); enableExceptionButtons(); mExcludeHolidays->setChecked(event.holidaysExcluded()); mWorkTimeOnly->setChecked(event.workTimeOnly()); // Get repetition within recurrence mSubRepetition->set(event.repetition()); rangeTypeClicked(); saveState(); } /****************************************************************************** * Update the specified KAEvent with the entered recurrence data. * If 'adjustStart' is true, the start date/time will be adjusted if necessary * to be the first date/time which recurs on or after the original start. */ void RecurrenceEdit::updateEvent(KAEvent& event, bool adjustStart) { // Get end date and repeat count, common to all types of recurring events QDate endDate; QTime endTime; int repeatCount; if (mNoEndDateButton->isChecked()) repeatCount = -1; else if (mRepeatCountButton->isChecked()) repeatCount = mRepeatCountEntry->value(); else { repeatCount = 0; endDate = mEndDateEdit->date(); endTime = mEndTimeEdit->time(); } // Set up the recurrence according to the type selected event.startChanges(); QAbstractButton* button = mRuleButtonGroup->checkedButton(); event.setRepeatAtLogin(button == mAtLoginButton); - int frequency = mRule ? mRule->frequency() : 0; + const int frequency = mRule ? mRule->frequency() : 0; if (button == mSubDailyButton) { const KADateTime endDateTime(endDate, endTime, mCurrStartDateTime.timeSpec()); event.setRecurMinutely(frequency, repeatCount, endDateTime); } else if (button == mDailyButton) { event.setRecurDaily(frequency, mDailyRule->days(), repeatCount, endDate); } else if (button == mWeeklyButton) { event.setRecurWeekly(frequency, mWeeklyRule->days(), repeatCount, endDate); } else if (button == mMonthlyButton) { if (mMonthlyRule->type() == MonthlyRule::POS) { // It's by position KAEvent::MonthPos pos; pos.days.fill(false); pos.days.setBit(mMonthlyRule->dayOfWeek() - 1); pos.weeknum = mMonthlyRule->week(); QVector poses(1, pos); event.setRecurMonthlyByPos(frequency, poses, repeatCount, endDate); } else { // It's by day - int daynum = mMonthlyRule->date(); + const int daynum = mMonthlyRule->date(); QVector daynums(1, daynum); event.setRecurMonthlyByDate(frequency, daynums, repeatCount, endDate); } } else if (button == mYearlyButton) { - QVector months = mYearlyRule->months(); + const QVector months = mYearlyRule->months(); if (mYearlyRule->type() == YearlyRule::POS) { // It's by position KAEvent::MonthPos pos; pos.days.fill(false); pos.days.setBit(mYearlyRule->dayOfWeek() - 1); pos.weeknum = mYearlyRule->week(); QVector poses(1, pos); event.setRecurAnnualByPos(frequency, poses, months, repeatCount, endDate); } else { // It's by date in month event.setRecurAnnualByDate(frequency, months, mYearlyRule->date(), mYearlyRule->feb29Type(), repeatCount, endDate); } } else { event.setNoRecur(); event.endChanges(); return; } if (!event.recurs()) { event.endChanges(); return; // an error occurred setting up the recurrence } if (adjustStart) event.setFirstRecurrence(); // Set up repetition within the recurrence // N.B. This requires the main recurrence to be set up first. event.setRepetition((mRuleButtonType < SUBDAILY) ? Repetition() : mSubRepetition->repetition()); // Set up exceptions event.recurrence()->setExDates(mExceptionDates); event.setWorkTimeOnly(mWorkTimeOnly->isChecked()); event.setExcludeHolidays(mExcludeHolidays->isChecked()); event.endChanges(); } /****************************************************************************** * Save the state of all controls. */ void RecurrenceEdit::saveState() { mSavedRuleButton = mRuleButtonGroup->checkedButton(); if (mRule) mRule->saveState(); mSavedRangeButton = mRangeButtonGroup->checkedButton(); if (mSavedRangeButton == mRepeatCountButton) mSavedRecurCount = mRepeatCountEntry->value(); else if (mSavedRangeButton == mEndDateButton) { mSavedEndDateTime = KADateTime(mEndDateEdit->date(), mEndTimeEdit->time(), mCurrStartDateTime.timeSpec()); mSavedEndDateTime.setDateOnly(mEndAnyTimeCheckBox->isChecked()); } mSavedExceptionDates = mExceptionDates; mSavedWorkTimeOnly = mWorkTimeOnly->isChecked(); mSavedExclHolidays = mExcludeHolidays->isChecked(); mSavedRepetition = mSubRepetition->repetition(); } /****************************************************************************** * Check whether any of the controls have changed state since initialisation. */ bool RecurrenceEdit::stateChanged() const { if (mSavedRuleButton != mRuleButtonGroup->checkedButton() || mSavedRangeButton != mRangeButtonGroup->checkedButton() || (mRule && mRule->stateChanged())) return true; if (mSavedRangeButton == mRepeatCountButton && mSavedRecurCount != mRepeatCountEntry->value()) return true; if (mSavedRangeButton == mEndDateButton) { KADateTime edt(mEndDateEdit->date(), mEndTimeEdit->time(), mCurrStartDateTime.timeSpec()); edt.setDateOnly(mEndAnyTimeCheckBox->isChecked()); if (mSavedEndDateTime != edt) return true; } if (mSavedExceptionDates != mExceptionDates || mSavedWorkTimeOnly != mWorkTimeOnly->isChecked() || mSavedExclHolidays != mExcludeHolidays->isChecked() || mSavedRepetition != mSubRepetition->repetition()) return true; return false; } /*============================================================================= = Class Rule = Base class for rule widgets, including recurrence frequency. =============================================================================*/ Rule::Rule(const QString& freqText, const QString& freqWhatsThis, bool time, bool readOnly, QWidget* parent) : NoRule(parent) { mLayout = new QVBoxLayout(this); mLayout->setContentsMargins(0, 0, 0, 0); mLayout->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); QHBoxLayout* freqLayout = new QHBoxLayout(); freqLayout->setContentsMargins(0, 0, 0, 0); freqLayout->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); mLayout->addLayout(freqLayout); QWidget* box = new QWidget(this); // this is to control the QWhatsThis text display area freqLayout->addWidget(box, 0, Qt::AlignLeft); QHBoxLayout* boxLayout = new QHBoxLayout(box); boxLayout->setContentsMargins(0, 0, 0, 0); QLabel* label = new QLabel(i18nc("@label:spinbox", "Recur e&very"), box); label->setFixedSize(label->sizeHint()); boxLayout->addWidget(label, 0, Qt::AlignLeft); if (time) { mIntSpinBox = nullptr; mSpinBox = mTimeSpinBox = new TimeSpinBox(1, 5999, box); mTimeSpinBox->setFixedSize(mTimeSpinBox->sizeHint()); mTimeSpinBox->setReadOnly(readOnly); boxLayout->addWidget(mSpinBox, 0, Qt::AlignLeft); } else { mTimeSpinBox = nullptr; mSpinBox = mIntSpinBox = new SpinBox(1, 999, box); mIntSpinBox->setFixedSize(mIntSpinBox->sizeHint()); mIntSpinBox->setReadOnly(readOnly); boxLayout->addWidget(mSpinBox, 0, Qt::AlignLeft); } connect(mSpinBox, SIGNAL(valueChanged(int)), SIGNAL(frequencyChanged())); connect(mSpinBox, SIGNAL(valueChanged(int)), SIGNAL(changed())); label->setBuddy(mSpinBox); label = new QLabel(freqText, box); label->setFixedSize(label->sizeHint()); boxLayout->addWidget(label, 0, Qt::AlignLeft); box->setFixedSize(sizeHint()); box->setWhatsThis(freqWhatsThis); } int Rule::frequency() const { if (mIntSpinBox) return mIntSpinBox->value(); if (mTimeSpinBox) return mTimeSpinBox->value(); return 0; } void Rule::setFrequency(int n) { if (mIntSpinBox) mIntSpinBox->setValue(n); if (mTimeSpinBox) mTimeSpinBox->setValue(n); } /****************************************************************************** * Save the state of all controls. */ void Rule::saveState() { mSavedFrequency = frequency(); } /****************************************************************************** * Check whether any of the controls have changed state since initialisation. */ bool Rule::stateChanged() const { return (mSavedFrequency != frequency()); } /*============================================================================= = Class SubDailyRule = Sub-daily rule widget. =============================================================================*/ SubDailyRule::SubDailyRule(bool readOnly, QWidget* parent) : Rule(i18nc("@label Time units for user-entered numbers", "hours:minutes"), i18nc("@info:whatsthis", "Enter the number of hours and minutes between repetitions of the alarm"), true, readOnly, parent) { } /*============================================================================= = Class DayWeekRule = Daily/weekly rule widget base class. =============================================================================*/ DayWeekRule::DayWeekRule(const QString& freqText, const QString& freqWhatsThis, const QString& daysWhatsThis, bool readOnly, QWidget* parent) - : Rule(freqText, freqWhatsThis, false, readOnly, parent), - mSavedDays(7) + : Rule(freqText, freqWhatsThis, false, readOnly, parent) + , mSavedDays(7) { QGridLayout* grid = new QGridLayout(); grid->setContentsMargins(0, 0, 0, 0); grid->setRowStretch(0, 1); layout()->addLayout(grid); QLabel* label = new QLabel(i18nc("@label On: Tuesday", "O&n:"), this); label->setFixedSize(label->sizeHint()); grid->addWidget(label, 0, 0, Qt::AlignRight | Qt::AlignTop); grid->setColumnMinimumWidth(1, style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); // List the days of the week starting at the user's start day of the week. // Save the first day of the week, just in case it changes while the dialog is open. QWidget* box = new QWidget(this); // this is to control the QWhatsThis text display area QGridLayout* dgrid = new QGridLayout(box); dgrid->setContentsMargins(0, 0, 0, 0); dgrid->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); QLocale locale; for (int i = 0; i < 7; ++i) { - int day = KAlarm::localeDayInWeek_to_weekDay(i); + const int day = KAlarm::localeDayInWeek_to_weekDay(i); mDayBox[i] = new CheckBox(locale.dayName(day), box); mDayBox[i]->setFixedSize(mDayBox[i]->sizeHint()); mDayBox[i]->setReadOnly(readOnly); connect(mDayBox[i], &QAbstractButton::toggled, this, &Rule::changed); dgrid->addWidget(mDayBox[i], i%4, i/4, Qt::AlignLeft); } box->setFixedSize(box->sizeHint()); box->setWhatsThis(daysWhatsThis); grid->addWidget(box, 0, 2, Qt::AlignLeft); label->setBuddy(mDayBox[0]); grid->setColumnStretch(3, 1); } /****************************************************************************** * Fetch which days of the week have been ticked. */ QBitArray DayWeekRule::days() const { QBitArray ds(7); ds.fill(false); for (int i = 0; i < 7; ++i) if (mDayBox[i]->isChecked()) ds.setBit(KAlarm::localeDayInWeek_to_weekDay(i) - 1, 1); return ds; } /****************************************************************************** * Tick/untick every day of the week. */ void DayWeekRule::setDays(bool tick) { for (int i = 0; i < 7; ++i) mDayBox[i]->setChecked(tick); } /****************************************************************************** * Tick/untick each day of the week according to the specified bits. */ void DayWeekRule::setDays(const QBitArray& days) { + if (days.size() != 7) + { + qCWarning(KALARM_LOG) << "DayWeekRule::setDays: Error! 'days' parameter must have 7 elements: actual size" << days.size(); + return; + } for (int i = 0; i < 7; ++i) { bool x = days.testBit(KAlarm::localeDayInWeek_to_weekDay(i) - 1); mDayBox[i]->setChecked(x); } } /****************************************************************************** * Tick the specified day of the week, and untick all other days. */ void DayWeekRule::setDay(int dayOfWeek) { for (int i = 0; i < 7; ++i) mDayBox[i]->setChecked(false); if (dayOfWeek > 0 && dayOfWeek <= 7) mDayBox[KAlarm::weekDay_to_localeDayInWeek(dayOfWeek)]->setChecked(true); } /****************************************************************************** * Validate: check that at least one day is selected. */ QWidget* DayWeekRule::validate(QString& errorMessage) { for (int i = 0; i < 7; ++i) if (mDayBox[i]->isChecked()) return nullptr; errorMessage = i18nc("@info", "No day selected"); return mDayBox[0]; } /****************************************************************************** * Save the state of all controls. */ void DayWeekRule::saveState() { Rule::saveState(); mSavedDays = days(); } /****************************************************************************** * Check whether any of the controls have changed state since initialisation. */ bool DayWeekRule::stateChanged() const { return (Rule::stateChanged() || mSavedDays != days()); } /*============================================================================= = Class DailyRule = Daily rule widget. =============================================================================*/ DailyRule::DailyRule(bool readOnly, QWidget* parent) : DayWeekRule(i18nc("@label Time unit for user-entered number", "day(s)"), i18nc("@info:whatsthis", "Enter the number of days between repetitions of the alarm"), i18nc("@info:whatsthis", "Select the days of the week on which the alarm is allowed to occur"), readOnly, parent) { } /*============================================================================= = Class WeeklyRule = Weekly rule widget. =============================================================================*/ WeeklyRule::WeeklyRule(bool readOnly, QWidget* parent) : DayWeekRule(i18nc("@label Time unit for user-entered number", "week(s)"), i18nc("@info:whatsthis", "Enter the number of weeks between repetitions of the alarm"), i18nc("@info:whatsthis", "Select the days of the week on which to repeat the alarm"), readOnly, parent) { } /*============================================================================= = Class MonthYearRule = Monthly/yearly rule widget base class. =============================================================================*/ MonthYearRule::MonthYearRule(const QString& freqText, const QString& freqWhatsThis, bool allowEveryWeek, bool readOnly, QWidget* parent) - : Rule(freqText, freqWhatsThis, false, readOnly, parent), - mEveryWeek(allowEveryWeek) + : Rule(freqText, freqWhatsThis, false, readOnly, parent) + , mEveryWeek(allowEveryWeek) { mButtonGroup = new ButtonGroup(this); // Month day selector QGridLayout* boxLayout = new QGridLayout(); boxLayout->setContentsMargins(0, 0, 0, 0); boxLayout->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); layout()->addLayout(boxLayout); mDayButton = new RadioButton(i18nc("@option:radio On day number in the month", "O&n day"), this); mDayButton->setFixedSize(mDayButton->sizeHint()); mDayButton->setReadOnly(readOnly); mButtonGroup->addButton(mDayButton); mDayButton->setWhatsThis(i18nc("@info:whatsthis", "Repeat the alarm on the selected day of the month")); boxLayout->addWidget(mDayButton, 0, 0); mDayCombo = new ComboBox(this); mDayCombo->setEditable(false); mDayCombo->setMaxVisibleItems(11); for (int i = 0; i < 31; ++i) mDayCombo->addItem(QString::number(i + 1)); mDayCombo->addItem(i18nc("@item:inlistbox Last day of month", "Last")); mDayCombo->setFixedSize(mDayCombo->sizeHint()); mDayCombo->setReadOnly(readOnly); mDayCombo->setWhatsThis(i18nc("@info:whatsthis", "Select the day of the month on which to repeat the alarm")); mDayButton->setFocusWidget(mDayCombo); connect(mDayCombo, static_cast(&ComboBox::activated), this, &MonthYearRule::slotDaySelected); connect(mDayCombo, static_cast(&ComboBox::currentIndexChanged), this, &MonthYearRule::changed); boxLayout->addWidget(mDayCombo, 0, 1, 1, 2, Qt::AlignLeft); // Month position selector mPosButton = new RadioButton(i18nc("@option:radio On the 1st Tuesday", "On t&he"), this); mPosButton->setFixedSize(mPosButton->sizeHint()); mPosButton->setReadOnly(readOnly); mButtonGroup->addButton(mPosButton); mPosButton->setWhatsThis(i18nc("@info:whatsthis", "Repeat the alarm on one day of the week, in the selected week of the month")); boxLayout->addWidget(mPosButton, 1, 0); mWeekCombo = new ComboBox(this); mWeekCombo->setEditable(false); mWeekCombo->addItem(i18nc("@item:inlistbox", "1st")); mWeekCombo->addItem(i18nc("@item:inlistbox", "2nd")); mWeekCombo->addItem(i18nc("@item:inlistbox", "3rd")); mWeekCombo->addItem(i18nc("@item:inlistbox", "4th")); mWeekCombo->addItem(i18nc("@item:inlistbox", "5th")); mWeekCombo->addItem(i18nc("@item:inlistbox Last Monday in March", "Last")); mWeekCombo->addItem(i18nc("@item:inlistbox", "2nd Last")); mWeekCombo->addItem(i18nc("@item:inlistbox", "3rd Last")); mWeekCombo->addItem(i18nc("@item:inlistbox", "4th Last")); mWeekCombo->addItem(i18nc("@item:inlistbox", "5th Last")); if (mEveryWeek) { mWeekCombo->addItem(i18nc("@item:inlistbox Every (Monday...) in month", "Every")); mWeekCombo->setMaxVisibleItems(11); } mWeekCombo->setWhatsThis(i18nc("@info:whatsthis", "Select the week of the month in which to repeat the alarm")); mWeekCombo->setFixedSize(mWeekCombo->sizeHint()); mWeekCombo->setReadOnly(readOnly); mPosButton->setFocusWidget(mWeekCombo); connect(mWeekCombo, static_cast(&ComboBox::currentIndexChanged), this, &MonthYearRule::changed); boxLayout->addWidget(mWeekCombo, 1, 1); mDayOfWeekCombo = new ComboBox(this); mDayOfWeekCombo->setEditable(false); QLocale locale; for (int i = 0; i < 7; ++i) { int day = KAlarm::localeDayInWeek_to_weekDay(i); mDayOfWeekCombo->addItem(locale.dayName(day)); } mDayOfWeekCombo->setReadOnly(readOnly); mDayOfWeekCombo->setWhatsThis(i18nc("@info:whatsthis", "Select the day of the week on which to repeat the alarm")); connect(mDayOfWeekCombo, static_cast(&ComboBox::currentIndexChanged), this, &MonthYearRule::changed); boxLayout->addWidget(mDayOfWeekCombo, 1, 2, Qt::AlignLeft); connect(mButtonGroup, &ButtonGroup::buttonSet, this, &MonthYearRule::clicked); connect(mButtonGroup, &ButtonGroup::buttonSet, this, &MonthYearRule::changed); } MonthYearRule::DayPosType MonthYearRule::type() const { return (mButtonGroup->checkedButton() == mDayButton) ? DATE : POS; } void MonthYearRule::setType(MonthYearRule::DayPosType type) { if (type == DATE) mDayButton->setChecked(true); else mPosButton->setChecked(true); } void MonthYearRule::setDefaultValues(int dayOfMonth, int dayOfWeek) { --dayOfMonth; mDayCombo->setCurrentIndex(dayOfMonth); mWeekCombo->setCurrentIndex(dayOfMonth / 7); mDayOfWeekCombo->setCurrentIndex(KAlarm::weekDay_to_localeDayInWeek(dayOfWeek)); } int MonthYearRule::date() const { - int daynum = mDayCombo->currentIndex() + 1; + const int daynum = mDayCombo->currentIndex() + 1; return (daynum <= 31) ? daynum : 31 - daynum; } int MonthYearRule::week() const { int weeknum = mWeekCombo->currentIndex() + 1; return (weeknum <= 5) ? weeknum : (weeknum == 11) ? 0 : 5 - weeknum; } int MonthYearRule::dayOfWeek() const { return KAlarm::localeDayInWeek_to_weekDay(mDayOfWeekCombo->currentIndex()); } void MonthYearRule::setDate(int dayOfMonth) { mDayButton->setChecked(true);; mDayCombo->setCurrentIndex(dayOfMonth > 0 ? dayOfMonth - 1 : dayOfMonth < 0 ? 30 - dayOfMonth : 0); // day 0 shouldn't ever occur } void MonthYearRule::setPosition(int week, int dayOfWeek) { mPosButton->setChecked(true); mWeekCombo->setCurrentIndex((week > 0) ? week - 1 : (week < 0) ? 4 - week : mEveryWeek ? 10 : 0); mDayOfWeekCombo->setCurrentIndex(KAlarm::weekDay_to_localeDayInWeek(dayOfWeek)); } void MonthYearRule::enableSelection(DayPosType type) { - bool date = (type == DATE); + const bool date = (type == DATE); mDayCombo->setEnabled(date); mWeekCombo->setEnabled(!date); mDayOfWeekCombo->setEnabled(!date); } void MonthYearRule::clicked(QAbstractButton* button) { enableSelection(button == mDayButton ? DATE : POS); } void MonthYearRule::slotDaySelected(int index) { daySelected(index <= 30 ? index + 1 : 30 - index); } /****************************************************************************** * Save the state of all controls. */ void MonthYearRule::saveState() { Rule::saveState(); mSavedType = type(); if (mSavedType == DATE) mSavedDay = date(); else { mSavedWeek = week(); mSavedWeekDay = dayOfWeek(); } } /****************************************************************************** * Check whether any of the controls have changed state since initialisation. */ bool MonthYearRule::stateChanged() const { if (Rule::stateChanged() || mSavedType != type()) return true; if (mSavedType == DATE) { if (mSavedDay != date()) return true; } else { if (mSavedWeek != week() || mSavedWeekDay != dayOfWeek()) return true; } return false; } /*============================================================================= = Class MonthlyRule = Monthly rule widget. =============================================================================*/ MonthlyRule::MonthlyRule(bool readOnly, QWidget* parent) : MonthYearRule(i18nc("@label Time unit for user-entered number", "month(s)"), i18nc("@info:whatsthis", "Enter the number of months between repetitions of the alarm"), false, readOnly, parent) { } /*============================================================================= = Class YearlyRule = Yearly rule widget. =============================================================================*/ YearlyRule::YearlyRule(bool readOnly, QWidget* parent) : MonthYearRule(i18nc("@label Time unit for user-entered number", "year(s)"), i18nc("@info:whatsthis", "Enter the number of years between repetitions of the alarm"), true, readOnly, parent) { // Set up the month selection widgets QHBoxLayout* hlayout = new QHBoxLayout(); hlayout->setContentsMargins(0, 0, 0, 0); layout()->addLayout(hlayout); QLabel* label = new QLabel(i18nc("@label List of months to select", "Months:"), this); label->setFixedSize(label->sizeHint()); hlayout->addWidget(label, 0, Qt::AlignLeft | Qt::AlignTop); // List the months of the year. QWidget* w = new QWidget(this); // this is to control the QWhatsThis text display area hlayout->addWidget(w, 1, Qt::AlignLeft); QGridLayout* grid = new QGridLayout(w); grid->setContentsMargins(0, 0, 0, 0); grid->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); QLocale locale; for (int i = 0; i < 12; ++i) { mMonthBox[i] = new CheckBox(locale.monthName(i + 1, QLocale::ShortFormat), w); mMonthBox[i]->setFixedSize(mMonthBox[i]->sizeHint()); mMonthBox[i]->setReadOnly(readOnly); connect(mMonthBox[i], &QAbstractButton::toggled, this, &Rule::changed); grid->addWidget(mMonthBox[i], i%3, i/3, Qt::AlignLeft); } connect(mMonthBox[1], &QAbstractButton::toggled, this, &YearlyRule::enableFeb29); w->setFixedHeight(w->sizeHint().height()); w->setWhatsThis(i18nc("@info:whatsthis", "Select the months of the year in which to repeat the alarm")); // February 29th handling option QHBoxLayout* f29box = new QHBoxLayout; layout()->addLayout(f29box); w = new QWidget(this); // this is to control the QWhatsThis text display area f29box->addWidget(w, 0, Qt::AlignLeft); QHBoxLayout* boxLayout = new QHBoxLayout(w); boxLayout->setContentsMargins(0, 0, 0, 0); boxLayout->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); mFeb29Label = new QLabel(i18nc("@label:listbox", "February 2&9th alarm in non-leap years:")); mFeb29Label->setFixedSize(mFeb29Label->sizeHint()); boxLayout->addWidget(mFeb29Label); mFeb29Combo = new ComboBox(); mFeb29Combo->setEditable(false); mFeb29Combo->addItem(i18nc("@item:inlistbox No date", "None")); mFeb29Combo->addItem(i18nc("@item:inlistbox 1st March (short form)", "1 Mar")); mFeb29Combo->addItem(i18nc("@item:inlistbox 28th February (short form)", "28 Feb")); mFeb29Combo->setFixedSize(mFeb29Combo->sizeHint()); mFeb29Combo->setReadOnly(readOnly); connect(mFeb29Combo, static_cast(&ComboBox::currentIndexChanged), this, &YearlyRule::changed); mFeb29Label->setBuddy(mFeb29Combo); boxLayout->addWidget(mFeb29Combo); w->setFixedSize(w->sizeHint()); w->setWhatsThis(i18nc("@info:whatsthis", "Select which date, if any, the February 29th alarm should trigger in non-leap years")); } void YearlyRule::setDefaultValues(int dayOfMonth, int dayOfWeek, int month) { MonthYearRule::setDefaultValues(dayOfMonth, dayOfWeek); --month; for (int i = 0; i < 12; ++i) mMonthBox[i]->setChecked(i == month); setFeb29Type(KARecurrence::defaultFeb29Type()); daySelected(dayOfMonth); // enable/disable month checkboxes as appropriate } /****************************************************************************** * Fetch which months have been checked (1 - 12). * Reply = true if February has been checked. */ QVector YearlyRule::months() const { QVector mnths; for (int i = 0; i < 12; ++i) if (mMonthBox[i]->isChecked() && mMonthBox[i]->isEnabled()) mnths.append(i + 1); return mnths; } /****************************************************************************** * Check/uncheck each month of the year according to the specified list. */ void YearlyRule::setMonths(const QList& mnths) { bool checked[12]; for (int i = 0; i < 12; ++i) checked[i] = false; for (int i = 0, end = mnths.count(); i < end; ++i) checked[mnths[i] - 1] = true; for (int i = 0; i < 12; ++i) mMonthBox[i]->setChecked(checked[i]); enableFeb29(); } /****************************************************************************** * Return the date for February 29th alarms in non-leap years. */ KARecurrence::Feb29Type YearlyRule::feb29Type() const { if (mFeb29Combo->isEnabled()) { switch (mFeb29Combo->currentIndex()) { case 1: return KARecurrence::Feb29_Mar1; case 2: return KARecurrence::Feb29_Feb28; default: break; } } return KARecurrence::Feb29_None; } /****************************************************************************** * Set the date for February 29th alarms to trigger in non-leap years. */ void YearlyRule::setFeb29Type(KARecurrence::Feb29Type type) { int index; switch (type) { default: case KARecurrence::Feb29_None: index = 0; break; case KARecurrence::Feb29_Mar1: index = 1; break; case KARecurrence::Feb29_Feb28: index = 2; break; } mFeb29Combo->setCurrentIndex(index); } /****************************************************************************** * Validate: check that at least one month is selected. */ QWidget* YearlyRule::validate(QString& errorMessage) { for (int i = 0; i < 12; ++i) if (mMonthBox[i]->isChecked() && mMonthBox[i]->isEnabled()) return nullptr; errorMessage = i18nc("@info", "No month selected"); return mMonthBox[0]; } /****************************************************************************** * Called when a yearly recurrence type radio button is clicked, * to enable/disable month checkboxes as appropriate for the date selected. */ void YearlyRule::clicked(QAbstractButton* button) { MonthYearRule::clicked(button); daySelected(buttonType(button) == DATE ? date() : 1); } /****************************************************************************** * Called when a day of the month is selected in a yearly recurrence, to * disable months for which the day is out of range. */ void YearlyRule::daySelected(int day) { mMonthBox[1]->setEnabled(day <= 29); // February - bool enable = (day != 31); + const bool enable = (day != 31); mMonthBox[3]->setEnabled(enable); // April mMonthBox[5]->setEnabled(enable); // June mMonthBox[8]->setEnabled(enable); // September mMonthBox[10]->setEnabled(enable); // November enableFeb29(); } /****************************************************************************** * Enable/disable the February 29th combo box depending on whether February * 29th is selected. */ void YearlyRule::enableFeb29() { - bool enable = (type() == DATE && date() == 29 && mMonthBox[1]->isChecked() && mMonthBox[1]->isEnabled()); + const bool enable = (type() == DATE && date() == 29 && mMonthBox[1]->isChecked() && mMonthBox[1]->isEnabled()); mFeb29Label->setEnabled(enable); mFeb29Combo->setEnabled(enable); } /****************************************************************************** * Save the state of all controls. */ void YearlyRule::saveState() { MonthYearRule::saveState(); mSavedMonths = months(); mSavedFeb29Type = feb29Type(); } /****************************************************************************** * Check whether any of the controls have changed state since initialisation. */ bool YearlyRule::stateChanged() const { return (MonthYearRule::stateChanged() || mSavedMonths != months() || mSavedFeb29Type != feb29Type()); } // vim: et sw=4: diff --git a/src/undo.cpp b/src/undo.cpp index 50df637a..e6fbbdd4 100644 --- a/src/undo.cpp +++ b/src/undo.cpp @@ -1,1271 +1,1270 @@ /* * undo.cpp - undo/redo facility * Program: kalarm - * Copyright © 2005-2014 by David Jarvie + * 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" //krazy:exclude=includes (kalarm.h must be first) #include "undo.h" #include "alarmcalendar.h" #include "functions.h" #include "messagebox.h" #include #include #include #include #include "kalarm_debug.h" static int maxCount = 12; #ifdef DELETE #undef DELETE // conflicting Windows macro #endif // Simplify access to Undo::Event struct #define RESOURCE_PARAM_TYPE const Collection& #define EVENT_RESOURCE collection using namespace Akonadi; class UndoItem { public: enum Operation { ADD, EDIT, DELETE, REACTIVATE, DEACTIVATE, MULTI }; UndoItem(); // needed by QList virtual ~UndoItem(); virtual Operation operation() const = 0; virtual QString actionText() const { return !mName.isEmpty() ? mName : defaultActionText(); } virtual QString defaultActionText() const = 0; virtual QString description() const { return QString(); } virtual QString eventID() const { return QString(); } virtual QString oldEventID() const { return QString(); } virtual QString newEventID() const { return QString(); } virtual Collection collection() const { return Collection(); } int id() const { return mId; } Undo::Type type() const { return mType; } void setType(Undo::Type t) { mType = t; } CalEvent::Type calendar() const { return mCalendar; } virtual void setCalendar(CalEvent::Type s) { mCalendar = s; } virtual UndoItem* restore() = 0; virtual bool deleteID(const QString& /*id*/) { return false; } enum Error { ERR_NONE, ERR_PROG, ERR_NOT_FOUND, ERR_CREATE, ERR_TEMPLATE, ERR_ARCHIVED }; enum Warning { WARN_NONE, WARN_KORG_ADD, WARN_KORG_MODIFY, WARN_KORG_DELETE }; static int mLastId; static Error mRestoreError; // error code valid only if restore() returns 0 static Warning mRestoreWarning; // warning code set by restore() static KAlarm::UpdateResult mRestoreWarningKorg; // KOrganizer error status set by restore() static int mRestoreWarningCount; // item count for mRestoreWarning (to allow i18n messages to work correctly) protected: UndoItem(Undo::Type, const QString& name = QString()); static QString addDeleteActionText(CalEvent::Type, bool add); QString description(const KAEvent&) const; void replaceWith(UndoItem* item) { Undo::replace(this, item); } QString mName; // specified action name (overrides default) int mId; // unique identifier (only for mType = UNDO, REDO) Undo::Type mType; // which list (if any) the object is in CalEvent::Type mCalendar; }; class UndoMultiBase : public UndoItem { public: UndoMultiBase(Undo::Type t, const QString& name) : UndoItem(t, name), mUndos(new Undo::List) {} UndoMultiBase(Undo::Type t, Undo::List* undos, const QString& name) : UndoItem(t, name), mUndos(undos) {} ~UndoMultiBase() { delete mUndos; } const Undo::List* undos() const { return mUndos; } protected: Undo::List* mUndos; // this list must always have >= 2 entries }; template class UndoMulti : public UndoMultiBase { public: UndoMulti(Undo::Type, const Undo::EventList&, const QString& name); UndoMulti(Undo::Type t, Undo::List* undos, const QString& name) : UndoMultiBase(t, undos, name) {} Operation operation() const override { return MULTI; } UndoItem* restore() override; bool deleteID(const QString& id) override; virtual UndoItem* createRedo(Undo::List*) = 0; }; class UndoAdd : public UndoItem { public: UndoAdd(Undo::Type, const Undo::Event&, const QString& name = QString()); UndoAdd(Undo::Type, const KAEvent&, RESOURCE_PARAM_TYPE, const QString& name = QString()); UndoAdd(Undo::Type, const KAEvent&, RESOURCE_PARAM_TYPE, const QString& name, CalEvent::Type); Operation operation() const override { return ADD; } QString defaultActionText() const override; QString description() const override { return mDescription; } Collection collection() const override { return mResource; } QString eventID() const override { return mEventId; } QString newEventID() const override { return mEventId; } UndoItem* restore() override { return doRestore(); } protected: UndoItem* doRestore(bool setArchive = false); virtual UndoItem* createRedo(const KAEvent&, RESOURCE_PARAM_TYPE); private: Collection mResource; // collection containing the event QString mEventId; QString mDescription; }; class UndoEdit : public UndoItem { public: UndoEdit(Undo::Type, const KAEvent& oldEvent, const QString& newEventID, RESOURCE_PARAM_TYPE, const QStringList& dontShowErrors, const QString& description); ~UndoEdit(); Operation operation() const override { return EDIT; } QString defaultActionText() const override; QString description() const override { return mDescription; } Collection collection() const override { return mResource; } QString eventID() const override { return mNewEventId; } QString oldEventID() const override { return mOldEvent->id(); } QString newEventID() const override { return mNewEventId; } UndoItem* restore() override; private: Collection mResource; // collection containing the event KAEvent* mOldEvent; QString mNewEventId; QString mDescription; QStringList mDontShowErrors; }; class UndoDelete : public UndoItem { public: UndoDelete(Undo::Type, const Undo::Event&, const QString& name = QString()); UndoDelete(Undo::Type, const KAEvent&, RESOURCE_PARAM_TYPE, const QStringList& dontShowErrors, const QString& name = QString()); ~UndoDelete(); Operation operation() const override { return DELETE; } QString defaultActionText() const override; QString description() const override { return UndoItem::description(*mEvent); } Collection collection() const override { return mResource; } QString eventID() const override { return mEvent->id(); } QString oldEventID() const override { return mEvent->id(); } UndoItem* restore() override; KAEvent* event() const { return mEvent; } protected: virtual UndoItem* createRedo(const KAEvent&, RESOURCE_PARAM_TYPE); private: Collection mResource; // collection containing the event KAEvent* mEvent; QStringList mDontShowErrors; }; class UndoReactivate : public UndoAdd { public: UndoReactivate(Undo::Type t, const Undo::Event& e, const QString& name = QString()) : UndoAdd(t, e.event, e.EVENT_RESOURCE, name, CalEvent::ACTIVE) {} UndoReactivate(Undo::Type t, const KAEvent& e, RESOURCE_PARAM_TYPE r, const QString& name = QString()) : UndoAdd(t, e, r, name, CalEvent::ACTIVE) {} Operation operation() const override { return REACTIVATE; } QString defaultActionText() const override; UndoItem* restore() override; protected: UndoItem* createRedo(const KAEvent&, RESOURCE_PARAM_TYPE) override; }; class UndoDeactivate : public UndoDelete { public: UndoDeactivate(Undo::Type t, const KAEvent& e, RESOURCE_PARAM_TYPE r, const QString& name = QString()) : UndoDelete(t, e, r, QStringList(), name) {} Operation operation() const override { return DEACTIVATE; } QString defaultActionText() const override; UndoItem* restore() override; protected: UndoItem* createRedo(const KAEvent&, RESOURCE_PARAM_TYPE) override; }; class UndoAdds : public UndoMulti { public: UndoAdds(Undo::Type t, const Undo::EventList& events, const QString& name = QString()) : UndoMulti(t, events, name) {} // UNDO only UndoAdds(Undo::Type t, Undo::List* undos, const QString& name) : UndoMulti(t, undos, name) {} QString defaultActionText() const override; UndoItem* createRedo(Undo::List*) override; }; class UndoDeletes : public UndoMulti { public: UndoDeletes(Undo::Type t, const Undo::EventList& events, const QString& name = QString()) : UndoMulti(t, events, name) {} // UNDO only UndoDeletes(Undo::Type t, Undo::List* undos, const QString& name) : UndoMulti(t, undos, name) {} QString defaultActionText() const override; UndoItem* createRedo(Undo::List*) override; }; class UndoReactivates : public UndoMulti { public: UndoReactivates(Undo::Type t, const Undo::EventList& events, const QString& name = QString()) : UndoMulti(t, events, name) {} // UNDO only UndoReactivates(Undo::Type t, Undo::List* undos, const QString& name) : UndoMulti(t, undos, name) {} QString defaultActionText() const override; UndoItem* createRedo(Undo::List*) override; }; Undo* Undo::mInstance = nullptr; Undo::List Undo::mUndoList; Undo::List Undo::mRedoList; /****************************************************************************** * Create the one and only instance of the Undo class. */ Undo* Undo::instance() { if (!mInstance) mInstance = new Undo(qApp); return mInstance; } /****************************************************************************** * Clear the lists of undo and redo items. */ void Undo::clear() { if (!mUndoList.isEmpty() || !mRedoList.isEmpty()) { mInstance->blockSignals(true); while (!mUndoList.isEmpty()) delete mUndoList.first(); // N.B. 'delete' removes the object from the list while (!mRedoList.isEmpty()) delete mRedoList.first(); // N.B. 'delete' removes the object from the list mInstance->blockSignals(false); emitChanged(); } } /****************************************************************************** * Create an undo item and add it to the list of undos. * N.B. The base class constructor adds the object to the undo list. */ void Undo::saveAdd(const KAEvent& event, RESOURCE_PARAM_TYPE resource, const QString& name) { new UndoAdd(UNDO, event, resource, name); emitChanged(); } void Undo::saveAdds(const Undo::EventList& events, const QString& name) { int count = events.count(); if (count == 1) saveAdd(events.first().event, events.first().EVENT_RESOURCE, name); else if (count > 1) { new UndoAdds(UNDO, events, name); emitChanged(); } } void Undo::saveEdit(const Undo::Event& oldEvent, const KAEvent& newEvent) { new UndoEdit(UNDO, oldEvent.event, newEvent.id(), oldEvent.EVENT_RESOURCE, oldEvent.dontShowErrors, AlarmText::summary(newEvent)); removeRedos(oldEvent.event.id()); // remove any redos which are made invalid by this edit emitChanged(); } void Undo::saveDelete(const Undo::Event& event, const QString& name) { new UndoDelete(UNDO, event.event, event.EVENT_RESOURCE, event.dontShowErrors, name); removeRedos(event.event.id()); // remove any redos which are made invalid by this deletion emitChanged(); } void Undo::saveDeletes(const Undo::EventList& events, const QString& name) { int count = events.count(); if (count == 1) saveDelete(events[0], name); else if (count > 1) { new UndoDeletes(UNDO, events, name); - for (int i = 0, end = events.count(); i < end; ++i) - removeRedos(events[i].event.id()); // remove any redos which are made invalid by these deletions + for (const Undo::Event& event : events) + removeRedos(event.event.id()); // remove any redos which are made invalid by these deletions emitChanged(); } } void Undo::saveReactivate(const KAEvent& event, RESOURCE_PARAM_TYPE resource, const QString& name) { new UndoReactivate(UNDO, event, resource, name); emitChanged(); } void Undo::saveReactivates(const EventList& events, const QString& name) { int count = events.count(); if (count == 1) saveReactivate(events[0].event, events[0].EVENT_RESOURCE, name); else if (count > 1) { new UndoReactivates(UNDO, events, name); emitChanged(); } } /****************************************************************************** * Remove any redos which are made invalid by a new undo. */ void Undo::removeRedos(const QString& eventID) { QString id = eventID; for (int i = 0; i < mRedoList.count(); ) { UndoItem* item = mRedoList[i]; -//qCDebug(KALARM_LOG)<eventID()<<" (looking for"<operation() == UndoItem::MULTI) { if (item->deleteID(id)) { // The old multi-redo was replaced with a new single redo delete item; // N.B. 'delete' removes the object from the list } } else if (item->eventID() == id) { if (item->operation() == UndoItem::EDIT) id = item->oldEventID(); // continue looking for its post-edit ID delete item; // N.B. 'delete' removes the object from the list } else ++i; } } /****************************************************************************** * Undo or redo a specified item. * Reply = true if success, or if the item no longer exists. */ bool Undo::undo(int i, Undo::Type type, QWidget* parent, const QString& action) { - UndoItem::mRestoreError = UndoItem::ERR_NONE; - UndoItem::mRestoreWarning = UndoItem::WARN_NONE; - UndoItem::mRestoreWarningKorg = KAlarm::UPDATE_OK; + UndoItem::mRestoreError = UndoItem::ERR_NONE; + UndoItem::mRestoreWarning = UndoItem::WARN_NONE; + UndoItem::mRestoreWarningKorg = KAlarm::UPDATE_OK; UndoItem::mRestoreWarningCount = 0; List& list = (type == UNDO) ? mUndoList : mRedoList; if (i < list.count() && list[i]->type() == type) { list[i]->restore(); delete list[i]; // N.B. 'delete' removes the object from its list emitChanged(); } QString err; switch (UndoItem::mRestoreError) { case UndoItem::ERR_NONE: { KAlarm::UpdateError errcode; switch (UndoItem::mRestoreWarning) { case UndoItem::WARN_KORG_ADD: errcode = KAlarm::ERR_ADD; break; case UndoItem::WARN_KORG_MODIFY: errcode = KAlarm::ERR_MODIFY; break; case UndoItem::WARN_KORG_DELETE: errcode = KAlarm::ERR_DELETE; break; case UndoItem::WARN_NONE: default: return true; } KAlarm::displayKOrgUpdateError(parent, errcode, UndoItem::mRestoreWarningKorg, UndoItem::mRestoreWarningCount); return true; } case UndoItem::ERR_NOT_FOUND: err = i18nc("@info", "Alarm not found"); break; case UndoItem::ERR_CREATE: err = i18nc("@info", "Error recreating alarm"); break; case UndoItem::ERR_TEMPLATE: err = i18nc("@info", "Error recreating alarm template"); break; case UndoItem::ERR_ARCHIVED: err = i18nc("@info", "Cannot reactivate archived alarm"); break; case UndoItem::ERR_PROG: err = i18nc("@info", "Program error"); break; default: err = i18nc("@info", "Unknown error"); break; } KAMessageBox::sorry(parent, i18nc("@info Undo-action: message", "%1: %2", action, err)); return false; } /****************************************************************************** * Add an undo item to the start of one of the lists. */ void Undo::add(UndoItem* item, bool undo) { if (item) { // Limit the number of items stored - int undoCount = mUndoList.count(); - int redoCount = mRedoList.count(); + const int undoCount = mUndoList.count(); + const int redoCount = mRedoList.count(); if (undoCount + redoCount >= maxCount - 1) { if (undoCount) mUndoList.pop_back(); else mRedoList.pop_back(); } // Append the new item - List* list = undo ? &mUndoList : &mRedoList; + List* const list = undo ? &mUndoList : &mRedoList; list->prepend(item); } } /****************************************************************************** * Remove an undo item from one of the lists. */ void Undo::remove(UndoItem* item, bool undo) { - List* list = undo ? &mUndoList : &mRedoList; + List* const list = undo ? &mUndoList : &mRedoList; if (!list->isEmpty()) list->removeAt(list->indexOf(item)); } /****************************************************************************** * Replace an undo item in one of the lists. */ void Undo::replace(UndoItem* old, UndoItem* New) { - Type type = old->type(); - List* list = (type == UNDO) ? &mUndoList : (type == REDO) ? &mRedoList : nullptr; + const Type type = old->type(); + List* const list = (type == UNDO) ? &mUndoList : (type == REDO) ? &mRedoList : nullptr; if (!list) return; - int i = list->indexOf(old); + const int i = list->indexOf(old); if (i >= 0) { New->setType(type); // ensure the item points to the correct list (*list)[i] = New; old->setType(NONE); // mark the old item as no longer being in a list } } /****************************************************************************** * Return the action description of the latest undo/redo item. */ QString Undo::actionText(Undo::Type type) { - List* list = (type == UNDO) ? &mUndoList : (type == REDO) ? &mRedoList : nullptr; + const List* const list = (type == UNDO) ? &mUndoList : (type == REDO) ? &mRedoList : nullptr; return (list && !list->isEmpty()) ? (*list)[0]->actionText() : QString(); } /****************************************************************************** * Return the action description of the undo/redo item with the specified ID. */ QString Undo::actionText(Undo::Type type, int id) { - UndoItem* undo = getItem(id, type); + const UndoItem* undo = getItem(id, type); return undo ? undo->actionText() : QString(); } /****************************************************************************** * Return the alarm description of the undo/redo item with the specified ID. */ QString Undo::description(Undo::Type type, int id) { - UndoItem* undo = getItem(id, type); + const UndoItem* undo = getItem(id, type); return undo ? undo->description() : QString(); } /****************************************************************************** * Return the descriptions of all undo or redo items, in order latest first. * For alarms which have undergone more than one change, only the first one is * listed, to force dependent undos to be executed in their correct order. * If 'ids' is non-null, also returns a list of their corresponding IDs. */ QList Undo::ids(Undo::Type type) { QList ids; QStringList ignoreIDs; //int n=0; - List* list = (type == UNDO) ? &mUndoList : (type == REDO) ? &mRedoList : nullptr; + const List* const list = (type == UNDO) ? &mUndoList : (type == REDO) ? &mRedoList : nullptr; if (!list) return ids; - for (int i = 0, end = list->count(); i < end; ++i) + for (const UndoItem* item : *list) { // Check whether this item should be ignored because it is a // dependent undo. If not, add this item's ID to the ignore list. - UndoItem* item = (*list)[i]; bool omit = false; if (item->operation() == UndoItem::MULTI) { // If any item in a multi-undo is disqualified, omit the whole multi-undo QStringList newIDs; const Undo::List* undos = ((UndoMultiBase*)item)->undos(); - for (int u = 0, uend = undos->count(); u < uend; ++u) + for (const UndoItem* undo : *undos) { - QString evid = (*undos)[u]->eventID(); + const QString evid = undo->eventID(); if (ignoreIDs.contains(evid)) omit = true; else if (omit) ignoreIDs.append(evid); else newIDs.append(evid); } if (omit) { - for (int i = 0, iend = newIDs.count(); i < iend; ++i) - ignoreIDs.append(newIDs[i]); + for (const QString& newID : qAsConst(newIDs)) + ignoreIDs.append(newID); } } else { omit = ignoreIDs.contains(item->eventID()); if (!omit) ignoreIDs.append(item->eventID()); if (item->operation() == UndoItem::EDIT) ignoreIDs.append(item->oldEventID()); // continue looking for its post-edit ID } if (!omit) ids.append(item->id()); //else qCDebug(KALARM_LOG)<<"Undo::ids(): omit"<actionText()<<":"<description(); } //qCDebug(KALARM_LOG)<<"Undo::ids():"<"<emitChanged(actionText(UNDO), actionText(REDO)); } /****************************************************************************** * Return the item with the specified ID. */ UndoItem* Undo::getItem(int id, Undo::Type type) { - List* list = (type == UNDO) ? &mUndoList : (type == REDO) ? &mRedoList : nullptr; + List* const list = (type == UNDO) ? &mUndoList : (type == REDO) ? &mRedoList : nullptr; if (list) { for (int i = 0, end = list->count(); i < end; ++i) { - if ((*list)[i]->id() == id) - return (*list)[i]; + UndoItem* item = (*list)[i]; + if (item->id() == id) + return item; } } return nullptr; } /****************************************************************************** * Find an item with the specified ID. */ int Undo::findItem(int id, Undo::Type type) { - List& list = (type == UNDO) ? mUndoList : mRedoList; + const List& list = (type == UNDO) ? mUndoList : mRedoList; int i = 0; - for (int end = list.count(); i < end; ++i) + for (const UndoItem* item : list) { - if (list[i]->id() == id) + if (item->id() == id) break; } return i; } /*============================================================================= = Class: UndoItem = A single undo action. =============================================================================*/ int UndoItem::mLastId = 0; UndoItem::Error UndoItem::mRestoreError; UndoItem::Warning UndoItem::mRestoreWarning; KAlarm::UpdateResult UndoItem::mRestoreWarningKorg; int UndoItem::mRestoreWarningCount; /****************************************************************************** * Constructor. * Optionally appends the undo to the list of undos. */ UndoItem::UndoItem(Undo::Type type, const QString& name) - : mName(name), - mId(0), - mType(type), - mCalendar(CalEvent::EMPTY) + : mName(name) + , mId(0) + , mType(type) + , mCalendar(CalEvent::EMPTY) { if (type != Undo::NONE) { mId = ++mLastId; if (mId < 0) mId = mLastId = 1; // wrap round if we reach a negative number Undo::add(this, (mType == Undo::UNDO)); } } /****************************************************************************** * Destructor. * Removes the undo from the list (if it's in the list). */ UndoItem::~UndoItem() { if (mType != Undo::NONE) Undo::remove(this, (mType == Undo::UNDO)); } /****************************************************************************** * Return the description of an event. */ QString UndoItem::description(const KAEvent& event) const { return (mCalendar == CalEvent::TEMPLATE) ? event.templateName() : AlarmText::summary(event); } /****************************************************************************** * Return the action description of an add or delete Undo/Redo item for displaying. */ QString UndoItem::addDeleteActionText(CalEvent::Type calendar, bool add) { switch (calendar) { case CalEvent::ACTIVE: if (add) return i18nc("@info Action to create a new alarm", "New alarm"); else return i18nc("@info Action to delete an alarm", "Delete alarm"); case CalEvent::TEMPLATE: if (add) return i18nc("@info Action to create a new alarm template", "New template"); else return i18nc("@info Action to delete an alarm template", "Delete template"); case CalEvent::ARCHIVED: return i18nc("@info", "Delete archived alarm"); default: break; } return QString(); } /*============================================================================= = Class: UndoMultiBase = Undo item for multiple alarms. =============================================================================*/ template UndoMulti::UndoMulti(Undo::Type type, const Undo::EventList& events, const QString& name) : UndoMultiBase(type, name) // UNDO only { - for (int i = 0, end = events.count(); i < end; ++i) - mUndos->append(new T(Undo::NONE, events[i])); + for (const Undo::Event& event : events) + mUndos->append(new T(Undo::NONE, event)); } /****************************************************************************** * Undo the item, i.e. restore multiple alarms which were deleted (or delete * alarms which were restored). * Create a redo item to delete (or restore) the alarms again. * Reply = redo item. */ template UndoItem* UndoMulti::restore() { Undo::List* newUndos = new Undo::List; for (int i = 0, end = mUndos->count(); i < end; ++i) { UndoItem* undo = (*mUndos)[i]->restore(); if (undo) newUndos->append(undo); } if (newUndos->isEmpty()) { delete newUndos; return nullptr; } // Create a redo item to delete the alarm again return createRedo(newUndos); } /****************************************************************************** * If one of the multiple items has the specified ID, delete it. * If an item is deleted and there is only one item left, the UndoMulti * instance is removed from its list and replaced by the remaining UndoItem instead. * Reply = true if this instance was replaced. The caller must delete it. * = false otherwise. */ template bool UndoMulti::deleteID(const QString& id) { for (int i = 0, end = mUndos->count(); i < end; ++i) { UndoItem* item = (*mUndos)[i]; if (item->eventID() == id) { // Found a matching entry - remove it mUndos->removeAt(i); if (mUndos->count() == 1) { // There is only one entry left after removal. // Replace 'this' multi instance with the remaining single entry. replaceWith(item); return true; } else { delete item; return false; } } } return false; } /*============================================================================= = Class: UndoAdd = Undo item for alarm creation. =============================================================================*/ UndoAdd::UndoAdd(Undo::Type type, const Undo::Event& undo, const QString& name) - : UndoItem(type, name), - mResource(undo.EVENT_RESOURCE), - mEventId(undo.event.id()) + : UndoItem(type, name) + , mResource(undo.EVENT_RESOURCE) + , mEventId(undo.event.id()) { setCalendar(undo.event.category()); mDescription = UndoItem::description(undo.event); // calendar must be set before calling this } UndoAdd::UndoAdd(Undo::Type type, const KAEvent& event, RESOURCE_PARAM_TYPE resource, const QString& name) - : UndoItem(type, name), - mResource(resource), - mEventId(event.id()) + : UndoItem(type, name) + , mResource(resource) + , mEventId(event.id()) { setCalendar(event.category()); mDescription = UndoItem::description(event); // calendar must be set before calling this } UndoAdd::UndoAdd(Undo::Type type, const KAEvent& event, RESOURCE_PARAM_TYPE resource, const QString& name, CalEvent::Type cal) - : UndoItem(type, name), - mResource(resource), - mEventId(CalEvent::uid(event.id(), cal)) // convert if old-style event ID + : UndoItem(type, name) + , mResource(resource) + , mEventId(CalEvent::uid(event.id(), cal)) // convert if old-style event ID { setCalendar(cal); mDescription = UndoItem::description(event); // calendar must be set before calling this } /****************************************************************************** * Undo the item, i.e. delete the alarm which was added. * Create a redo item to add the alarm back again. * Reply = redo item. */ UndoItem* UndoAdd::doRestore(bool setArchive) { // Retrieve the current state of the alarm qCDebug(KALARM_LOG) << "UndoAdd::doRestore:" << mEventId; const KAEvent* ev = AlarmCalendar::getEvent(EventId(mResource.id(), mEventId)); if (!ev) { mRestoreError = ERR_NOT_FOUND; // alarm is no longer in calendar return nullptr; } KAEvent event(*ev); // Create a redo item to recreate the alarm. // Do it now, since 'event' gets modified by KAlarm::deleteEvent() UndoItem* undo = createRedo(event, mResource); switch (calendar()) { case CalEvent::ACTIVE: { if (setArchive) event.setArchive(); // Archive it if it has already triggered KAlarm::UpdateResult status = KAlarm::deleteEvent(event, true); switch (status.status) { case KAlarm::UPDATE_ERROR: case KAlarm::UPDATE_FAILED: case KAlarm::SAVE_FAILED: mRestoreError = ERR_CREATE; break; case KAlarm::UPDATE_KORG_FUNCERR: case KAlarm::UPDATE_KORG_ERRINIT: case KAlarm::UPDATE_KORG_ERRSTART: case KAlarm::UPDATE_KORG_ERR: mRestoreWarning = WARN_KORG_DELETE; ++mRestoreWarningCount; if (status.status > mRestoreWarningKorg.status) mRestoreWarningKorg = status; break; default: break; } break; } case CalEvent::TEMPLATE: if (KAlarm::deleteTemplate(event) != KAlarm::UPDATE_OK) mRestoreError = ERR_TEMPLATE; break; case CalEvent::ARCHIVED: // redoing the deletion of an archived alarm KAlarm::deleteEvent(event); break; default: delete undo; mRestoreError = ERR_PROG; return nullptr; } return undo; } /****************************************************************************** * Create a redo item to add the alarm back again. */ UndoItem* UndoAdd::createRedo(const KAEvent& event, RESOURCE_PARAM_TYPE resource) { - Undo::Type t = (type() == Undo::UNDO) ? Undo::REDO : (type() == Undo::REDO) ? Undo::UNDO : Undo::NONE; + const Undo::Type t = (type() == Undo::UNDO) ? Undo::REDO : (type() == Undo::REDO) ? Undo::UNDO : Undo::NONE; return new UndoDelete(t, event, resource, QStringList(), mName); } /****************************************************************************** * Return the action description of the Undo item for displaying. */ QString UndoAdd::defaultActionText() const { return addDeleteActionText(calendar(), (type() == Undo::UNDO)); } /*============================================================================= = Class: UndoAdds = Undo item for multiple alarm creation. =============================================================================*/ /****************************************************************************** * Create a redo item to add the alarms again. */ UndoItem* UndoAdds::createRedo(Undo::List* undos) { - Undo::Type t = (type() == Undo::UNDO) ? Undo::REDO : (type() == Undo::REDO) ? Undo::UNDO : Undo::NONE; + const Undo::Type t = (type() == Undo::UNDO) ? Undo::REDO : (type() == Undo::REDO) ? Undo::UNDO : Undo::NONE; return new UndoAdds(t, undos, mName); } /****************************************************************************** * Return the action description of the Undo item for displaying. */ QString UndoAdds::defaultActionText() const { return i18nc("@info", "Create multiple alarms"); } /*============================================================================= = Class: UndoEdit = Undo item for alarm edit. =============================================================================*/ UndoEdit::UndoEdit(Undo::Type type, const KAEvent& oldEvent, const QString& newEventID, RESOURCE_PARAM_TYPE resource, const QStringList& dontShowErrors, const QString& description) - : UndoItem(type), - mResource(resource), - mOldEvent(new KAEvent(oldEvent)), - mNewEventId(newEventID), - mDescription(description), - mDontShowErrors(dontShowErrors) + : UndoItem(type) + , mResource(resource) + , mOldEvent(new KAEvent(oldEvent)) + , mNewEventId(newEventID) + , mDescription(description) + , mDontShowErrors(dontShowErrors) { setCalendar(oldEvent.category()); } UndoEdit::~UndoEdit() { delete mOldEvent; } /****************************************************************************** * Undo the item, i.e. undo an edit to a previously existing alarm. * Create a redo item to reapply the edit. * Reply = redo item. */ UndoItem* UndoEdit::restore() { qCDebug(KALARM_LOG) << "UndoEdit::restore:" << mNewEventId; // Retrieve the current state of the alarm const KAEvent* event = AlarmCalendar::getEvent(EventId(mResource.id(), mNewEventId)); if (!event) { mRestoreError = ERR_NOT_FOUND; // alarm is no longer in calendar return nullptr; } KAEvent newEvent(*event); // Create a redo item to restore the edit - Undo::Type t = (type() == Undo::UNDO) ? Undo::REDO : (type() == Undo::REDO) ? Undo::UNDO : Undo::NONE; + const Undo::Type t = (type() == Undo::UNDO) ? Undo::REDO : (type() == Undo::REDO) ? Undo::UNDO : Undo::NONE; UndoItem* undo = new UndoEdit(t, newEvent, mOldEvent->id(), mResource, KAlarm::dontShowErrors(EventId(newEvent)), mDescription); switch (calendar()) { case CalEvent::ACTIVE: { KAlarm::UpdateResult status = KAlarm::modifyEvent(newEvent, *mOldEvent); switch (status.status) { case KAlarm::UPDATE_ERROR: case KAlarm::UPDATE_FAILED: case KAlarm::SAVE_FAILED: mRestoreError = ERR_CREATE; break; case KAlarm::UPDATE_KORG_FUNCERR: case KAlarm::UPDATE_KORG_ERRINIT: case KAlarm::UPDATE_KORG_ERRSTART: case KAlarm::UPDATE_KORG_ERR: mRestoreWarning = WARN_KORG_MODIFY; ++mRestoreWarningCount; if (status.status > mRestoreWarningKorg.status) mRestoreWarningKorg = status; // fall through to default Q_FALLTHROUGH(); default: KAlarm::setDontShowErrors(EventId(*mOldEvent), mDontShowErrors); break; } break; } case CalEvent::TEMPLATE: if (KAlarm::updateTemplate(*mOldEvent) != KAlarm::UPDATE_OK) mRestoreError = ERR_TEMPLATE; break; case CalEvent::ARCHIVED: // editing of archived events is not allowed default: delete undo; mRestoreError = ERR_PROG; return nullptr; } return undo; } /****************************************************************************** * Return the action description of the Undo item for displaying. */ QString UndoEdit::defaultActionText() const { switch (calendar()) { case CalEvent::ACTIVE: return i18nc("@info Action to edit an alarm", "Edit alarm"); case CalEvent::TEMPLATE: return i18nc("@info Action to edit an alarm template", "Edit template"); default: break; } return QString(); } /*============================================================================= = Class: UndoDelete = Undo item for alarm deletion. =============================================================================*/ UndoDelete::UndoDelete(Undo::Type type, const Undo::Event& undo, const QString& name) - : UndoItem(type, name), - mResource(undo.EVENT_RESOURCE), - mEvent(new KAEvent(undo.event)), - mDontShowErrors(undo.dontShowErrors) + : UndoItem(type, name) + , mResource(undo.EVENT_RESOURCE) + , mEvent(new KAEvent(undo.event)) + , mDontShowErrors(undo.dontShowErrors) { setCalendar(mEvent->category()); } UndoDelete::UndoDelete(Undo::Type type, const KAEvent& event, RESOURCE_PARAM_TYPE resource, const QStringList& dontShowErrors, const QString& name) - : UndoItem(type, name), - mResource(resource), - mEvent(new KAEvent(event)), - mDontShowErrors(dontShowErrors) + : UndoItem(type, name) + , mResource(resource) + , mEvent(new KAEvent(event)) + , mDontShowErrors(dontShowErrors) { setCalendar(mEvent->category()); } UndoDelete::~UndoDelete() { delete mEvent; } /****************************************************************************** * Undo the item, i.e. restore an alarm which was deleted. * Create a redo item to delete the alarm again. * Reply = redo item. */ UndoItem* UndoDelete::restore() { qCDebug(KALARM_LOG) << "UndoDelete::restore:" << mEvent->id(); // Restore the original event switch (calendar()) { case CalEvent::ACTIVE: if (mEvent->toBeArchived()) { // It was archived when it was deleted mEvent->setCategory(CalEvent::ARCHIVED); - KAlarm::UpdateResult status = KAlarm::reactivateEvent(*mEvent, &mResource); + const KAlarm::UpdateResult status = KAlarm::reactivateEvent(*mEvent, &mResource); switch (status.status) { case KAlarm::UPDATE_KORG_FUNCERR: case KAlarm::UPDATE_KORG_ERRINIT: case KAlarm::UPDATE_KORG_ERRSTART: case KAlarm::UPDATE_KORG_ERR: mRestoreWarning = WARN_KORG_ADD; ++mRestoreWarningCount; if (status.status > mRestoreWarningKorg.status) mRestoreWarningKorg = status; break; case KAlarm::UPDATE_ERROR: case KAlarm::UPDATE_FAILED: case KAlarm::SAVE_FAILED: mRestoreError = ERR_ARCHIVED; return nullptr; case KAlarm::UPDATE_OK: break; } } else { - KAlarm::UpdateResult status = KAlarm::addEvent(*mEvent, &mResource, nullptr, true); + const KAlarm::UpdateResult status = KAlarm::addEvent(*mEvent, &mResource, nullptr, true); switch (status.status) { case KAlarm::UPDATE_KORG_FUNCERR: case KAlarm::UPDATE_KORG_ERRINIT: case KAlarm::UPDATE_KORG_ERRSTART: case KAlarm::UPDATE_KORG_ERR: mRestoreWarning = WARN_KORG_ADD; ++mRestoreWarningCount; if (status.status > mRestoreWarningKorg.status) mRestoreWarningKorg = status; break; case KAlarm::UPDATE_ERROR: case KAlarm::UPDATE_FAILED: case KAlarm::SAVE_FAILED: mRestoreError = ERR_CREATE; return nullptr; case KAlarm::UPDATE_OK: break; } } KAlarm::setDontShowErrors(EventId(*mEvent), mDontShowErrors); break; case CalEvent::TEMPLATE: if (KAlarm::addTemplate(*mEvent, &mResource) != KAlarm::UPDATE_OK) { mRestoreError = ERR_CREATE; return nullptr; } break; case CalEvent::ARCHIVED: if (!KAlarm::addArchivedEvent(*mEvent, &mResource)) { mRestoreError = ERR_CREATE; return nullptr; } break; default: mRestoreError = ERR_PROG; return nullptr; } // Create a redo item to delete the alarm again return createRedo(*mEvent, mResource); } /****************************************************************************** * Create a redo item to archive the alarm again. */ UndoItem* UndoDelete::createRedo(const KAEvent& event, RESOURCE_PARAM_TYPE resource) { - Undo::Type t = (type() == Undo::UNDO) ? Undo::REDO : (type() == Undo::REDO) ? Undo::UNDO : Undo::NONE; + const Undo::Type t = (type() == Undo::UNDO) ? Undo::REDO : (type() == Undo::REDO) ? Undo::UNDO : Undo::NONE; return new UndoAdd(t, event, resource, mName); } /****************************************************************************** * Return the action description of the Undo item for displaying. */ QString UndoDelete::defaultActionText() const { return addDeleteActionText(calendar(), (type() == Undo::REDO)); } /*============================================================================= = Class: UndoDeletes = Undo item for multiple alarm deletion. =============================================================================*/ /****************************************************************************** * Create a redo item to delete the alarms again. */ UndoItem* UndoDeletes::createRedo(Undo::List* undos) { - Undo::Type t = (type() == Undo::UNDO) ? Undo::REDO : (type() == Undo::REDO) ? Undo::UNDO : Undo::NONE; + const Undo::Type t = (type() == Undo::UNDO) ? Undo::REDO : (type() == Undo::REDO) ? Undo::UNDO : Undo::NONE; return new UndoDeletes(t, undos, mName); } /****************************************************************************** * Return the action description of the Undo item for displaying. */ QString UndoDeletes::defaultActionText() const { if (mUndos->isEmpty()) return QString(); - for (int i = 0, end = mUndos->count(); i < end; ++i) + for (const UndoItem* item : *mUndos) { - switch ((*mUndos)[i]->calendar()) + switch (item->calendar()) { case CalEvent::ACTIVE: return i18nc("@info", "Delete multiple alarms"); case CalEvent::TEMPLATE: return i18nc("@info", "Delete multiple templates"); case CalEvent::ARCHIVED: break; // check if they are ALL archived default: return QString(); } } return i18nc("@info", "Delete multiple archived alarms"); } /*============================================================================= = Class: UndoReactivate = Undo item for alarm reactivation. =============================================================================*/ /****************************************************************************** * Undo the item, i.e. re-archive the alarm which was reactivated. * Create a redo item to reactivate the alarm back again. * Reply = redo item. */ UndoItem* UndoReactivate::restore() { qCDebug(KALARM_LOG) << "UndoReactivate::restore"; // Validate the alarm's calendar switch (calendar()) { case CalEvent::ACTIVE: break; default: mRestoreError = ERR_PROG; return nullptr; } return UndoAdd::doRestore(true); // restore alarm, ensuring that it is re-archived } /****************************************************************************** * Create a redo item to add the alarm back again. */ UndoItem* UndoReactivate::createRedo(const KAEvent& event, RESOURCE_PARAM_TYPE resource) { - Undo::Type t = (type() == Undo::UNDO) ? Undo::REDO : (type() == Undo::REDO) ? Undo::UNDO : Undo::NONE; + const Undo::Type t = (type() == Undo::UNDO) ? Undo::REDO : (type() == Undo::REDO) ? Undo::UNDO : Undo::NONE; return new UndoDeactivate(t, event, resource, mName); } /****************************************************************************** * Return the action description of the Undo item for displaying. */ QString UndoReactivate::defaultActionText() const { return i18nc("@info", "Reactivate alarm"); } /*============================================================================= = Class: UndoDeactivate = Redo item for alarm reactivation. =============================================================================*/ /****************************************************************************** * Undo the item, i.e. reactivate an alarm which was archived. * Create a redo item to archive the alarm again. * Reply = redo item. */ UndoItem* UndoDeactivate::restore() { qCDebug(KALARM_LOG) << "UndoDeactivate::restore"; // Validate the alarm's calendar switch (calendar()) { case CalEvent::ACTIVE: break; default: mRestoreError = ERR_PROG; return nullptr; } return UndoDelete::restore(); } /****************************************************************************** * Create a redo item to archive the alarm again. */ UndoItem* UndoDeactivate::createRedo(const KAEvent& event, RESOURCE_PARAM_TYPE resource) { - Undo::Type t = (type() == Undo::UNDO) ? Undo::REDO : (type() == Undo::REDO) ? Undo::UNDO : Undo::NONE; + const Undo::Type t = (type() == Undo::UNDO) ? Undo::REDO : (type() == Undo::REDO) ? Undo::UNDO : Undo::NONE; return new UndoReactivate(t, event, resource, mName); } /****************************************************************************** * Return the action description of the Undo item for displaying. */ QString UndoDeactivate::defaultActionText() const { return i18nc("@info", "Reactivate alarm"); } /*============================================================================= = Class: UndoReactivates = Undo item for multiple alarm reactivation. =============================================================================*/ /****************************************************************************** * Create a redo item to reactivate the alarms again. */ UndoItem* UndoReactivates::createRedo(Undo::List* undos) { - Undo::Type t = (type() == Undo::UNDO) ? Undo::REDO : (type() == Undo::REDO) ? Undo::UNDO : Undo::NONE; + const Undo::Type t = (type() == Undo::UNDO) ? Undo::REDO : (type() == Undo::REDO) ? Undo::UNDO : Undo::NONE; return new UndoReactivates(t, undos, mName); } /****************************************************************************** * Return the action description of the Undo item for displaying. */ QString UndoReactivates::defaultActionText() const { return i18nc("@info", "Reactivate multiple alarms"); } /*============================================================================= = Class: Event = Event details for external calls. =============================================================================*/ Undo::Event::Event(const KAEvent& e, RESOURCE_PARAM_TYPE r) - : event(e), - EVENT_RESOURCE(r) + : event(e) + , EVENT_RESOURCE(r) { if (e.category() == CalEvent::ACTIVE) dontShowErrors = KAlarm::dontShowErrors(EventId(e)); } // vim: et sw=4: diff --git a/src/wakedlg.cpp b/src/wakedlg.cpp index c3e16bc7..aecd431d 100644 --- a/src/wakedlg.cpp +++ b/src/wakedlg.cpp @@ -1,200 +1,200 @@ /* * wakedlg.cpp - dialog to configure wake-from-suspend alarms * Program: kalarm * Copyright © 2011-2012 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 "wakedlg.h" #include "ui_wakedlg.h" #include "alarmcalendar.h" #include "functions.h" #include "kalarmapp.h" #include "mainwindow.h" #include "messagebox.h" #include "preferences.h" #include #include #include #include #include #include "kalarm_debug.h" using namespace KAlarmCal; WakeFromSuspendDlg* WakeFromSuspendDlg::mInstance = nullptr; WakeFromSuspendDlg* WakeFromSuspendDlg::create(QWidget* parent) { if (!mInstance) mInstance = new WakeFromSuspendDlg(parent); return mInstance; } WakeFromSuspendDlg::WakeFromSuspendDlg(QWidget* parent) : QDialog(parent) { setAttribute(Qt::WA_DeleteOnClose); setWindowTitle(i18nc("@title:window", "Wake From Suspend")); mUi = new Ui_WakeFromSuspendDlgWidget; mUi->setupUi(this); mUi->advanceWakeTime->setValue(Preferences::wakeFromSuspendAdvance()); mMainWindow = qobject_cast(parent); if (!mMainWindow) mMainWindow = MainWindow::mainMainWindow(); // Check if there is any alarm selected in the main window, and enable/disable // the Show and Cancel buttons as necessary. enableDisableUseButton(); // Update the Show and Cancel button status every 5 seconds mTimer = new QTimer(this); connect(mTimer, &QTimer::timeout, this, &WakeFromSuspendDlg::checkPendingAlarm); mTimer->start(5000); connect(mMainWindow, &MainWindow::selectionChanged, this, &WakeFromSuspendDlg::enableDisableUseButton); connect(mUi->showWakeButton, &QPushButton::clicked, this, &WakeFromSuspendDlg::showWakeClicked); connect(mUi->useWakeButton, &QPushButton::clicked, this, &WakeFromSuspendDlg::useWakeClicked); connect(mUi->cancelWakeButton, &QPushButton::clicked, this, &WakeFromSuspendDlg::cancelWakeClicked); connect(mUi->buttonBox, &QDialogButtonBox::rejected, this, &WakeFromSuspendDlg::close); connect(theApp(), &KAlarmApp::alarmEnabledToggled, this, &WakeFromSuspendDlg::enableDisableUseButton); } WakeFromSuspendDlg::~WakeFromSuspendDlg() { if (mInstance == this) mInstance = nullptr; delete mUi; } /****************************************************************************** * Called when the alarm selection in the main window changes. * Enable or disable the Use Highlighted Alarm button. */ void WakeFromSuspendDlg::enableDisableUseButton() { bool enable = theApp()->alarmsEnabled(); if (enable) { - QString wakeFromSuspendId = KAlarm::checkRtcWakeConfig().value(0); + const QString wakeFromSuspendId = KAlarm::checkRtcWakeConfig().value(0); const KAEvent event = mMainWindow->selectedEvent(); enable = event.isValid() && event.category() == CalEvent::ACTIVE && event.enabled() && !event.mainDateTime().isDateOnly() && event.id() != wakeFromSuspendId; } mUi->useWakeButton->setEnabled(enable); checkPendingAlarm(); } /****************************************************************************** * Update the Show and Cancel buttons if the pending alarm status has changed. * Reply = true if an alarm is still pending. */ bool WakeFromSuspendDlg::checkPendingAlarm() { if (KAlarm::checkRtcWakeConfig(true).isEmpty()) { mUi->showWakeButton->setEnabled(false); mUi->cancelWakeButton->setEnabled(false); return false; } return true; } /****************************************************************************** * Called when the user clicks the Show Current Alarm button. * Highlight the currently scheduled wake-from-suspend alarm in the main window. */ void WakeFromSuspendDlg::showWakeClicked() { if (checkPendingAlarm()) { const QStringList params = KAlarm::checkRtcWakeConfig(); if (!params.isEmpty()) { - KAEvent* event = AlarmCalendar::resources()->event(EventId(params[0].toLongLong(), params[1])); + const KAEvent* event = AlarmCalendar::resources()->event(EventId(params[0].toLongLong(), params[1])); if (event) { mMainWindow->selectEvent(event->itemId()); return; } } } mMainWindow->clearSelection(); } /****************************************************************************** * Called when the user clicks the Use Highlighted Alarm button. * Schedules system wakeup for that alarm. */ void WakeFromSuspendDlg::useWakeClicked() { - KAEvent event = mMainWindow->selectedEvent(); + const KAEvent event = mMainWindow->selectedEvent(); if (!event.isValid()) return; const KADateTime dt = event.mainDateTime().kDateTime(); if (dt.isDateOnly()) { KAMessageBox::sorry(this, i18nc("@info", "Cannot schedule wakeup time for a date-only alarm")); return; } if (KAMessageBox::warningContinueCancel(this, xi18nc("@info", "This wakeup will cancel any existing wakeup which has been set by KAlarm " "or any other application, because your computer can only schedule a single wakeup time." "Note: Wake From Suspend is not supported at all on some computers, especially older ones, " "and some computers only support setting a wakeup time up to 24 hours ahead. " "You may wish to set up a test alarm to check your system's capability."), QString(), KStandardGuiItem::cont(), KStandardGuiItem::cancel(), QStringLiteral("wakeupWarning")) != KMessageBox::Continue) return; - int advance = mUi->advanceWakeTime->value(); - qint64 triggerTime = dt.addSecs(-advance * 60).toSecsSinceEpoch(); + const int advance = mUi->advanceWakeTime->value(); + const qint64 triggerTime = dt.addSecs(-advance * 60).toSecsSinceEpoch(); if (KAlarm::setRtcWakeTime(triggerTime, this)) { const QStringList param{QString::number(event.collectionId()), event.id(), QString::number(triggerTime)}; KConfigGroup config(KSharedConfig::openConfig(), "General"); config.writeEntry("RtcWake", param); config.sync(); Preferences::setWakeFromSuspendAdvance(advance); close(); } } /****************************************************************************** * Called when the user clicks the Cancel Wake From Suspend button. * Cancels any currently scheduled system wakeup. */ void WakeFromSuspendDlg::cancelWakeClicked() { KAlarm::setRtcWakeTime(0, this); KAlarm::deleteRtcWakeConfig(); mUi->showWakeButton->setEnabled(false); mUi->cancelWakeButton->setEnabled(false); enableDisableUseButton(); } // vim: et sw=4: