diff --git a/src/akonadimodel.cpp b/src/akonadimodel.cpp index 5f593b10..bda9e6a3 100644 --- a/src/akonadimodel.cpp +++ b/src/akonadimodel.cpp @@ -1,1943 +1,1943 @@ /* * akonadimodel.cpp - KAlarm calendar file access using Akonadi * Program: kalarm * Copyright © 2007-2014,2018 by 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 #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) { // 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 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) << "Server running"; mResourcesChecked = true; mMigrating = true; CalendarMigrator::execute(); } break; case ServerManager::NotRunning: qCDebug(KALARM_LOG) << "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 (!collection.hasAttribute()) return 0; return static_cast(collection.attribute()->enabled()); 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 (!collection.hasAttribute() || !isCompatible(collection)) return 0; return static_cast(collection.attribute()->standard()); case KeepFormatRole: if (!collection.hasAttribute()) return false; return collection.attribute()->keepFormat(); 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 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(); if (collection.isValid()) { // This is a Collection row bool updateCollection = false; CollectionAttribute* attr = nullptr; switch (role) { case Qt::BackgroundRole: { const QColor colour = value.value(); attr = collection.attribute(Collection::AddIfMissing); if (attr->backgroundColor() == colour) return true; // no change attr->setBackgroundColor(colour); updateCollection = true; break; } case EnabledTypesRole: { const CalEvent::Types types = static_cast(value.toInt()); attr = collection.attribute(Collection::AddIfMissing); if (attr->enabled() == types) return true; // no change qCDebug(KALARM_LOG) << "Set enabled:" << types << ", was=" << attr->enabled(); attr->setEnabled(types); updateCollection = true; break; } case IsStandardRole: if (collection.hasAttribute() && isCompatible(collection)) { const CalEvent::Types types = static_cast(value.toInt()); attr = collection.attribute(Collection::AddIfMissing); qCDebug(KALARM_LOG)<<"Set standard:"<"<= ColumnCount) return QVariant(); if (role == Qt::DisplayRole) { 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 QString(); case TypeColumn: return QString(); 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); + 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); + 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); + 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); + 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() || !collection.hasAttribute()) return QColor(); return collection.attribute()->backgroundColor(); } /****************************************************************************** * 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 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; } /****************************************************************************** * 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 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) << collection.id(); + 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) { QMap::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) << 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) << collection.id(); + 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); + qCDebug(KALARM_LOG) << "AkonadiModel::reload"; const Collection::List collections = mMonitor->collectionsMonitored(); foreach (const Collection& collection, collections) { mMonitor->setCollectionMonitored(collection, false); mMonitor->setCollectionMonitored(collection, true); } } /****************************************************************************** * Called when a collection modification job has completed. * Checks for any error. */ void AkonadiModel::modifyCollectionJobDone(KJob* j) { Collection collection = static_cast(j)->collection(); const Collection::Id id = collection.id(); if (j->error()) { Q_EMIT collectionModified(id, false); if (mCollectionsDeleted.contains(id)) mCollectionsDeleted.removeAll(id); else { const QString errMsg = i18nc("@info", "Failed to update calendar \"%1\".", displayName(collection)); qCCritical(KALARM_LOG) << "Id:" << collection.id() << errMsg << ":" << j->errorString(); KAMessageBox::error(MainWindow::mainMainWindow(), i18nc("@info", "%1\n(%2)", errMsg, j->errorString())); } } else Q_EMIT collectionModified(id, true); } /****************************************************************************** * 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 QModelIndexList indexes = match(start, RemoteIdRole, event.id(), -1, flags); foreach (const QModelIndex& ix, indexes) { if (ix.isValid()) { 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) << event->id(); + 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); 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) << "ID:" << event.id(); + qCDebug(KALARM_LOG) << "AkonadiModel::addEvent: ID:" << event.id(); Item item; if (!event.setItemPayload(item, collection.contentMimeTypes())) { qCWarning(KALARM_LOG) << "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) << "ID:" << event.id(); + 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) << item.id(); + qCDebug(KALARM_LOG) << "AkonadiModel::queueItemModifyJob:" << item.id(); QMap::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 QMap::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) << jobClass; + 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) << 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 QMap::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 QMap::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) << start << "-" << end << "(parent =" << parent << ")"; + 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(); 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) << start << "-" << end << "(parent =" << parent << ")"; + qCDebug(KALARM_LOG) << "AkonadiModel::slotRowsAboutToBeRemoved:" << start << "-" << end << "(parent =" << parent << ")"; const EventList events = eventList(parent, start, end); if (!events.isEmpty()) { foreach (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'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) << "Collection" << collection.id() << ": rights ->" << newRights; + 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) << "Collection" << collection.id() << ": alarm types ->" << newAlarmTypes; + 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())) { static bool firstEnabled = true; const CalEvent::Types oldEnabled = mCollectionEnabled.value(collection.id(), CalEvent::EMPTY); const CalEvent::Types newEnabled = collection.hasAttribute() ? collection.attribute()->enabled() : CalEvent::EMPTY; if (firstEnabled || newEnabled != oldEnabled) { - qCDebug(KALARM_LOG) << "Collection" << collection.id() << ": enabled ->" << newEnabled; + 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) << "CompatibilityAttribute"; + 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) << "Migration 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) << 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; } /****************************************************************************** * 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) << "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) << "item id=" << item.id() << ", revision=" << item.revision(); + 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); foreach (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/akonadiresourcecreator.cpp b/src/akonadiresourcecreator.cpp index 30862300..77c2981a 100644 --- a/src/akonadiresourcecreator.cpp +++ b/src/akonadiresourcecreator.cpp @@ -1,185 +1,185 @@ /* * akonadiresourcecreator.cpp - interactively create an Akonadi resource * Program: kalarm * Copyright © 2011,2019 by 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 "akonadiresourcecreator.h" #include "autoqpointer.h" #include "kalarmsettings.h" #include "kalarmdirsettings.h" #include "controlinterface.h" #include #include #include #include #include #include #include #include #include #include "kalarm_debug.h" using namespace Akonadi; using namespace KAlarmCal; AkonadiResourceCreator::AkonadiResourceCreator(CalEvent::Type defaultType, QWidget* parent) : QObject(), mParent(parent), mDefaultType(defaultType) { } /****************************************************************************** * Create a new resource. The user will be prompted to enter its configuration. */ void AkonadiResourceCreator::createResource() { QTimer::singleShot(0, this, &AkonadiResourceCreator::getAgentType); } void AkonadiResourceCreator::getAgentType() { - qCDebug(KALARM_LOG) << "Type:" << mDefaultType; + qCDebug(KALARM_LOG) << "AkonadiResourceCreator::getAgentType: Type:" << mDefaultType; // 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 dlg = new AgentTypeDialog(mParent); QString mimeType; switch (mDefaultType) { case CalEvent::ACTIVE: mimeType = KAlarmCal::MIME_ACTIVE; break; case CalEvent::ARCHIVED: mimeType = KAlarmCal::MIME_ARCHIVED; break; case CalEvent::TEMPLATE: mimeType = KAlarmCal::MIME_TEMPLATE; break; default: Q_EMIT finished(this, false); return; } dlg->agentFilterProxyModel()->addMimeTypeFilter(mimeType); dlg->agentFilterProxyModel()->addCapabilityFilter(QStringLiteral("Resource")); if (dlg->exec() != QDialog::Accepted) { Q_EMIT finished(this, false); return; } mAgentType = dlg->agentType(); if (!mAgentType.isValid()) { Q_EMIT finished(this, false); return; } AgentInstanceCreateJob* job = new AgentInstanceCreateJob(mAgentType, mParent); connect(job, &AgentInstanceCreateJob::result, this, &AkonadiResourceCreator::agentInstanceCreated); job->start(); } /****************************************************************************** * Called when an agent creation job has completed. * Checks for any error. */ void AkonadiResourceCreator::agentInstanceCreated(KJob* j) { AgentInstanceCreateJob* job = static_cast(j); if (j->error()) { - qCCritical(KALARM_LOG) << "Failed to create new calendar resource:" << j->errorString(); + qCCritical(KALARM_LOG) << "AkonadiResourceCreator::agentInstanceCreated: Failed to create new calendar resource:" << j->errorString(); KMessageBox::error(nullptr, xi18nc("@info", "%1(%2)", i18nc("@info", "Failed to create new calendar resource"), j->errorString())); exitWithError(); } else { // Set the default alarm type for a directory resource config dialog mAgentInstance = job->instance(); QString type = mAgentInstance.type().identifier(); if (type == QLatin1String("akonadi_kalarm_dir_resource")) setResourceAlarmType(); else if (type == QLatin1String("akonadi_kalarm_resource")) setResourceAlarmType(); // Display the resource config dialog, but first ensure we get // notified of the user cancelling the operation. org::freedesktop::Akonadi::Agent::Control* agentControlIface = new org::freedesktop::Akonadi::Agent::Control(QStringLiteral("org.freedesktop.Akonadi.Agent.") + mAgentInstance.identifier(), QStringLiteral("/"), KDBusConnectionPool::threadConnection(), this); bool controlOk = agentControlIface && agentControlIface->isValid(); if (!controlOk) { delete agentControlIface; - qCWarning(KALARM_LOG) << "Unable to access D-Bus interface of created agent."; + qCWarning(KALARM_LOG) << "AkonadiResourceCreator::agentInstanceCreated: Unable to access D-Bus interface of created agent."; } else { connect(agentControlIface, &org::freedesktop::Akonadi::Agent::Control::configurationDialogAccepted, this, &AkonadiResourceCreator::configurationDialogAccepted); connect(agentControlIface, &org::freedesktop::Akonadi::Agent::Control::configurationDialogRejected, this, &AkonadiResourceCreator::exitWithError); } QPointer dlg = new AgentConfigurationDialog(mAgentInstance, mParent); dlg->exec(); delete dlg; if (!controlOk) Q_EMIT finished(this, true); // don't actually know the result in this case } } /****************************************************************************** * Set the alarm type for an Akonadi resource. */ template void AkonadiResourceCreator::setResourceAlarmType() { Settings iface(QStringLiteral("org.freedesktop.Akonadi.Resource.") + mAgentInstance.identifier(), QStringLiteral("/Settings"), QDBusConnection::sessionBus(), this); if (!iface.isValid()) - qCCritical(KALARM_LOG) << "Error creating D-Bus interface for" << mAgentInstance.identifier() << "resource configuration."; + qCCritical(KALARM_LOG) << "AkonadiResourceCreator::setResourceAlarmType: Error creating D-Bus interface for" << mAgentInstance.identifier() << "resource configuration."; else { iface.setAlarmTypes(CalEvent::mimeTypes(mDefaultType)); iface.save(); mAgentInstance.reconfigure(); // notify the agent that its configuration has changed } } /****************************************************************************** * Called when the user has clicked OK in the resource configuration dialog. */ void AkonadiResourceCreator::configurationDialogAccepted() { Q_EMIT finished(this, true); } /****************************************************************************** * Called when the user has clicked cancel in the resource configuration dialog. * Remove the newly created agent instance. */ void AkonadiResourceCreator::exitWithError() { AgentManager::self()->removeInstance(mAgentInstance); Q_EMIT finished(this, false); } // vim: et sw=4: diff --git a/src/alarmcalendar.cpp b/src/alarmcalendar.cpp index e0da06d7..7ae18f0d 100644 --- a/src/alarmcalendar.cpp +++ b/src/alarmcalendar.cpp @@ -1,1601 +1,1601 @@ /* * alarmcalendar.cpp - KAlarm calendar file access * Program: kalarm * Copyright © 2001-2018 by 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 KCalCore; using namespace KAlarmCal; static KACalendar::Compat fix(const KCalCore::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; /****************************************************************************** * 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) << "Open error"; + 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) { 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) { 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) << mUrl.toDisplayString(); + 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) << mUrl.toDisplayString(); + qCDebug(KALARM_LOG) << "AlarmCalendar::load:" << mUrl.toDisplayString(); if (!mUrl.isLocalFile()) { auto getJob = KIO::storedGet(mUrl); KJobWidgets::setWindow(getJob, MainWindow::mainMainWindow()); if (!getJob->exec()) { - qCCritical(KALARM_LOG) << "Download failure"; + qCCritical(KALARM_LOG) << "AlarmCalendar::load: Download failure"; KAMessageBox::error(MainWindow::mainMainWindow(), xi18nc("@info", "Cannot download calendar: %1", mUrl.toDisplayString())); return -1; } QTemporaryFile tmpFile; tmpFile.setAutoRemove(false); tmpFile.write(getJob->data()); qCDebug(KALARM_LOG) << "--- Downloaded to" << tmpFile.fileName(); filename = tmpFile.fileName(); } else { filename = mUrl.toLocalFile(); } mCalendarStorage->calendar()->setTimeZone(Preferences::timeSpecAsZone()); mCalendarStorage->setFileName(filename); if (!mCalendarStorage->load()) { // Check if the file is zero length if (mUrl.isLocalFile()) { auto statJob = KIO::stat(KIO::upUrl(mUrl)); KJobWidgets::setWindow(statJob, MainWindow::mainMainWindow()); statJob->exec(); KFileItem fi(statJob->statResult(), mUrl); if (!fi.size()) return 0; // file is zero length } - qCCritical(KALARM_LOG) << "Error loading calendar file '" << filename <<"'"; + qCCritical(KALARM_LOG) << "AlarmCalendar::load: Error loading calendar file '" << filename <<"'"; KAMessageBox::error(MainWindow::mainMainWindow(), xi18nc("@info", "Error loading calendar:%1Please fix or delete the file.", mUrl.toDisplayString())); // load() could have partially populated the calendar, so clear it out mCalendarStorage->calendar()->close(); mCalendarStorage->calendar().clear(); mCalendarStorage.clear(); mOpen = false; return -1; } if (!mLocalFile.isEmpty()) if (mLocalFile.startsWith(QDir::tempPath())) { QFile::remove(mLocalFile); } mLocalFile = filename; fix(mCalendarStorage); // convert events to current KAlarm format for when calendar is saved updateDisplayKAEvents(); } mOpen = true; return 1; } /****************************************************************************** * Reload the calendar file into memory. */ bool AlarmCalendar::reload() { if (mCalType == RESOURCES) return true; if (!mCalendarStorage) return false; { - qCDebug(KALARM_LOG) << mUrl.toDisplayString(); + 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) << "\"" << newFile << "\"," << mEventType; + 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) << "Saving" << saveFilename << "failed."; + 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) << saveFilename << "upload failed."; + qCCritical(KALARM_LOG) << "AlarmCalendar::saveCal:" << saveFilename << "upload failed."; KAMessageBox::error(MainWindow::mainMainWindow(), xi18nc("@info", "Cannot upload calendar to %1", mICalUrl.toDisplayString())); return false; } } if (mCalType == LOCAL_VCAL) { // The file was in vCalendar format, but has now been saved in iCalendar format. mUrl = mICalUrl; mCalType = LOCAL_ICAL; } Q_EMIT calendarSaved(this); } mUpdateSave = false; return true; } /****************************************************************************** * Delete any temporary file at program exit. */ void AlarmCalendar::close() { if (mCalType != RESOURCES) { if (!mLocalFile.isEmpty()) { if (mLocalFile.startsWith(QDir::tempPath())) { // removes it only if it IS a temporary file QFile::remove(mLocalFile); } mLocalFile = QStringLiteral(""); } } // Flag as closed now to prevent removeKAEvents() doing silly things // when it's called again mOpen = false; if (mCalendarStorage) { mCalendarStorage->calendar()->close(); mCalendarStorage->calendar().clear(); mCalendarStorage.clear(); } // Resource map should be empty, but just in case... while (!mResourceMap.isEmpty()) removeKAEvents(mResourceMap.begin().key(), true, CalEvent::ACTIVE | CalEvent::ARCHIVED | CalEvent::TEMPLATE | CalEvent::DISPLAYING); } /****************************************************************************** * 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); + 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) { 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) { 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) << "Ignoring unusable event" << kcalevent->uid(); + 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) << "Event" << event->id() << ", collection" << event->collectionId() << "Indexed under collection" << key; + 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]); } /****************************************************************************** * 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) << "Inconsistent AkonadiModel::Event: event:" << event.event.collectionId() << ", collection" << event.collection.id(); + 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)); bool enabled = event.event.enabled(); checkForDisabledAlarms(!enabled, enabled); if (added && enabled && event.event.category() == CalEvent::ACTIVE && 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) { if (!events[i].isConsistent()) - qCCritical(KALARM_LOG) << "Inconsistent AkonadiModel::Event: event:" << events[i].event.collectionId() << ", collection" << events[i].collection.id(); + 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); } } /****************************************************************************** * 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); + qCDebug(KALARM_LOG) << "AlarmCalendar::importAlarms"; QUrl url = KFileDialog::getOpenUrl(QUrl(QStringLiteral("filedialog:///importalarms")), QStringLiteral("*.vcs *.ics|%1").arg(i18nc("@info", "Calendar Files")), parent); if (url.isEmpty()) { - qCCritical(KALARM_LOG) << "Empty URL"; + qCCritical(KALARM_LOG) << "AlarmCalendar::importAlarms: Empty URL"; return false; } if (!url.isValid()) { - qCDebug(KALARM_LOG) << "Invalid URL"; + qCDebug(KALARM_LOG) << "AlarmCalendar::importAlarms: Invalid URL"; return false; } - qCDebug(KALARM_LOG) << url.toDisplayString(); + 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) << "File '" << url.toDisplayString() <<"' not found"; + 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) << "Download failure"; + 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) << "Error loading calendar '" << filename <<"'"; + 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; Collection activeColl, archiveColl, templateColl; Event::List events = cal->rawEvents(); for (int i = 0, end = events.count(); i < end; ++i) { 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) { 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); if (!url.isValid()) { - qCDebug(KALARM_LOG) << "Invalid URL" << url; + qCDebug(KALARM_LOG) << "AlarmCalendar::exportAlarms: Invalid URL" << url; return false; } - qCDebug(KALARM_LOG) << url.toDisplayString(); + 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) << "Error loading calendar file" << file << "for append"; + 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); 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) << file << ": failed"; + 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) << file << ": upload failed"; + 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) { deleteEventInternal(*events[i]); } 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) << evnt.id(); + qCDebug(KALARM_LOG) << "AlarmCalendar::addEvent:" << evnt.id(); 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 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; 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); 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()); - qCDebug(KALARM_LOG) << oldEventId << "->" << newId; + qCDebug(KALARM_LOG) << "AlarmCalendar::modifyEvent:" << oldEventId << "->" << newId; bool noNewId = newId.isEmpty(); if (!noNewId && oldEventId == newId) { - qCCritical(KALARM_LOG) << "Same IDs"; + 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); if (!storedEvent) { - qCCritical(KALARM_LOG) << "Old event not found"; + 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) << "error"; + 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); 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); 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()); 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) << "Event" << event.id() << ": collection" << event.collectionId() << "differs from 'collection'" << 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; 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); if (list.count() > 1) { - qCWarning(KALARM_LOG) << "Multiple events found with ID" << eventId; + 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) { if (eventlist[i]->templateName() == templateName) return eventlist[i]; } 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; 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]; } 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]; } } } 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]; 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); 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) { if (!eventlist[i]->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) { 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) { 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(); bool wasPending = mPendingAlarms.contains(id); - qCDebug(KALARM_LOG) << id << "," << pending << "(was" << wasPending << ")"; + 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/calendarmigrator.cpp b/src/calendarmigrator.cpp index aab905de..da29cca5 100644 --- a/src/calendarmigrator.cpp +++ b/src/calendarmigrator.cpp @@ -1,872 +1,872 @@ /* * calendarmigrator.cpp - migrates or creates KAlarm Akonadi resources * Program: kalarm * Copyright © 2011-2016 by 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 "calendarmigrator.h" #include "akonadimodel.h" #include "functions.h" #include "kalarmsettings.h" #include "kalarmdirsettings.h" #include "mainwindow.h" #include "messagebox.h" #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; namespace { const QString KALARM_RESOURCE(QStringLiteral("akonadi_kalarm_resource")); const QString KALARM_DIR_RESOURCE(QStringLiteral("akonadi_kalarm_dir_resource")); } // Creates, or migrates from KResources, a single alarm calendar class CalendarCreator : public QObject { Q_OBJECT public: // Constructor to migrate a calendar from KResources. CalendarCreator(const QString& resourceType, const KConfigGroup&); // Constructor to create a default Akonadi calendar. CalendarCreator(CalEvent::Type, const QString& file, const QString& name); bool isValid() const { return mAlarmType != CalEvent::EMPTY; } CalEvent::Type alarmType() const { return mAlarmType; } bool newCalendar() const { return mNew; } QString resourceName() const { return mName; } Collection::Id collectionId() const { return mCollectionId; } QString path() const { return mUrlString; } QString errorMessage() const { return mErrorMessage; } void createAgent(const QString& agentType, QObject* parent); public Q_SLOTS: void agentCreated(KJob*); Q_SIGNALS: void creating(const QString& path); void finished(CalendarCreator*); private Q_SLOTS: void fetchCollection(); void collectionFetchResult(KJob*); void resourceSynchronised(KJob*); void modifyCollectionJobDone(KJob*); private: void finish(bool cleanup); bool writeLocalFileConfig(); bool writeLocalDirectoryConfig(); bool writeRemoteFileConfig(); template Interface* writeBasicConfig(); enum ResourceType { LocalFile, LocalDir, RemoteFile }; AgentInstance mAgent; CalEvent::Type mAlarmType; ResourceType mResourceType; QString mUrlString; QString mName; QColor mColour; QString mErrorMessage; Collection::Id mCollectionId; int mCollectionFetchRetryCount; bool mReadOnly; bool mEnabled; bool mStandard; const bool mNew; // true if creating default, false if converting bool mFinished; }; // Updates the backend calendar format of a single alarm calendar class CalendarUpdater : public QObject { Q_OBJECT public: CalendarUpdater(const Collection& collection, bool dirResource, bool ignoreKeepFormat, bool newCollection, QObject* parent); ~CalendarUpdater(); // Return whether another instance is already updating this collection bool isDuplicate() const { return mDuplicate; } // Check whether any instance is for the given collection ID static bool containsCollection(Collection::Id); public Q_SLOTS: bool update(); private: static QList mInstances; Akonadi::Collection mCollection; QObject* mParent; const bool mDirResource; const bool mIgnoreKeepFormat; const bool mNewCollection; const bool mDuplicate; // another instance is already updating this collection }; CalendarMigrator* CalendarMigrator::mInstance = nullptr; bool CalendarMigrator::mCompleted = false; CalendarMigrator::CalendarMigrator(QObject* parent) : QObject(parent), mExistingAlarmTypes(0) { } CalendarMigrator::~CalendarMigrator() { - qCDebug(KALARM_LOG); + qCDebug(KALARM_LOG) << "~CalendarMigrator"; mInstance = nullptr; } /****************************************************************************** * Reset to allow migration to be run again. */ void CalendarMigrator::reset() { mCompleted = false; } /****************************************************************************** * Create and return the unique CalendarMigrator instance. */ CalendarMigrator* CalendarMigrator::instance() { if (!mInstance && !mCompleted) mInstance = new CalendarMigrator; return mInstance; } /****************************************************************************** * Migrate old KResource calendars, or if none, create default Akonadi resources. */ void CalendarMigrator::execute() { instance()->migrateOrCreate(); } /****************************************************************************** * Migrate old KResource calendars, and create default Akonadi resources. */ void CalendarMigrator::migrateOrCreate() { - qCDebug(KALARM_LOG); + qCDebug(KALARM_LOG) << "CalendarMigrator::migrateOrCreate"; // First, check whether any Akonadi resources already exist, and if // so, find their alarm types. const AgentInstance::List agents = AgentManager::self()->instances(); foreach (const AgentInstance& agent, agents) { const QString type = agent.type().identifier(); if (type == KALARM_RESOURCE || type == KALARM_DIR_RESOURCE) { // Fetch the resource's collection to determine its alarm types CollectionFetchJob* job = new CollectionFetchJob(Collection::root(), CollectionFetchJob::FirstLevel); job->fetchScope().setResource(agent.identifier()); mFetchesPending << job; connect(job, &KJob::result, this, &CalendarMigrator::collectionFetchResult); // Note: Once all collections have been fetched, any missing // default resources will be created. } } if (mFetchesPending.isEmpty()) { // There are no Akonadi resources, so migrate any KResources alarm // calendars from pre-Akonadi versions of KAlarm. const QString configFile = QStandardPaths::writableLocation(QStandardPaths::ConfigLocation) + QStringLiteral("/kresources/alarms/stdrc"); const KConfig config(configFile, KConfig::SimpleConfig); // Fetch all the KResource identifiers which are actually in use const KConfigGroup group = config.group("General"); const QStringList keys = group.readEntry("ResourceKeys", QStringList()) + group.readEntry("PassiveResourceKeys", QStringList()); // Create an Akonadi resource for each KResource id CalendarCreator* creator; foreach (const QString& id, keys) { const KConfigGroup configGroup = config.group(QStringLiteral("Resource_") + id); const QString resourceType = configGroup.readEntry("ResourceType", QString()); QString agentType; if (resourceType == QStringLiteral("file")) agentType = KALARM_RESOURCE; else if (resourceType == QStringLiteral("dir")) agentType = KALARM_DIR_RESOURCE; else if (resourceType == QStringLiteral("remote")) agentType = KALARM_RESOURCE; else continue; // unknown resource type - can't convert creator = new CalendarCreator(resourceType, configGroup); if (!creator->isValid()) delete creator; else { connect(creator, &CalendarCreator::finished, this, &CalendarMigrator::calendarCreated); connect(creator, &CalendarCreator::creating, this, &CalendarMigrator::creatingCalendar); mExistingAlarmTypes |= creator->alarmType(); mCalendarsPending << creator; creator->createAgent(agentType, this); } } // After migrating KResources, create any necessary additional default // Akonadi resources. createDefaultResources(); } } /****************************************************************************** * Called when a collection fetch job has completed. * Finds which mime types are handled by the existing collection. */ void CalendarMigrator::collectionFetchResult(KJob* j) { CollectionFetchJob* job = static_cast(j); const QString id = job->fetchScope().resource(); if (j->error()) - qCCritical(KALARM_LOG) << "CollectionFetchJob" << id << "error: " << j->errorString(); + qCCritical(KALARM_LOG) << "CalendarMigrator::collectionFetchResult: CollectionFetchJob" << id << "error: " << j->errorString(); else { const Collection::List collections = job->collections(); if (collections.isEmpty()) - qCCritical(KALARM_LOG) << "No collections found for resource" << id; + qCCritical(KALARM_LOG) << "CalendarMigrator::collectionFetchResult: No collections found for resource" << id; else mExistingAlarmTypes |= CalEvent::types(collections[0].contentMimeTypes()); } mFetchesPending.removeAll(job); if (mFetchesPending.isEmpty()) { // The alarm types of all collections have been found, so now // create any necessary default Akonadi resources. createDefaultResources(); } } /****************************************************************************** * Create default Akonadi resources for any alarm types not covered by existing * resources. Normally, this occurs on the first run of KAlarm, but if resources * have been deleted, it could occur on later runs. * If the default calendar files already exist, they will be used; otherwise * they will be created. */ void CalendarMigrator::createDefaultResources() { - qCDebug(KALARM_LOG); + qCDebug(KALARM_LOG) << "CalendarMigrator::createDefaultResources"; CalendarCreator* creator; if (!(mExistingAlarmTypes & CalEvent::ACTIVE)) { creator = new CalendarCreator(CalEvent::ACTIVE, QStringLiteral("calendar.ics"), i18nc("@info", "Active Alarms")); connect(creator, &CalendarCreator::finished, this, &CalendarMigrator::calendarCreated); connect(creator, &CalendarCreator::creating, this, &CalendarMigrator::creatingCalendar); mCalendarsPending << creator; creator->createAgent(KALARM_RESOURCE, this); } if (!(mExistingAlarmTypes & CalEvent::ARCHIVED)) { creator = new CalendarCreator(CalEvent::ARCHIVED, QStringLiteral("expired.ics"), i18nc("@info", "Archived Alarms")); connect(creator, &CalendarCreator::finished, this, &CalendarMigrator::calendarCreated); connect(creator, &CalendarCreator::creating, this, &CalendarMigrator::creatingCalendar); mCalendarsPending << creator; creator->createAgent(KALARM_RESOURCE, this); } if (!(mExistingAlarmTypes & CalEvent::TEMPLATE)) { creator = new CalendarCreator(CalEvent::TEMPLATE, QStringLiteral("template.ics"), i18nc("@info", "Alarm Templates")); connect(creator, &CalendarCreator::finished, this, &CalendarMigrator::calendarCreated); connect(creator, &CalendarCreator::creating, this, &CalendarMigrator::creatingCalendar); mCalendarsPending << creator; creator->createAgent(KALARM_RESOURCE, this); } if (mCalendarsPending.isEmpty()) { mCompleted = true; deleteLater(); } } /****************************************************************************** * Called when a calendar resource is about to be created. * Emits the 'creating' signal. */ void CalendarMigrator::creatingCalendar(const QString& path) { Q_EMIT creating(path, -1, false); } /****************************************************************************** * Called when creation of a migrated or new default calendar resource has * completed or failed. */ void CalendarMigrator::calendarCreated(CalendarCreator* creator) { int i = mCalendarsPending.indexOf(creator); if (i < 0) return; // calendar already finished Q_EMIT creating(creator->path(), creator->collectionId(), true); if (!creator->errorMessage().isEmpty()) { QString errmsg = creator->newCalendar() ? xi18nc("@info/plain", "Failed to create default calendar %1", creator->resourceName()) : xi18nc("@info/plain 'Import Alarms' is the name of a menu option", "Failed to convert old configuration for calendar %1. " "Please use Import Alarms to load its alarms into a new or existing calendar.", creator->resourceName()); const QString locn = i18nc("@info File path or URL", "Location: %1", creator->path()); if (creator->errorMessage().isEmpty()) errmsg = xi18nc("@info", "%1%2", errmsg, locn); else errmsg = xi18nc("@info", "%1%2(%3)", errmsg, locn, creator->errorMessage()); KAMessageBox::error(MainWindow::mainMainWindow(), errmsg); } creator->deleteLater(); mCalendarsPending.removeAt(i); // remove it from the pending list if (mCalendarsPending.isEmpty()) { mCompleted = true; deleteLater(); } } /****************************************************************************** * If an existing Akonadi resource calendar can be converted to the current * KAlarm format, prompt the user whether to convert it, and if yes, tell the * Akonadi resource to update the backend storage to the current format. * The CollectionAttribute's KeepFormat property will be updated if the user * chooses not to update the calendar. * * Note: the collection should be up to date: use AkonadiModel::refresh() before * calling this function. */ void CalendarMigrator::updateToCurrentFormat(const Collection& collection, bool ignoreKeepFormat, QWidget* parent) { - qCDebug(KALARM_LOG) << collection.id(); + qCDebug(KALARM_LOG) << "CalendarMigrator::updateToCurrentFormat:" << collection.id(); if (CalendarUpdater::containsCollection(collection.id())) return; // prevent multiple simultaneous user prompts const AgentInstance agent = AgentManager::self()->instance(collection.resource()); const QString id = agent.type().identifier(); bool dirResource; if (id == KALARM_RESOURCE) dirResource = false; else if (id == KALARM_DIR_RESOURCE) dirResource = true; else { - qCCritical(KALARM_LOG) << "Invalid agent type" << id; + qCCritical(KALARM_LOG) << "CalendarMigrator::updateToCurrentFormat: Invalid agent type" << id; return; } CalendarUpdater* updater = new CalendarUpdater(collection, dirResource, ignoreKeepFormat, false, parent); QTimer::singleShot(0, updater, &CalendarUpdater::update); } QList CalendarUpdater::mInstances; CalendarUpdater::CalendarUpdater(const Collection& collection, bool dirResource, bool ignoreKeepFormat, bool newCollection, QObject* parent) : mCollection(collection), mParent(parent), mDirResource(dirResource), mIgnoreKeepFormat(ignoreKeepFormat), mNewCollection(newCollection), mDuplicate(containsCollection(collection.id())) { mInstances.append(this); } CalendarUpdater::~CalendarUpdater() { mInstances.removeAll(this); } bool CalendarUpdater::containsCollection(Collection::Id id) { for (int i = 0, count = mInstances.count(); i < count; ++i) { if (mInstances[i]->mCollection.id() == id) return true; } return false; } bool CalendarUpdater::update() { - qCDebug(KALARM_LOG) << mCollection.id() << (mDirResource ? "directory" : "file"); + qCDebug(KALARM_LOG) << "CalendarUpdater::update:" << mCollection.id() << (mDirResource ? "directory" : "file"); bool result = true; if (!mDuplicate // prevent concurrent updates && mCollection.hasAttribute()) // must know format to update { const CompatibilityAttribute* compatAttr = mCollection.attribute(); const KACalendar::Compat compatibility = compatAttr->compatibility(); if ((compatibility & ~KACalendar::Converted) // The calendar isn't in the current KAlarm format && !(compatibility & ~(KACalendar::Convertible | KACalendar::Converted))) { // The calendar format is convertible to the current KAlarm format if (!mIgnoreKeepFormat && mCollection.hasAttribute() && mCollection.attribute()->keepFormat()) - qCDebug(KALARM_LOG) << "Not updating format (previous user choice)"; + qCDebug(KALARM_LOG) << "CalendarUpdater::update: Not updating format (previous user choice)"; else { // The user hasn't previously said not to convert it const QString versionString = KAlarmCal::getVersionString(compatAttr->version()); const QString msg = KAlarm::conversionPrompt(mCollection.name(), versionString, false); - qCDebug(KALARM_LOG) << "Version" << versionString; + qCDebug(KALARM_LOG) << "CalendarUpdater::update: Version" << versionString; if (KAMessageBox::warningYesNo(qobject_cast(mParent), msg) != KMessageBox::Yes) result = false; // the user chose not to update the calendar else { // Tell the resource to update the backend storage format QString errmsg; if (!mNewCollection) { // Refetch the collection's details because anything could // have happened since the prompt was first displayed. if (!AkonadiModel::instance()->refresh(mCollection)) errmsg = i18nc("@info", "Invalid collection"); } if (errmsg.isEmpty()) { const AgentInstance agent = AgentManager::self()->instance(mCollection.resource()); if (mDirResource) CalendarMigrator::updateStorageFormat(agent, errmsg, mParent); else CalendarMigrator::updateStorageFormat(agent, errmsg, mParent); } if (!errmsg.isEmpty()) { KAMessageBox::error(MainWindow::mainMainWindow(), xi18nc("@info", "%1(%2)", xi18nc("@info/plain", "Failed to update format of calendar %1", mCollection.name()), errmsg)); } } if (!mNewCollection) { // Record the user's choice of whether to update the calendar const QModelIndex ix = AkonadiModel::instance()->collectionIndex(mCollection); AkonadiModel::instance()->setData(ix, !result, AkonadiModel::KeepFormatRole); } } } } deleteLater(); return result; } /****************************************************************************** * Tell an Akonadi resource to update the backend storage format to the current * KAlarm format. * Reply = true if success; if false, 'errorMessage' contains the error message. */ template bool CalendarMigrator::updateStorageFormat(const AgentInstance& agent, QString& errorMessage, QObject* parent) { - qCDebug(KALARM_LOG); + qCDebug(KALARM_LOG) << "CalendarMigrator::updateStorageFormat"; Interface* iface = getAgentInterface(agent, errorMessage, parent); if (!iface) { - qCDebug(KALARM_LOG) << errorMessage; + qCDebug(KALARM_LOG) << "CalendarMigrator::updateStorageFormat:" << errorMessage; return false; } iface->setUpdateStorageFormat(true); iface->save(); delete iface; - qCDebug(KALARM_LOG) << "true"; + qCDebug(KALARM_LOG) << "CalendarMigrator::updateStorageFormat: true"; return true; } /****************************************************************************** * Create a D-Bus interface to an Akonadi resource. * Reply = interface if success * = 0 if error: 'errorMessage' contains the error message. */ template Interface* CalendarMigrator::getAgentInterface(const AgentInstance& agent, QString& errorMessage, QObject* parent) { Interface* iface = new Interface(QStringLiteral("org.freedesktop.Akonadi.Resource.") + agent.identifier(), QStringLiteral("/Settings"), QDBusConnection::sessionBus(), parent); if (!iface->isValid()) { errorMessage = iface->lastError().message(); - qCDebug(KALARM_LOG) << "D-Bus error accessing resource:" << errorMessage; + qCDebug(KALARM_LOG) << "CalendarMigrator::getAgentInterface: D-Bus error accessing resource:" << errorMessage; delete iface; return nullptr; } return iface; } /****************************************************************************** * Constructor to migrate a KResources calendar, using its parameters. */ CalendarCreator::CalendarCreator(const QString& resourceType, const KConfigGroup& config) : mAlarmType(CalEvent::EMPTY), mNew(false), mFinished(false) { // Read the resource configuration parameters from the config const char* pathKey = nullptr; if (resourceType == QStringLiteral("file")) { mResourceType = LocalFile; pathKey = "CalendarURL"; } else if (resourceType == QStringLiteral("dir")) { mResourceType = LocalDir; pathKey = "CalendarURL"; } else if (resourceType == QStringLiteral("remote")) { mResourceType = RemoteFile; pathKey = "DownloadUrl"; } else { - qCCritical(KALARM_LOG) << "Invalid resource type:" << resourceType; + qCCritical(KALARM_LOG) << "CalendarCreator: Invalid resource type:" << resourceType; return; } const QString path = config.readPathEntry(pathKey, QString()); mUrlString = QUrl::fromUserInput(path).toString(); switch (config.readEntry("AlarmType", (int)0)) { case 1: mAlarmType = CalEvent::ACTIVE; break; case 2: mAlarmType = CalEvent::ARCHIVED; break; case 4: mAlarmType = CalEvent::TEMPLATE; break; default: - qCCritical(KALARM_LOG) << "Invalid alarm type for resource"; + qCCritical(KALARM_LOG) << "CalendarCreator: Invalid alarm type for resource"; return; } mName = config.readEntry("ResourceName", QString()); mColour = config.readEntry("Color", QColor()); mReadOnly = config.readEntry("ResourceIsReadOnly", true); mEnabled = config.readEntry("ResourceIsActive", false); mStandard = config.readEntry("Standard", false); - qCDebug(KALARM_LOG) << "Migrating:" << mName << ", type=" << mAlarmType << ", path=" << mUrlString; + qCDebug(KALARM_LOG) << "CalendarCreator: Migrating:" << mName << ", type=" << mAlarmType << ", path=" << mUrlString; } /****************************************************************************** * Constructor to create a new default local file resource. * This is created as enabled, read-write, and standard for its alarm type. */ CalendarCreator::CalendarCreator(CalEvent::Type alarmType, const QString& file, const QString& name) : mAlarmType(alarmType), mResourceType(LocalFile), mName(name), mColour(), mReadOnly(false), mEnabled(true), mStandard(true), mNew(true), mFinished(false) { const QString path = QStandardPaths::writableLocation(QStandardPaths::DataLocation) + QLatin1Char('/') + file; mUrlString = QUrl::fromLocalFile(path).toString(); - qCDebug(KALARM_LOG) << "New:" << mName << ", type=" << mAlarmType << ", path=" << mUrlString; + qCDebug(KALARM_LOG) << "CalendarCreator: New:" << mName << ", type=" << mAlarmType << ", path=" << mUrlString; } /****************************************************************************** * Create the Akonadi agent for this calendar. */ void CalendarCreator::createAgent(const QString& agentType, QObject* parent) { Q_EMIT creating(mUrlString); AgentInstanceCreateJob* job = new AgentInstanceCreateJob(agentType, parent); connect(job, &KJob::result, this, &CalendarCreator::agentCreated); job->start(); } /****************************************************************************** * Called when the agent creation job for this resource has completed. * Applies the calendar resource configuration to the Akonadi agent. */ void CalendarCreator::agentCreated(KJob* j) { if (j->error()) { mErrorMessage = j->errorString(); - qCCritical(KALARM_LOG) << "AgentInstanceCreateJob error:" << mErrorMessage; + qCCritical(KALARM_LOG) << "CalendarCreator::agentCreated: AgentInstanceCreateJob error:" << mErrorMessage; finish(false); return; } // Configure the Akonadi Agent - qCDebug(KALARM_LOG) << mName; + qCDebug(KALARM_LOG) << "CalendarCreator::agentCreated:" << mName; AgentInstanceCreateJob* job = static_cast(j); mAgent = job->instance(); mAgent.setName(mName); bool ok = false; switch (mResourceType) { case LocalFile: ok = writeLocalFileConfig(); break; case LocalDir: ok = writeLocalDirectoryConfig(); break; case RemoteFile: ok = writeRemoteFileConfig(); break; default: - qCCritical(KALARM_LOG) << "Invalid resource type"; + qCCritical(KALARM_LOG) << "CalendarCreator::agentCreated: Invalid resource type"; break; } if (!ok) { finish(true); return; } mAgent.reconfigure(); // notify the agent that its configuration has been changed // Wait for the resource to create its collection and synchronize the backend storage. ResourceSynchronizationJob* sjob = new ResourceSynchronizationJob(mAgent); connect(sjob, &KJob::result, this, &CalendarCreator::resourceSynchronised); sjob->start(); // this is required (not an Akonadi::Job) } /****************************************************************************** * Called when a resource synchronization job has completed. * Fetches the collection which this agent manages. */ void CalendarCreator::resourceSynchronised(KJob* j) { - qCDebug(KALARM_LOG) << mName; + qCDebug(KALARM_LOG) << "CalendarCreator::resourceSynchronised:" << mName; if (j->error()) { // Don't give up on error - we can still try to fetch the collection qCCritical(KALARM_LOG) << "ResourceSynchronizationJob error: " << j->errorString(); // Try again to synchronize the backend storage. mAgent.synchronize(); } mCollectionFetchRetryCount = 0; fetchCollection(); } /****************************************************************************** * Find the collection which this agent manages. */ void CalendarCreator::fetchCollection() { CollectionFetchJob* job = new CollectionFetchJob(Collection::root(), CollectionFetchJob::FirstLevel); job->fetchScope().setResource(mAgent.identifier()); connect(job, &KJob::result, this, &CalendarCreator::collectionFetchResult); job->start(); } bool CalendarCreator::writeLocalFileConfig() { OrgKdeAkonadiKAlarmSettingsInterface* iface = writeBasicConfig(); if (!iface) return false; iface->setMonitorFile(true); iface->save(); // save the Agent config changes delete iface; return true; } bool CalendarCreator::writeLocalDirectoryConfig() { OrgKdeAkonadiKAlarmDirSettingsInterface* iface = writeBasicConfig(); if (!iface) return false; iface->setMonitorFiles(true); iface->save(); // save the Agent config changes delete iface; return true; } bool CalendarCreator::writeRemoteFileConfig() { OrgKdeAkonadiKAlarmSettingsInterface* iface = writeBasicConfig(); if (!iface) return false; iface->setMonitorFile(true); iface->save(); // save the Agent config changes delete iface; return true; } template Interface* CalendarCreator::writeBasicConfig() { Interface* iface = CalendarMigrator::getAgentInterface(mAgent, mErrorMessage, this); if (iface) { iface->setReadOnly(mReadOnly); iface->setDisplayName(mName); iface->setPath(mUrlString); // this must be a full URL, not a local path iface->setAlarmTypes(CalEvent::mimeTypes(mAlarmType)); iface->setUpdateStorageFormat(false); } return iface; } /****************************************************************************** * Called when a collection fetch job has completed. * Obtains the collection handled by the agent, and configures it. */ void CalendarCreator::collectionFetchResult(KJob* j) { - qCDebug(KALARM_LOG) << mName; + qCDebug(KALARM_LOG) << "CalendarCreator::collectionFetchResult:" << mName; if (j->error()) { mErrorMessage = j->errorString(); - qCCritical(KALARM_LOG) << "CollectionFetchJob error: " << mErrorMessage; + qCCritical(KALARM_LOG) << "CalendarCreator::collectionFetchResult: CollectionFetchJob error: " << mErrorMessage; finish(true); return; } CollectionFetchJob* job = static_cast(j); const Collection::List collections = job->collections(); if (collections.isEmpty()) { if (++mCollectionFetchRetryCount >= 10) { mErrorMessage = i18nc("@info", "New configuration timed out"); - qCCritical(KALARM_LOG) << "Timeout fetching collection for resource"; + qCCritical(KALARM_LOG) << "CalendarCreator::collectionFetchResult: Timeout fetching collection for resource"; finish(true); return; } // Need to wait a bit longer until the resource has initialised and // created its collection. Retry after 200ms. - qCDebug(KALARM_LOG) << "Retrying"; + qCDebug(KALARM_LOG) << "CalendarCreator::collectionFetchResult: Retrying"; QTimer::singleShot(200, this, &CalendarCreator::fetchCollection); return; } if (collections.count() > 1) { mErrorMessage = i18nc("@info", "New configuration was corrupt"); - qCCritical(KALARM_LOG) << "Wrong number of collections for this resource:" << collections.count(); + qCCritical(KALARM_LOG) << "CalendarCreator::collectionFetchResult: Wrong number of collections for this resource:" << collections.count(); finish(true); return; } // Set Akonadi Collection attributes Collection collection = collections[0]; mCollectionId = collection.id(); collection.setContentMimeTypes(CalEvent::mimeTypes(mAlarmType)); EntityDisplayAttribute* dattr = collection.attribute(Collection::AddIfMissing); dattr->setIconName(QStringLiteral("kalarm")); CollectionAttribute* attr = collection.attribute(Collection::AddIfMissing); attr->setEnabled(mEnabled ? mAlarmType : CalEvent::EMPTY); if (mStandard) attr->setStandard(mAlarmType); if (mColour.isValid()) attr->setBackgroundColor(mColour); // Update the calendar to the current KAlarm format if necessary, // and if the user agrees. bool dirResource = false; switch (mResourceType) { case LocalFile: case RemoteFile: break; case LocalDir: dirResource = true; break; default: Q_ASSERT(0); // Invalid resource type break; } bool keep = false; bool duplicate = false; if (!mReadOnly) { CalendarUpdater* updater = new CalendarUpdater(collection, dirResource, false, true, this); duplicate = updater->isDuplicate(); keep = !updater->update(); // note that 'updater' will auto-delete when finished } if (!duplicate) { // Record the user's choice of whether to update the calendar attr->setKeepFormat(keep); } // Update the collection's CollectionAttribute value in the Akonadi database. // 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. Collection c(collection.id()); CollectionAttribute* att = c.attribute(Collection::AddIfMissing); *att = *attr; CollectionModifyJob* cmjob = new CollectionModifyJob(c, this); connect(cmjob, &KJob::result, this, &CalendarCreator::modifyCollectionJobDone); } /****************************************************************************** * Called when a collection modification job has completed. * Checks for any error. */ void CalendarCreator::modifyCollectionJobDone(KJob* j) { Collection collection = static_cast(j)->collection(); if (j->error()) { mErrorMessage = j->errorString(); - qCCritical(KALARM_LOG) << "CollectionFetchJob error: " << mErrorMessage; + qCCritical(KALARM_LOG) << "CalendarCreator::modifyCollectionJobDone: CollectionFetchJob error: " << mErrorMessage; finish(true); } else { - qCDebug(KALARM_LOG) << "Completed:" << mName; + qCDebug(KALARM_LOG) << "CalendarCreator::modifyCollectionJobDone: Completed:" << mName; finish(false); } } /****************************************************************************** * Emit the finished() signal. If 'cleanup' is true, delete the newly created * but incomplete Agent. */ void CalendarCreator::finish(bool cleanup) { if (!mFinished) { if (cleanup) AgentManager::self()->removeInstance(mAgent); mFinished = true; Q_EMIT finished(this); } } #include "calendarmigrator.moc" // vim: et sw=4: diff --git a/src/collectionmodel.cpp b/src/collectionmodel.cpp index 6192961b..1877b717 100644 --- a/src/collectionmodel.cpp +++ b/src/collectionmodel.cpp @@ -1,1339 +1,1339 @@ /* * collectionmodel.cpp - Akonadi collection models * Program: kalarm - * Copyright © 2007-2018 by David Jarvie + * Copyright © 2007-2019 by 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 "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) { 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) { 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) { ++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); connect(mModel, SIGNAL(rowsAboutToBeInserted(QModelIndex,int,int)), SIGNAL(layoutAboutToBeChanged())); 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); // 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(); foreach (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(); foreach (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) << "Enabled" << collection.id(); + qCDebug(KALARM_LOG) << "CollectionCheckListModel::collectionStatusChanged: Enabled" << collection.id(); break; case AkonadiModel::AlarmTypes: - qCDebug(KALARM_LOG) << "AlarmTypes" << collection.id(); + qCDebug(KALARM_LOG) << "CollectionCheckListModel::collectionStatusChanged: 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); } /*============================================================================= = 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) { setDynamicSortFilter(true); 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 (qVariantCanConvert(value)) { 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 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_ViewItemCheckIndicator, &opt).width(); int left = spacing() + 3*margin + checkWidth + viewOptions().decorationSize.width(); // left offset of text 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; 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) { // Initialise the list of enabled collections EntityMimeTypeFilterModel* filter = new EntityMimeTypeFilterModel(this); filter->addMimeTypeInclusionFilter(Collection::mimeType()); filter->setSourceModel(AkonadiModel::instance()); Collection::List collections; findEnabledCollections(filter, QModelIndex(), collections); setCollections(collections); connect(AkonadiModel::instance(), &AkonadiModel::collectionStatusChanged, this, &CollectionControlModel::statusChanged); connect(AkonadiModel::instance(), &EntityTreeModel::collectionTreeFetched, this, &CollectionControlModel::collectionPopulated); connect(AkonadiModel::instance(), &EntityTreeModel::collectionPopulated, this, &CollectionControlModel::collectionPopulated); connect(AkonadiModel::instance(), SIGNAL(serverStopped()), SLOT(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, Collection::List& collections) 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, collections, 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) collections += collection; if (filter->rowCount(ix) > 0) findEnabledCollections(filter, ix, collections); } } 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) << "id:" << collection.id() << ", alarm types" << types << "->" << 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) << "id:" << collection.id() << ", types=" << types; + qCDebug(KALARM_LOG) << "CollectionControlModel::setEnabledStatus:" << collection.id() << ", types=" << types; CalEvent::Types disallowedStdTypes(0); CalEvent::Types stdTypes(0); // Prevent the enabling of duplicate alarm types if another collection // uses the same backend storage. const Collection::List cols = collections(); const CalEvent::Types canEnable = checkTypesToEnable(collection, cols, types); // Update the list of enabled collections if (canEnable) { const QList colIds = collectionIds(); 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) { foreach (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) << "id:" << collection.id() << ", enabled=" << enabled << ", inserted=" << inserted; + 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) << "id:" << collection.id() << ", readOnly=" << readOnly; + 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.) * collections = list of collections 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 Collection::List& collections, CalEvent::Types types) { types &= (CalEvent::ACTIVE | CalEvent::ARCHIVED | CalEvent::TEMPLATE); if (types) { - // At least on alarm type is to be enabled + // At least one alarm type is to be enabled const QUrl location = QUrl::fromUserInput(collection.remoteId(), QString(), QUrl::AssumeLocalFile); foreach (const Collection& c, collections) { const QUrl cLocation = QUrl::fromUserInput(c.remoteId(), QString(), QUrl::AssumeLocalFile); if (c.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) << "Collection" << c.id() << "duplicates backend for" << collection.id(); + 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); 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 'useDefault' is true and there is no standard collection, the only * collection for the mime type will be returned as a default. */ Collection CollectionControlModel::getStandard(CalEvent::Type type, bool useDefault) { const QString mimeType = CalEvent::mimeType(type); int defalt = -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; defalt = (defalt == -1) ? i : -2; } } return (useDefault && defalt >= 0) ? cols[defalt] : Collection(); } /****************************************************************************** * Return whether a collection is the standard collection for a specified * mime type. */ bool CollectionControlModel::isStandard(Akonadi::Collection& collection, CalEvent::Type type) { 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) { CalEvent::Types types; Collection c(colIds[i]); if (colIds[i] == 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) { CalEvent::Types t; Collection c(colIds[i]); if (colIds[i] == 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 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) { Collection c(colIds[i]); 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 Collection::List cols = instance()->collections(); for (int i = 0, count = cols.count(); i < count; ++i) { if (cols[i].resource() == resourceId) return cols[i]; } return Collection(); } /****************************************************************************** * Return whether all enabled collections have been populated. */ bool CollectionControlModel::isPopulated(Collection::Id colId) { AkonadiModel* model = AkonadiModel::instance(); const QList colIds = instance()->collectionIds(); for (int i = 0, count = colIds.count(); i < count; ++i) { if ((colId == -1 || colId == colIds[i]) && !model->data(model->collectionIndex(colIds[i]), AkonadiModel::IsPopulatedRole).toBool()) { Collection c(colIds[i]); 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); + 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; } /****************************************************************************** * 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/collectionsearch.cpp b/src/collectionsearch.cpp index d33fc1eb..7a5786ac 100644 --- a/src/collectionsearch.cpp +++ b/src/collectionsearch.cpp @@ -1,174 +1,174 @@ /* * collectionsearch.cpp - Search Akonadi Collections * Program: kalarm * Copyright © 2014 by 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 "collectionsearch.h" #include #include #include #include #include #include #include #include "kalarm_debug.h" using namespace Akonadi; /****************************************************************************** * Constructor. * Creates jobs to fetch all collections for resources containing the mime type. * Its subsequent actions depend on the parameters: * - If 'remove' is true, it will locate all Items with the specified 'gid' and * delete them. The deleted() signal will be emitted. * - Otherwise, if 'gid' is specified, it will Q_EMIT the signal items() to * notify all Items with that GID. * - Otherwise, it will Q_EMIT the signal collections() to notify all Collections. */ CollectionSearch::CollectionSearch(const QString& mimeType, const QString& gid, bool remove) : mMimeType(mimeType), mGid(gid), mDeleteCount(0), mDelete(remove && !mGid.isEmpty()) { const AgentInstance::List agents = AgentManager::self()->instances(); foreach (const AgentInstance& agent, agents) { if (agent.type().mimeTypes().contains(mimeType)) { { CollectionFetchJob* job = new CollectionFetchJob(Collection::root(), CollectionFetchJob::FirstLevel); job->fetchScope().setResource(agent.identifier()); mCollectionJobs << job; connect(job, &CollectionFetchJob::result, this, &CollectionSearch::collectionFetchResult); } } } if (mCollectionJobs.isEmpty()) { // There are no resources containing the mime type, so ensure that a // signal is emitted after construction. QTimer::singleShot(0, this, &CollectionSearch::finish); } } /****************************************************************************** * Called when a CollectionFetchJob has completed. */ void CollectionSearch::collectionFetchResult(KJob* j) { CollectionFetchJob* job = static_cast(j); if (j->error()) - qCCritical(KALARM_LOG) << "CollectionFetchJob" << job->fetchScope().resource() << "error: " << j->errorString(); + qCCritical(KALARM_LOG) << "CollectionSearch::collectionFetchResult: CollectionFetchJob" << job->fetchScope().resource() << "error: " << j->errorString(); else { const Collection::List collections = job->collections(); foreach (const Collection& c, collections) { if (c.contentMimeTypes().contains(mMimeType)) { if (mGid.isEmpty()) mCollections << c; else { // Search for all Items with the specified GID Item item; item.setGid(mGid); ItemFetchJob* ijob = new ItemFetchJob(item, this); ijob->setCollection(c); mItemFetchJobs[ijob] = c.id(); connect(ijob, &ItemFetchJob::result, this, &CollectionSearch::itemFetchResult); } } } } mCollectionJobs.removeAll(job); if (mCollectionJobs.isEmpty()) { // All collections have now been fetched if (mGid.isEmpty()) finish(); } } /****************************************************************************** * Called when an ItemFetchJob has completed. */ void CollectionSearch::itemFetchResult(KJob* j) { ItemFetchJob* job = static_cast(j); if (j->error()) - qCDebug(KALARM_LOG) << "ItemFetchJob: collection" << mItemFetchJobs[job] << "GID" << mGid << "error: " << j->errorString(); + qCDebug(KALARM_LOG) << "CollectionSearch::itemFetchResult: ItemFetchJob: collection" << mItemFetchJobs[job] << "GID" << mGid << "error: " << j->errorString(); else { if (mDelete) { Item::List items = job->items(); foreach (const Item& item, items) { ItemDeleteJob* djob = new ItemDeleteJob(item, this); mItemDeleteJobs[djob] = mItemFetchJobs[job]; connect(djob, &ItemDeleteJob::result, this, &CollectionSearch::itemDeleteResult); } } else mItems << job->items(); } mItemFetchJobs.remove(job); if (mItemFetchJobs.isEmpty() && mItemDeleteJobs.isEmpty() && mCollectionJobs.isEmpty()) finish(); // all Items have now been fetched or deleted, so notify the result } /****************************************************************************** * Called when an ItemDeleteJob has completed. */ void CollectionSearch::itemDeleteResult(KJob* j) { ItemDeleteJob* job = static_cast(j); if (j->error()) - qCDebug(KALARM_LOG) << "ItemDeleteJob: resource" << mItemDeleteJobs[job] << "GID" << mGid << "error: " << j->errorString(); + qCDebug(KALARM_LOG) << "CollectionSearch::itemDeleteResult: ItemDeleteJob: resource" << mItemDeleteJobs[job] << "GID" << mGid << "error: " << j->errorString(); else ++mDeleteCount; mItemDeleteJobs.remove(job); if (mItemFetchJobs.isEmpty() && mItemDeleteJobs.isEmpty() && mCollectionJobs.isEmpty()) finish(); // all Items have now been deleted, so notify the result } /****************************************************************************** * Notify the result of the search/delete operation, and delete this instance. */ void CollectionSearch::finish() { if (mDelete) Q_EMIT deleted(mDeleteCount); else if (mGid.isEmpty()) Q_EMIT collections(mCollections); else Q_EMIT items(mItems); deleteLater(); } // vim: et sw=4: diff --git a/src/commandoptions.cpp b/src/commandoptions.cpp index cd557bc5..4e726127 100644 --- a/src/commandoptions.cpp +++ b/src/commandoptions.cpp @@ -1,872 +1,872 @@ /* * commandoptions.cpp - extract command line options * Program: kalarm * Copyright © 2001-2018 by 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) { 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]) - qCCritical(KALARM_LOG) << "Command option" << i << "not initialised"; + qCCritical(KALARM_LOG) << "CommandOptions::setOptions: Command option" << i << "not initialised"; else mParser->addOption(*(mOptions[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])) { const QString time = mParser->value(*mOptions[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])); } if (checkCommand(OptEDIT_NEW_PRESET, EDIT_NEW_PRESET)) { mTemplateName = mParser->value(*mOptions[mCommandOpt]); } if (checkCommand(FILE, NEW)) { mEditType = EditAlarmDlg::DISPLAY; mEditAction = KAEvent::FILE; mEditActionSet = true; mText = mParser->value(*mOptions[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(' ')); } if (checkCommand(EXEC, NEW)) { mEditType = EditAlarmDlg::COMMAND; mEditAction = KAEvent::COMMAND; mEditActionSet = true; mText = mParser->value(*mOptions[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) << "Message"; + 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) { QString addr = params[i]; if (!KAMail::checkAddress(addr)) setError(xi18nc("@info:shell", "%1: invalid email address", optionName(MAIL))); KCalCore::Person::Ptr person(new KCalCore::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 args = mParser->positionalArguments(); if (!args.empty()) mText = args[0]; } if (mParser->isSet(*mOptions[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])) setErrorIncompatible(DISABLE, mCommandOpt); // Fall through to NEW case NEW: { // Display a message or file, execute a command, or send an email if (mParser->isSet(*mOptions[COLOUR])) { // Background colour is specified QString colourText = mParser->value(*mOptions[COLOUR]); if (colourText[0] == QLatin1Char('0') && colourText[1].toLower() == QLatin1Char('x')) colourText.replace(0, 2, QStringLiteral("#")); mBgColour.setNamedColor(colourText); if (!mBgColour.isValid()) setErrorParameter(COLOUR); } if (mParser->isSet(*mOptions[COLOURFG])) { // Foreground colour is specified QString colourText = mParser->value(*mOptions[COLOURFG]); if (colourText[0] == QLatin1Char('0') && colourText[1].toLower() == QLatin1Char('x')) colourText.replace(0, 2, QStringLiteral("#")); mFgColour.setNamedColor(colourText); if (!mFgColour.isValid()) setErrorParameter(COLOURFG); } if (mParser->isSet(*mOptions[TIME])) { QByteArray dateTime = mParser->value(*mOptions[TIME]).toLocal8Bit(); if (!AlarmTime::convertTimeString(dateTime, mAlarmTime)) setErrorParameter(TIME); } else mAlarmTime = KADateTime::currentLocalDateTime(); bool haveRecurrence = mParser->isSet(*mOptions[RECURRENCE]); if (haveRecurrence) { if (mParser->isSet(*mOptions[LOGIN])) setErrorIncompatible(LOGIN, RECURRENCE); else if (mParser->isSet(*mOptions[UNTIL])) setErrorIncompatible(UNTIL, RECURRENCE); QString rule = mParser->value(*mOptions[RECURRENCE]); mRecurrence = new KARecurrence; mRecurrence->set(rule); } if (mParser->isSet(*mOptions[INTERVAL])) { // Repeat count is specified int count = 0; KADateTime endTime; if (mParser->isSet(*mOptions[LOGIN])) setErrorIncompatible(LOGIN, INTERVAL); bool ok; if (mParser->isSet(*mOptions[REPEAT])) { count = mParser->value(*mOptions[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])) { count = 0; QByteArray dateTime = mParser->value(*mOptions[UNTIL]).toLocal8Bit(); bool ok; if (mParser->isSet(*mOptions[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)) 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 = KCalCore::Duration(intervalOfType * 60); KCalCore::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])) setErrorRequires(REPEAT, INTERVAL); else if (mParser->isSet(*mOptions[UNTIL])) setErrorRequires(UNTIL, INTERVAL); } bool audioRepeat = mParser->isSet(*mOptions[PLAY_REPEAT]); if (audioRepeat || mParser->isSet(*mOptions[PLAY])) { // Play a sound with the alarm Option opt = audioRepeat ? PLAY_REPEAT : PLAY; if (audioRepeat && mParser->isSet(*mOptions[PLAY])) setErrorIncompatible(PLAY, PLAY_REPEAT); if (mParser->isSet(*mOptions[BEEP])) setErrorIncompatible(BEEP, opt); else if (mParser->isSet(*mOptions[SPEAK])) setErrorIncompatible(SPEAK, opt); mAudioFile = mParser->value(*mOptions[audioRepeat ? PLAY_REPEAT : PLAY]); if (mParser->isSet(*mOptions[VOLUME])) { bool ok; int volumepc = mParser->value(*mOptions[VOLUME]).toInt(&ok); if (!ok || volumepc < 0 || volumepc > 100) setErrorParameter(VOLUME); mAudioVolume = static_cast(volumepc) / 100; } } else if (mParser->isSet(*mOptions[VOLUME])) setErrorRequires(VOLUME, PLAY, PLAY_REPEAT); if (mParser->isSet(*mOptions[SPEAK])) { if (mParser->isSet(*mOptions[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) { // Issue a reminder alarm in advance of or after the main alarm if (onceOnly && mParser->isSet(*mOptions[REMINDER])) setErrorIncompatible(REMINDER, REMINDER_ONCE); Option opt = onceOnly ? REMINDER_ONCE : REMINDER; KARecurrence::Type recurType; QString optval = mParser->value(*mOptions[onceOnly ? REMINDER_ONCE : REMINDER]); bool after = (optval[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])) { KARecurrence::Type recurType; bool ok = convInterval(mParser->value(*mOptions[LATE_CANCEL]), recurType, mLateCancel); if (!ok) setErrorParameter(LATE_CANCEL); } else if (mParser->isSet(*mOptions[AUTO_CLOSE])) setErrorRequires(AUTO_CLOSE, LATE_CANCEL); if (mParser->isSet(*mOptions[ACK_CONFIRM])) mFlags |= KAEvent::CONFIRM_ACK; if (mParser->isSet(*mOptions[AUTO_CLOSE])) mFlags |= KAEvent::AUTO_CLOSE; if (mParser->isSet(*mOptions[BEEP])) mFlags |= KAEvent::BEEP; if (mParser->isSet(*mOptions[SPEAK])) mFlags |= KAEvent::SPEAK; if (mParser->isSet(*mOptions[KORGANIZER])) mFlags |= KAEvent::COPY_KORGANIZER; if (mParser->isSet(*mOptions[DISABLE])) mFlags |= KAEvent::DISABLED; if (audioRepeat) mFlags |= KAEvent::REPEAT_SOUND; if (mParser->isSet(*mOptions[LOGIN])) mFlags |= KAEvent::REPEAT_AT_LOGIN; if (mParser->isSet(*mOptions[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) << "Interactive"; + qCDebug(KALARM_LOG) << "CommandOptions::process: Interactive"; QStringList errors; if (mParser->isSet(*mOptions[ACK_CONFIRM])) errors << optionName(ACK_CONFIRM); if (mParser->isSet(*mOptions[ATTACH])) errors << optionName(ATTACH); if (mParser->isSet(*mOptions[AUTO_CLOSE])) errors << optionName(AUTO_CLOSE); if (mParser->isSet(*mOptions[BCC])) errors << optionName(BCC); if (mParser->isSet(*mOptions[BEEP])) errors << optionName(BEEP); if (mParser->isSet(*mOptions[COLOUR])) errors << optionName(COLOUR); if (mParser->isSet(*mOptions[COLOURFG])) errors << optionName(COLOURFG); if (mParser->isSet(*mOptions[DISABLE])) errors << optionName(DISABLE); if (mParser->isSet(*mOptions[FROM_ID])) errors << optionName(FROM_ID); if (mParser->isSet(*mOptions[KORGANIZER])) errors << optionName(KORGANIZER); if (mParser->isSet(*mOptions[LATE_CANCEL])) errors << optionName(LATE_CANCEL); if (mParser->isSet(*mOptions[LOGIN])) errors << optionName(LOGIN); if (mParser->isSet(*mOptions[PLAY])) errors << optionName(PLAY); if (mParser->isSet(*mOptions[PLAY_REPEAT])) errors << optionName(PLAY_REPEAT); if (mParser->isSet(*mOptions[REMINDER])) errors << optionName(REMINDER); if (mParser->isSet(*mOptions[REMINDER_ONCE])) errors << optionName(REMINDER_ONCE); if (mParser->isSet(*mOptions[SPEAK])) errors << optionName(SPEAK); if (mParser->isSet(*mOptions[SUBJECT])) errors << optionName(SUBJECT); if (mParser->isSet(*mOptions[TIME])) errors << optionName(TIME); if (mParser->isSet(*mOptions[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) << 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(); } /****************************************************************************** * 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])) return false; if (mCommand != NONE && (allowedEditType == EditAlarmDlg::NO_TYPE || (allowedEditType != EditAlarmDlg::NO_TYPE && (mCommand != NEW || mEditType != allowedEditType)))) setErrorIncompatible(mCommandOpt, command); - qCDebug(KALARM_LOG).nospace() << optionName(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 && ((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(); if (names.empty()) return QString(); for (int i = 0; i < names.size(); ++i) { if (shortName && names[i].size() == 1) return QStringLiteral("-") + names[i]; else if (!shortName && names[i].size() > 1) return QStringLiteral("--") + names[i]; } 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]) { 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 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/dbushandler.cpp b/src/dbushandler.cpp index fd1e1ab5..b061133e 100644 --- a/src/dbushandler.cpp +++ b/src/dbushandler.cpp @@ -1,579 +1,579 @@ /* * dbushandler.cpp - handler for D-Bus calls by other applications * Program: kalarm * Copyright © 2002-2012,2018 by 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 "alarmtime.h" #include "functions.h" #include "kalarmapp.h" #include "kamail.h" #include "mainwindow.h" #include "preferences.h" #include "dbushandler.h" #include #include #include #include using namespace KCalCore; #include #include "kalarm_debug.h" #include namespace { const QString REQUEST_DBUS_OBJECT(QStringLiteral("/kalarm")); // D-Bus object path of KAlarm's request interface } /*============================================================================= = DBusHandler = This class's function is to handle D-Bus requests by other applications. =============================================================================*/ DBusHandler::DBusHandler() { - qCDebug(KALARM_LOG); + qCDebug(KALARM_LOG) << "DBusHandler:"; new KalarmAdaptor(this); QDBusConnection::sessionBus().registerObject(REQUEST_DBUS_OBJECT, this); } bool DBusHandler::cancelEvent(const QString& eventId) { return theApp()->dbusDeleteEvent(EventId(eventId)); } bool DBusHandler::triggerEvent(const QString& eventId) { return theApp()->dbusTriggerEvent(EventId(eventId)); } QString DBusHandler::list() { return theApp()->dbusList(); } bool DBusHandler::scheduleMessage(const QString& message, const QString& startDateTime, int lateCancel, unsigned flags, const QString& bgColor, const QString& fgColor, const QString& font, const QString& audioUrl, int reminderMins, const QString& recurrence, int subRepeatInterval, int subRepeatCount) { KADateTime start; KARecurrence recur; Duration subRepeatDuration; if (!convertRecurrence(start, recur, startDateTime, recurrence, subRepeatInterval, subRepeatDuration)) return false; return scheduleMessage(message, start, lateCancel, flags, bgColor, fgColor, font, QUrl::fromUserInput(audioUrl, QString(), QUrl::AssumeLocalFile), reminderMins, recur, subRepeatDuration, subRepeatCount); } bool DBusHandler::scheduleMessage(const QString& message, const QString& startDateTime, int lateCancel, unsigned flags, const QString& bgColor, const QString& fgColor, const QString& font, const QString& audioUrl, int reminderMins, int recurType, int recurInterval, int recurCount) { KADateTime start; KARecurrence recur; if (!convertRecurrence(start, recur, startDateTime, recurType, recurInterval, recurCount)) return false; return scheduleMessage(message, start, lateCancel, flags, bgColor, fgColor, font, QUrl::fromUserInput(audioUrl, QString(), QUrl::AssumeLocalFile), reminderMins, recur); } bool DBusHandler::scheduleMessage(const QString& message, const QString& startDateTime, int lateCancel, unsigned flags, const QString& bgColor, const QString& fgColor, const QString& font, const QString& audioUrl, int reminderMins, int recurType, int recurInterval, const QString& endDateTime) { KADateTime start; KARecurrence recur; if (!convertRecurrence(start, recur, startDateTime, recurType, recurInterval, endDateTime)) return false; return scheduleMessage(message, start, lateCancel, flags, bgColor, fgColor, font, QUrl::fromUserInput(audioUrl, QString(), QUrl::AssumeLocalFile), reminderMins, recur); } bool DBusHandler::scheduleFile(const QString& url, const QString& startDateTime, int lateCancel, unsigned flags, const QString& bgColor, const QString& audioUrl, int reminderMins, const QString& recurrence, int subRepeatInterval, int subRepeatCount) { KADateTime start; KARecurrence recur; Duration subRepeatDuration; if (!convertRecurrence(start, recur, startDateTime, recurrence, subRepeatInterval, subRepeatDuration)) return false; return scheduleFile(QUrl::fromUserInput(url, QString(), QUrl::AssumeLocalFile), start, lateCancel, flags, bgColor, QUrl::fromUserInput(audioUrl, QString(), QUrl::AssumeLocalFile), reminderMins, recur, subRepeatDuration, subRepeatCount); } bool DBusHandler::scheduleFile(const QString& url, const QString& startDateTime, int lateCancel, unsigned flags, const QString& bgColor, const QString& audioUrl, int reminderMins, int recurType, int recurInterval, int recurCount) { KADateTime start; KARecurrence recur; if (!convertRecurrence(start, recur, startDateTime, recurType, recurInterval, recurCount)) return false; return scheduleFile(QUrl::fromUserInput(url, QString(), QUrl::AssumeLocalFile), start, lateCancel, flags, bgColor, QUrl::fromUserInput(audioUrl, QString(), QUrl::AssumeLocalFile), reminderMins, recur); } bool DBusHandler::scheduleFile(const QString& url, const QString& startDateTime, int lateCancel, unsigned flags, const QString& bgColor, const QString& audioUrl, int reminderMins, int recurType, int recurInterval, const QString& endDateTime) { KADateTime start; KARecurrence recur; if (!convertRecurrence(start, recur, startDateTime, recurType, recurInterval, endDateTime)) return false; return scheduleFile(QUrl::fromUserInput(url, QString(), QUrl::AssumeLocalFile), start, lateCancel, flags, bgColor, QUrl::fromUserInput(audioUrl, QString(), QUrl::AssumeLocalFile), reminderMins, recur); } bool DBusHandler::scheduleCommand(const QString& commandLine, const QString& startDateTime, int lateCancel, unsigned flags, const QString& recurrence, int subRepeatInterval, int subRepeatCount) { KADateTime start; KARecurrence recur; Duration subRepeatDuration; if (!convertRecurrence(start, recur, startDateTime, recurrence, subRepeatInterval, subRepeatDuration)) return false; return scheduleCommand(commandLine, start, lateCancel, flags, recur, subRepeatDuration, subRepeatCount); } bool DBusHandler::scheduleCommand(const QString& commandLine, const QString& startDateTime, int lateCancel, unsigned flags, int recurType, int recurInterval, int recurCount) { KADateTime start = convertDateTime(startDateTime); if (!start.isValid()) return false; KARecurrence recur; if (!convertRecurrence(start, recur, startDateTime, recurType, recurInterval, recurCount)) return false; return scheduleCommand(commandLine, start, lateCancel, flags, recur); } bool DBusHandler::scheduleCommand(const QString& commandLine, const QString& startDateTime, int lateCancel, unsigned flags, int recurType, int recurInterval, const QString& endDateTime) { KADateTime start; KARecurrence recur; if (!convertRecurrence(start, recur, startDateTime, recurType, recurInterval, endDateTime)) return false; return scheduleCommand(commandLine, start, lateCancel, flags, recur); } bool DBusHandler::scheduleEmail(const QString& fromID, const QString& addresses, const QString& subject, const QString& message, const QString& attachments, const QString& startDateTime, int lateCancel, unsigned flags, const QString& recurrence, int subRepeatInterval, int subRepeatCount) { KADateTime start; KARecurrence recur; Duration subRepeatDuration; if (!convertRecurrence(start, recur, startDateTime, recurrence, subRepeatInterval, subRepeatDuration)) return false; return scheduleEmail(fromID, addresses, subject, message, attachments, start, lateCancel, flags, recur, subRepeatDuration, subRepeatCount); } bool DBusHandler::scheduleEmail(const QString& fromID, const QString& addresses, const QString& subject, const QString& message, const QString& attachments, const QString& startDateTime, int lateCancel, unsigned flags, int recurType, int recurInterval, int recurCount) { KADateTime start; KARecurrence recur; if (!convertRecurrence(start, recur, startDateTime, recurType, recurInterval, recurCount)) return false; return scheduleEmail(fromID, addresses, subject, message, attachments, start, lateCancel, flags, recur); } bool DBusHandler::scheduleEmail(const QString& fromID, const QString& addresses, const QString& subject, const QString& message, const QString& attachments, const QString& startDateTime, int lateCancel, unsigned flags, int recurType, int recurInterval, const QString& endDateTime) { KADateTime start; KARecurrence recur; if (!convertRecurrence(start, recur, startDateTime, recurType, recurInterval, endDateTime)) return false; return scheduleEmail(fromID, addresses, subject, message, attachments, start, lateCancel, flags, recur); } bool DBusHandler::scheduleAudio(const QString& audioUrl, int volumePercent, const QString& startDateTime, int lateCancel, unsigned flags, const QString& recurrence, int subRepeatInterval, int subRepeatCount) { KADateTime start; KARecurrence recur; Duration subRepeatDuration; if (!convertRecurrence(start, recur, startDateTime, recurrence, subRepeatInterval, subRepeatDuration)) return false; return scheduleAudio(audioUrl, volumePercent, start, lateCancel, flags, recur, subRepeatDuration, subRepeatCount); } bool DBusHandler::scheduleAudio(const QString& audioUrl, int volumePercent, const QString& startDateTime, int lateCancel, unsigned flags, int recurType, int recurInterval, int recurCount) { KADateTime start = convertDateTime(startDateTime); if (!start.isValid()) return false; KARecurrence recur; if (!convertRecurrence(start, recur, startDateTime, recurType, recurInterval, recurCount)) return false; return scheduleAudio(audioUrl, volumePercent, start, lateCancel, flags, recur); } bool DBusHandler::scheduleAudio(const QString& audioUrl, int volumePercent, const QString& startDateTime, int lateCancel, unsigned flags, int recurType, int recurInterval, const QString& endDateTime) { KADateTime start; KARecurrence recur; if (!convertRecurrence(start, recur, startDateTime, recurType, recurInterval, endDateTime)) return false; return scheduleAudio(audioUrl, volumePercent, start, lateCancel, flags, recur); } bool DBusHandler::edit(const QString& eventID) { return KAlarm::editAlarmById(EventId(eventID)); } bool DBusHandler::editNew(int type) { EditAlarmDlg::Type dlgtype; switch (type) { case DISPLAY: dlgtype = EditAlarmDlg::DISPLAY; break; case COMMAND: dlgtype = EditAlarmDlg::COMMAND; break; case EMAIL: dlgtype = EditAlarmDlg::EMAIL; break; case AUDIO: dlgtype = EditAlarmDlg::AUDIO; break; default: qCCritical(KALARM_LOG) << "D-Bus call: invalid alarm type:" << type; return false; } KAlarm::editNewAlarm(dlgtype); return true; } bool DBusHandler::editNew(const QString& templateName) { return KAlarm::editNewAlarm(templateName); } /****************************************************************************** * Schedule a message alarm, after converting the parameters from strings. */ bool DBusHandler::scheduleMessage(const QString& message, const KADateTime& start, int lateCancel, unsigned flags, const QString& bgColor, const QString& fgColor, const QString& fontStr, const QUrl& audioFile, int reminderMins, const KARecurrence& recurrence, const KCalCore::Duration& subRepeatDuration, int subRepeatCount) { KAEvent::Flags kaEventFlags = convertStartFlags(start, flags); KAEvent::SubAction action = (kaEventFlags & KAEvent::DISPLAY_COMMAND) ? KAEvent::COMMAND : KAEvent::MESSAGE; QColor bg = convertBgColour(bgColor); if (!bg.isValid()) return false; QColor fg; if (fgColor.isEmpty()) fg = Preferences::defaultFgColour(); else { fg.setNamedColor(fgColor); if (!fg.isValid()) { qCCritical(KALARM_LOG) << "D-Bus call: invalid foreground color:" << fgColor; return false; } } QFont font; if (fontStr.isEmpty()) kaEventFlags |= KAEvent::DEFAULT_FONT; else { if (!font.fromString(fontStr)) // N.B. this doesn't do good validation { qCCritical(KALARM_LOG) << "D-Bus call: invalid font:" << fontStr; return false; } } return theApp()->scheduleEvent(action, message, start, lateCancel, kaEventFlags, bg, fg, font, audioFile.toString(), -1, reminderMins, recurrence, subRepeatDuration, subRepeatCount); } /****************************************************************************** * Schedule a file alarm, after converting the parameters from strings. */ bool DBusHandler::scheduleFile(const QUrl& file, const KADateTime& start, int lateCancel, unsigned flags, const QString& bgColor, const QUrl& audioFile, int reminderMins, const KARecurrence& recurrence, const KCalCore::Duration& subRepeatDuration, int subRepeatCount) { KAEvent::Flags kaEventFlags = convertStartFlags(start, flags); QColor bg = convertBgColour(bgColor); if (!bg.isValid()) return false; return theApp()->scheduleEvent(KAEvent::FILE, file.toString(), start, lateCancel, kaEventFlags, bg, Qt::black, QFont(), audioFile.toString(), -1, reminderMins, recurrence, subRepeatDuration, subRepeatCount); } /****************************************************************************** * Schedule a command alarm, after converting the parameters from strings. */ bool DBusHandler::scheduleCommand(const QString& commandLine, const KADateTime& start, int lateCancel, unsigned flags, const KARecurrence& recurrence, const KCalCore::Duration& subRepeatDuration, int subRepeatCount) { KAEvent::Flags kaEventFlags = convertStartFlags(start, flags); return theApp()->scheduleEvent(KAEvent::COMMAND, commandLine, start, lateCancel, kaEventFlags, Qt::black, Qt::black, QFont(), QString(), -1, 0, recurrence, subRepeatDuration, subRepeatCount); } /****************************************************************************** * Schedule an email alarm, after validating the addresses and attachments. */ bool DBusHandler::scheduleEmail(const QString& fromID, const QString& addresses, const QString& subject, const QString& message, const QString& attachments, const KADateTime& start, int lateCancel, unsigned flags, const KARecurrence& recurrence, const KCalCore::Duration& subRepeatDuration, int subRepeatCount) { KAEvent::Flags kaEventFlags = convertStartFlags(start, flags); uint senderId = 0; if (!fromID.isEmpty()) { senderId = Identities::identityUoid(fromID); if (!senderId) { qCCritical(KALARM_LOG) << "D-Bus call scheduleEmail(): unknown sender ID:" << fromID; return false; } } KCalCore::Person::List addrs; QString bad = KAMail::convertAddresses(addresses, addrs); if (!bad.isEmpty()) { qCCritical(KALARM_LOG) << "D-Bus call scheduleEmail(): invalid email addresses:" << bad; return false; } if (addrs.isEmpty()) { qCCritical(KALARM_LOG) << "D-Bus call scheduleEmail(): no email address"; return false; } QStringList atts; bad = KAMail::convertAttachments(attachments, atts); if (!bad.isEmpty()) { qCCritical(KALARM_LOG) << "D-Bus call scheduleEmail(): invalid email attachment:" << bad; return false; } return theApp()->scheduleEvent(KAEvent::EMAIL, message, start, lateCancel, kaEventFlags, Qt::black, Qt::black, QFont(), QString(), -1, 0, recurrence, subRepeatDuration, subRepeatCount, senderId, addrs, subject, atts); } /****************************************************************************** * Schedule a audio alarm, after converting the parameters from strings. */ bool DBusHandler::scheduleAudio(const QString& audioUrl, int volumePercent, const KADateTime& start, int lateCancel, unsigned flags, const KARecurrence& recurrence, const KCalCore::Duration& subRepeatDuration, int subRepeatCount) { KAEvent::Flags kaEventFlags = convertStartFlags(start, flags); float volume = (volumePercent >= 0) ? volumePercent / 100.0f : -1; return theApp()->scheduleEvent(KAEvent::AUDIO, QString(), start, lateCancel, kaEventFlags, Qt::black, Qt::black, QFont(), audioUrl, volume, 0, recurrence, subRepeatDuration, subRepeatCount); } /****************************************************************************** * Convert the start date/time string to a KADateTime. The date/time string is in * the format YYYY-MM-DD[THH:MM[:SS]][ TZ] or [T]HH:MM[:SS]. * The time zone specifier (TZ) is a system time zone name, e.g. "Europe/London". * If no time zone is specified, it defaults to the local clock time (which is * not the same as the local time zone). */ KADateTime DBusHandler::convertDateTime(const QString& dateTime, const KADateTime& defaultDt) { int i = dateTime.indexOf(QLatin1Char(' ')); QString dtString = dateTime.left(i); QString zone = dateTime.mid(i); QDate date; QTime time; bool haveTime = true; bool error = false; if (dtString.length() > 10) { // Both a date and a time are specified QDateTime dt = QDateTime::fromString(dtString, Qt::ISODate); error = !dt.isValid(); date = dt.date(); time = dt.time(); } else { // Check whether a time is specified QString t; if (dtString[0] == QLatin1Char('T')) t = dtString.mid(1); // it's a time: remove the leading 'T' else if (!dtString[2].isDigit()) t = dtString; // it's a time with no leading 'T' if (t.isEmpty()) { // It's a date only date = QDate::fromString(dtString, Qt::ISODate); error = !date.isValid(); haveTime = false; } else { // It's a time only time = QTime::fromString(t, Qt::ISODate); error = !time.isValid(); } } KADateTime result; if (!error) result = AlarmTime::applyTimeZone(zone, date, time, haveTime, defaultDt); if (error || !result.isValid()) { if (!defaultDt.isValid()) qCCritical(KALARM_LOG) << "D-Bus call: invalid start date/time: '" << dateTime << "'"; else qCCritical(KALARM_LOG) << "D-Bus call: invalid recurrence end date/time: '" << dateTime << "'"; } return result; } /****************************************************************************** * Convert the flag bits to KAEvent flag bits. */ KAEvent::Flags DBusHandler::convertStartFlags(const KADateTime& start, unsigned flags) { KAEvent::Flags kaEventFlags = {}; if (flags & REPEAT_AT_LOGIN) kaEventFlags |= KAEvent::REPEAT_AT_LOGIN; if (flags & BEEP) kaEventFlags |= KAEvent::BEEP; if (flags & SPEAK) kaEventFlags |= KAEvent::SPEAK; if (flags & CONFIRM_ACK) kaEventFlags |= KAEvent::CONFIRM_ACK; if (flags & REPEAT_SOUND) kaEventFlags |= KAEvent::REPEAT_SOUND; if (flags & AUTO_CLOSE) kaEventFlags |= KAEvent::AUTO_CLOSE; if (flags & EMAIL_BCC) kaEventFlags |= KAEvent::EMAIL_BCC; if (flags & DISPLAY_COMMAND) kaEventFlags |= KAEvent::DISPLAY_COMMAND; if (flags & SCRIPT) kaEventFlags |= KAEvent::SCRIPT; if (flags & EXEC_IN_XTERM) kaEventFlags |= KAEvent::EXEC_IN_XTERM; if (flags & SHOW_IN_KORG) kaEventFlags |= KAEvent::COPY_KORGANIZER; if (flags & EXCL_HOLIDAYS) kaEventFlags |= KAEvent::EXCL_HOLIDAYS; if (flags & WORK_TIME_ONLY) kaEventFlags |= KAEvent::WORK_TIME_ONLY; if (flags & DISABLED) kaEventFlags |= KAEvent::DISABLED; if (start.isDateOnly()) kaEventFlags |= KAEvent::ANY_TIME; return kaEventFlags; } /****************************************************************************** * Convert the background colour string to a QColor. */ QColor DBusHandler::convertBgColour(const QString& bgColor) { if (bgColor.isEmpty()) return Preferences::defaultBgColour(); QColor bg(bgColor); if (!bg.isValid()) qCCritical(KALARM_LOG) << "D-Bus call: invalid background color:" << bgColor; return bg; } bool DBusHandler::convertRecurrence(KADateTime& start, KARecurrence& recurrence, const QString& startDateTime, const QString& icalRecurrence, int subRepeatInterval, KCalCore::Duration& subRepeatDuration) { start = convertDateTime(startDateTime); if (!start.isValid()) return false; if (!recurrence.set(icalRecurrence)) return false; if (subRepeatInterval && recurrence.type() == KARecurrence::NO_RECUR) { subRepeatInterval = 0; qCWarning(KALARM_LOG) << "D-Bus call: no recurrence specified, so sub-repetition ignored"; } if (subRepeatInterval && !(subRepeatInterval % (24*60))) subRepeatDuration = Duration(subRepeatInterval / (24*60), Duration::Days); else subRepeatDuration = Duration(subRepeatInterval * 60, Duration::Seconds); return true; } bool DBusHandler::convertRecurrence(KADateTime& start, KARecurrence& recurrence, const QString& startDateTime, int recurType, int recurInterval, int recurCount) { start = convertDateTime(startDateTime); if (!start.isValid()) return false; return convertRecurrence(recurrence, start, recurType, recurInterval, recurCount, KADateTime()); } bool DBusHandler::convertRecurrence(KADateTime& start, KARecurrence& recurrence, const QString& startDateTime, int recurType, int recurInterval, const QString& endDateTime) { start = convertDateTime(startDateTime); if (!start.isValid()) return false; KADateTime end = convertDateTime(endDateTime, start); if (end.isDateOnly() && !start.isDateOnly()) { qCCritical(KALARM_LOG) << "D-Bus call: alarm is date-only, but recurrence end is date/time"; return false; } if (!end.isDateOnly() && start.isDateOnly()) { qCCritical(KALARM_LOG) << "D-Bus call: alarm is timed, but recurrence end is date-only"; return false; } return convertRecurrence(recurrence, start, recurType, recurInterval, 0, end); } bool DBusHandler::convertRecurrence(KARecurrence& recurrence, const KADateTime& start, int recurType, int recurInterval, int recurCount, const KADateTime& end) { KARecurrence::Type type; switch (recurType) { case MINUTELY: type = KARecurrence::MINUTELY; break; case DAILY: type = KARecurrence::DAILY; break; case WEEKLY: type = KARecurrence::WEEKLY; break; case MONTHLY: type = KARecurrence::MONTHLY_DAY; break; case YEARLY: type = KARecurrence::ANNUAL_DATE; break; default: qCCritical(KALARM_LOG) << "D-Bus call: invalid recurrence type:" << recurType; return false; } recurrence.set(type, recurInterval, recurCount, start, end); return true; } // vim: et sw=4: diff --git a/src/editdlg.cpp b/src/editdlg.cpp index 2cc2f7dc..4c030356 100644 --- a/src/editdlg.cpp +++ b/src/editdlg.cpp @@ -1,1496 +1,1496 @@ /* * editdlg.cpp - dialog to create or modify an alarm or alarm template * Program: kalarm * Copyright © 2001-2018 by 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 "editdlg.h" #include "editdlg_p.h" #include "editdlgtypes.h" #include "alarmcalendar.h" #include "collectionmodel.h" #include "alarmtimewidget.h" #include "autoqpointer.h" #include "buttongroup.h" #include "checkbox.h" #include "deferdlg.h" #include "functions.h" #include "kalarmapp.h" #include "latecancel.h" #include "lineedit.h" #include "mainwindow.h" #include "messagebox.h" #include "packedlayout.h" #include "preferences.h" #include "radiobutton.h" #include "recurrenceedit.h" #include "reminder.h" #include "shellprocess.h" #include "spinbox.h" #include "stackedwidgets.h" #include "templatepickdlg.h" #include "timeedit.h" #include "timespinbox.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "kalarm_debug.h" using namespace KCal; using namespace KAlarmCal; static const char EDIT_DIALOG_NAME[] = "EditDialog"; static const char TEMPLATE_DIALOG_NAME[] = "EditTemplateDialog"; static const char EDIT_MORE_GROUP[] = "ShowOpts"; static const char EDIT_MORE_KEY[] = "EditMore"; static const int maxDelayTime = 99*60 + 59; // < 100 hours inline QString recurText(const KAEvent& event) { QString r; if (event.repetition()) r = QStringLiteral("%1 / %2").arg(event.recurrenceText()).arg(event.repetitionText()); else r = event.recurrenceText(); return i18nc("@title:tab", "Recurrence - [%1]", r); } QList EditAlarmDlg::mWindowList; // Collect these widget labels together to ensure consistent wording and // translations across different modules. QString EditAlarmDlg::i18n_chk_ShowInKOrganizer() { return i18nc("@option:check", "Show in KOrganizer"); } EditAlarmDlg* EditAlarmDlg::create(bool Template, Type type, QWidget* parent, GetResourceType getResource) { - qCDebug(KALARM_LOG); + qCDebug(KALARM_LOG) << "EditAlarmDlg::create"; switch (type) { case DISPLAY: return new EditDisplayAlarmDlg(Template, parent, getResource); case COMMAND: return new EditCommandAlarmDlg(Template, parent, getResource); case EMAIL: return new EditEmailAlarmDlg(Template, parent, getResource); case AUDIO: return new EditAudioAlarmDlg(Template, parent, getResource); default: break; } return nullptr; } EditAlarmDlg* EditAlarmDlg::create(bool Template, const KAEvent* event, bool newAlarm, QWidget* parent, GetResourceType getResource, bool readOnly) { switch (event->actionTypes()) { case KAEvent::ACT_COMMAND: return new EditCommandAlarmDlg(Template, event, newAlarm, parent, getResource, readOnly); case KAEvent::ACT_DISPLAY_COMMAND: case KAEvent::ACT_DISPLAY: return new EditDisplayAlarmDlg(Template, event, newAlarm, parent, getResource, readOnly); case KAEvent::ACT_EMAIL: return new EditEmailAlarmDlg(Template, event, newAlarm, parent, getResource, readOnly); case KAEvent::ACT_AUDIO: return new EditAudioAlarmDlg(Template, event, newAlarm, parent, getResource, readOnly); default: break; } return nullptr; } /****************************************************************************** * Constructor. * Parameters: * Template = true to edit/create an alarm template * = false to edit/create an alarm. * event != to initialise the dialog to show the specified event's data. */ EditAlarmDlg::EditAlarmDlg(bool Template, KAEvent::SubAction action, QWidget* parent, GetResourceType getResource) : QDialog(parent), mAlarmType(action), mLoadTemplateButton(nullptr), mMainPageShown(false), mRecurPageShown(false), mRecurSetDefaultEndDate(true), mTemplateName(nullptr), mDeferGroup(nullptr), mDeferChangeButton(nullptr), mTimeWidget(nullptr), mShowInKorganizer(nullptr), mDeferGroupHeight(0), mTemplate(Template), mNewAlarm(true), mDesiredReadOnly(false), mReadOnly(false), mShowingMore(true), mSavedEvent(nullptr) { init(nullptr, getResource); mWindowList.append(this); } EditAlarmDlg::EditAlarmDlg(bool Template, const KAEvent* event, bool newAlarm, QWidget* parent, GetResourceType getResource, bool readOnly) : QDialog(parent), mAlarmType(event->actionSubType()), mLoadTemplateButton(nullptr), mMainPageShown(false), mRecurPageShown(false), mRecurSetDefaultEndDate(true), mTemplateName(nullptr), mDeferGroup(nullptr), mDeferChangeButton(nullptr), mTimeWidget(nullptr), mShowInKorganizer(nullptr), mDeferGroupHeight(0), mEventId(newAlarm ? QString() : event->id()), mTemplate(Template), mNewAlarm(newAlarm), mDesiredReadOnly(readOnly), mReadOnly(readOnly), mShowingMore(true), mSavedEvent(nullptr) { init(event, getResource); mWindowList.append(this); } void EditAlarmDlg::init(const KAEvent* event, GetResourceType getResource) { switch (getResource) { case RES_USE_EVENT_ID: if (event) { mCollectionItemId = event->itemId(); break; } // fall through to RES_PROMPT case RES_PROMPT: mCollectionItemId = -1; break; case RES_IGNORE: default: mCollectionItemId = -2; break; } } void EditAlarmDlg::init(const KAEvent* event) { setObjectName(mTemplate ? QStringLiteral("TemplEditDlg") : QStringLiteral("EditDlg")); // used by LikeBack QString caption; if (mReadOnly) caption = mTemplate ? i18nc("@title:window", "Alarm Template [read-only]") : event->expired() ? i18nc("@title:window", "Archived Alarm [read-only]") : i18nc("@title:window", "Alarm [read-only]"); else caption = type_caption(); setWindowTitle(caption); // Create button box now so that types can work with it, but don't insert // it into layout just yet mButtonBox = new QDialogButtonBox(this); if (mReadOnly) { mButtonBox->addButton(QDialogButtonBox::Cancel); mTryButton = mButtonBox->addButton(i18nc("@action:button", "Try"), QDialogButtonBox::ActionRole); mMoreLessButton = mButtonBox->addButton(QDialogButtonBox::RestoreDefaults); } else if (mTemplate) { mButtonBox->addButton(QDialogButtonBox::Ok); mButtonBox->addButton(QDialogButtonBox::Cancel); mTryButton = mButtonBox->addButton(i18nc("@action:button", "Try"), QDialogButtonBox::ActionRole); mMoreLessButton = mButtonBox->addButton(QDialogButtonBox::RestoreDefaults); } else { mButtonBox->addButton(QDialogButtonBox::Ok); mButtonBox->addButton(QDialogButtonBox::Cancel); mTryButton = mButtonBox->addButton(i18nc("@action:button", "Try"), QDialogButtonBox::ActionRole); mLoadTemplateButton = mButtonBox->addButton(i18nc("@action:button", "Load Template..."), QDialogButtonBox::HelpRole); mMoreLessButton = mButtonBox->addButton(QDialogButtonBox::RestoreDefaults); } connect(mButtonBox, &QDialogButtonBox::clicked, this, &EditAlarmDlg::slotButtonClicked); if (mButtonBox->button(QDialogButtonBox::Ok)) mButtonBox->button(QDialogButtonBox::Ok)->setWhatsThis(i18nc("@info:whatsthis", "Schedule the alarm at the specified time.")); QVBoxLayout* mainLayout = new QVBoxLayout(this); if (mTemplate) { QFrame* frame = new QFrame; QHBoxLayout* box = new QHBoxLayout(); frame->setLayout(box); box->setMargin(0); box->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); QLabel* label = new QLabel(i18nc("@label:textbox", "Template name:")); label->setFixedSize(label->sizeHint()); box->addWidget(label); mTemplateName = new QLineEdit(); mTemplateName->setReadOnly(mReadOnly); connect(mTemplateName, &QLineEdit::textEdited, this, &EditAlarmDlg::contentsChanged); label->setBuddy(mTemplateName); box->addWidget(mTemplateName); frame->setWhatsThis(i18nc("@info:whatsthis", "Enter the name of the alarm template")); frame->setFixedHeight(box->sizeHint().height()); mainLayout->addWidget(frame); } mTabs = new QTabWidget(this); mainLayout->addWidget(mTabs); mTabScrollGroup = new StackedScrollGroup(this, mTabs); StackedScrollWidget* mainScroll = new StackedScrollWidget(mTabScrollGroup); mTabs->addTab(mainScroll, i18nc("@title:tab", "Alarm")); mMainPageIndex = 0; PageFrame* mainPage = new PageFrame(mainScroll); mainScroll->setWidget(mainPage); // mainPage becomes the child of mainScroll connect(mainPage, &PageFrame::shown, this, &EditAlarmDlg::slotShowMainPage); QVBoxLayout* topLayout = new QVBoxLayout(mainPage); topLayout->setMargin(style()->pixelMetric(QStyle::PM_DefaultChildMargin)); topLayout->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); // Recurrence tab StackedScrollWidget* recurScroll = new StackedScrollWidget(mTabScrollGroup); mTabs->addTab(recurScroll, QString()); mRecurPageIndex = 1; QFrame* recurTab = new QFrame; QVBoxLayout* recurTabLayout = new QVBoxLayout(); recurTabLayout->setMargin(style()->pixelMetric(QStyle::PM_DefaultChildMargin)); recurTab->setLayout(recurTabLayout); recurScroll->setWidget(recurTab); // recurTab becomes the child of recurScroll mRecurrenceEdit = new RecurrenceEdit(mReadOnly); recurTabLayout->addWidget(mRecurrenceEdit); connect(mRecurrenceEdit, &RecurrenceEdit::shown, this, &EditAlarmDlg::slotShowRecurrenceEdit); connect(mRecurrenceEdit, &RecurrenceEdit::typeChanged, this, &EditAlarmDlg::slotRecurTypeChange); connect(mRecurrenceEdit, &RecurrenceEdit::frequencyChanged, this, &EditAlarmDlg::slotRecurFrequencyChange); connect(mRecurrenceEdit, &RecurrenceEdit::repeatNeedsInitialisation, this, &EditAlarmDlg::slotSetSubRepetition); connect(mRecurrenceEdit, &RecurrenceEdit::contentsChanged, this, &EditAlarmDlg::contentsChanged); // Controls specific to the alarm type QGroupBox* actionBox = new QGroupBox(i18nc("@title:group", "Action"), mainPage); topLayout->addWidget(actionBox, 1); QVBoxLayout* layout = new QVBoxLayout(actionBox); layout->setMargin(style()->pixelMetric(QStyle::PM_DefaultChildMargin)); layout->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); type_init(actionBox, layout); if (!mTemplate) { // Deferred date/time: visible only for a deferred recurring event. mDeferGroup = new QGroupBox(i18nc("@title:group", "Deferred Alarm"), mainPage); topLayout->addWidget(mDeferGroup); QHBoxLayout* hlayout = new QHBoxLayout(mDeferGroup); hlayout->setMargin(style()->pixelMetric(QStyle::PM_DefaultChildMargin)); hlayout->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); QLabel* label = new QLabel(i18nc("@label", "Deferred to:"), mDeferGroup); label->setFixedSize(label->sizeHint()); hlayout->addWidget(label); mDeferTimeLabel = new QLabel(mDeferGroup); hlayout->addWidget(mDeferTimeLabel); mDeferChangeButton = new QPushButton(i18nc("@action:button", "Change..."), mDeferGroup); mDeferChangeButton->setFixedSize(mDeferChangeButton->sizeHint()); connect(mDeferChangeButton, &QPushButton::clicked, this, &EditAlarmDlg::slotEditDeferral); mDeferChangeButton->setWhatsThis(i18nc("@info:whatsthis", "Change the alarm's deferred time, or cancel the deferral")); hlayout->addWidget(mDeferChangeButton); //?? mDeferGroup->addSpace(0); } QHBoxLayout* hlayout = new QHBoxLayout(); hlayout->setMargin(0); topLayout->addLayout(hlayout); // Date and time entry if (mTemplate) { QGroupBox* templateTimeBox = new QGroupBox(i18nc("@title:group", "Time"), mainPage); topLayout->addWidget(templateTimeBox); QGridLayout* grid = new QGridLayout(templateTimeBox); grid->setMargin(style()->pixelMetric(QStyle::PM_DefaultChildMargin)); grid->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); mTemplateTimeGroup = new ButtonGroup(templateTimeBox); connect(mTemplateTimeGroup, &ButtonGroup::buttonSet, this, &EditAlarmDlg::slotTemplateTimeType); connect(mTemplateTimeGroup, &ButtonGroup::buttonSet, this, &EditAlarmDlg::contentsChanged); mTemplateDefaultTime = new RadioButton(i18nc("@option:radio", "Default time"), templateTimeBox); mTemplateDefaultTime->setFixedSize(mTemplateDefaultTime->sizeHint()); mTemplateDefaultTime->setReadOnly(mReadOnly); mTemplateDefaultTime->setWhatsThis(i18nc("@info:whatsthis", "Do not specify a start time for alarms based on this template. " "The normal default start time will be used.")); mTemplateTimeGroup->addButton(mTemplateDefaultTime); grid->addWidget(mTemplateDefaultTime, 0, 0, Qt::AlignLeft); QWidget* box = new QWidget(templateTimeBox); QHBoxLayout* layout = new QHBoxLayout(box); layout->setMargin(0); layout->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); mTemplateUseTime = new RadioButton(i18nc("@option:radio", "Time:"), box); mTemplateUseTime->setFixedSize(mTemplateUseTime->sizeHint()); mTemplateUseTime->setReadOnly(mReadOnly); mTemplateUseTime->setWhatsThis(i18nc("@info:whatsthis", "Specify a start time for alarms based on this template.")); layout->addWidget(mTemplateUseTime); mTemplateTimeGroup->addButton(mTemplateUseTime); mTemplateTime = new TimeEdit(); mTemplateTime->setFixedSize(mTemplateTime->sizeHint()); mTemplateTime->setReadOnly(mReadOnly); mTemplateTime->setWhatsThis(xi18nc("@info:whatsthis", "Enter the start time for alarms based on this template.%1", TimeSpinBox::shiftWhatsThis())); connect(mTemplateTime, &TimeEdit::valueChanged, this, &EditAlarmDlg::contentsChanged); layout->addWidget(mTemplateTime); layout->addStretch(1); grid->addWidget(box, 0, 1, Qt::AlignLeft); mTemplateAnyTime = new RadioButton(i18nc("@option:radio", "Date only"), templateTimeBox); mTemplateAnyTime->setFixedSize(mTemplateAnyTime->sizeHint()); mTemplateAnyTime->setReadOnly(mReadOnly); mTemplateAnyTime->setWhatsThis(xi18nc("@info:whatsthis", "Set the Any time option for alarms based on this template.")); mTemplateTimeGroup->addButton(mTemplateAnyTime); grid->addWidget(mTemplateAnyTime, 1, 0, Qt::AlignLeft); box = new QWidget(templateTimeBox); layout = new QHBoxLayout(box); layout->setMargin(0); layout->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); mTemplateUseTimeAfter = new RadioButton(i18nc("@option:radio", "Time from now:"), box); mTemplateUseTimeAfter->setFixedSize(mTemplateUseTimeAfter->sizeHint()); mTemplateUseTimeAfter->setReadOnly(mReadOnly); mTemplateUseTimeAfter->setWhatsThis(i18nc("@info:whatsthis", "Set alarms based on this template to start after the specified time " "interval from when the alarm is created.")); layout->addWidget(mTemplateUseTimeAfter); mTemplateTimeGroup->addButton(mTemplateUseTimeAfter); mTemplateTimeAfter = new TimeSpinBox(1, maxDelayTime); mTemplateTimeAfter->setValue(1439); mTemplateTimeAfter->setFixedSize(mTemplateTimeAfter->sizeHint()); mTemplateTimeAfter->setReadOnly(mReadOnly); connect(mTemplateTimeAfter, static_cast(&TimeSpinBox::valueChanged), this, &EditAlarmDlg::contentsChanged); mTemplateTimeAfter->setWhatsThis(xi18nc("@info:whatsthis", "%1%2", AlarmTimeWidget::i18n_TimeAfterPeriod(), TimeSpinBox::shiftWhatsThis())); layout->addWidget(mTemplateTimeAfter); box->setFixedHeight(box->sizeHint().height()); grid->addWidget(box, 1, 1, Qt::AlignLeft); hlayout->addStretch(); } else { mTimeWidget = new AlarmTimeWidget(i18nc("@title:group", "Time"), AlarmTimeWidget::AT_TIME, mainPage); connect(mTimeWidget, &AlarmTimeWidget::dateOnlyToggled, this, &EditAlarmDlg::slotAnyTimeToggled); connect(mTimeWidget, &AlarmTimeWidget::changed, this, &EditAlarmDlg::contentsChanged); topLayout->addWidget(mTimeWidget); } // Optional controls depending on More/Less Options button mMoreOptions = new QFrame(mainPage); mMoreOptions->setFrameStyle(QFrame::NoFrame); topLayout->addWidget(mMoreOptions); QVBoxLayout* moreLayout = new QVBoxLayout(mMoreOptions); moreLayout->setMargin(0); moreLayout->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); // Reminder mReminder = createReminder(mMoreOptions); if (mReminder) { mReminder->setFixedSize(mReminder->sizeHint()); connect(mReminder, &Reminder::changed, this, &EditAlarmDlg::contentsChanged); moreLayout->addWidget(mReminder, 0, Qt::AlignLeft); if (mTimeWidget) connect(mTimeWidget, &AlarmTimeWidget::changed, mReminder, &Reminder::setDefaultUnits); } // Late cancel selector - default = allow late display mLateCancel = new LateCancelSelector(true, mMoreOptions); connect(mLateCancel, &LateCancelSelector::changed, this, &EditAlarmDlg::contentsChanged); moreLayout->addWidget(mLateCancel, 0, Qt::AlignLeft); PackedLayout* playout = new PackedLayout(Qt::AlignJustify); playout->setSpacing(2 * style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); moreLayout->addLayout(playout); // Acknowledgement confirmation required - default = no confirmation CheckBox* confirmAck = type_createConfirmAckCheckbox(mMoreOptions); if (confirmAck) { confirmAck->setFixedSize(confirmAck->sizeHint()); connect(confirmAck, &CheckBox::toggled, this, &EditAlarmDlg::contentsChanged); playout->addWidget(confirmAck); } if (theApp()->korganizerEnabled()) { // Show in KOrganizer checkbox mShowInKorganizer = new CheckBox(i18n_chk_ShowInKOrganizer(), mMoreOptions); mShowInKorganizer->setFixedSize(mShowInKorganizer->sizeHint()); connect(mShowInKorganizer, &CheckBox::toggled, this, &EditAlarmDlg::contentsChanged); mShowInKorganizer->setWhatsThis(i18nc("@info:whatsthis", "Check to copy the alarm into KOrganizer's calendar")); playout->addWidget(mShowInKorganizer); } mainLayout->addWidget(mButtonBox); // Hide optional controls KConfigGroup config(KSharedConfig::openConfig(), EDIT_MORE_GROUP); showOptions(config.readEntry(EDIT_MORE_KEY, false)); // Initialise the state of all controls according to the specified event, if any initValues(event); if (mTemplateName) mTemplateName->setFocus(); if (!mNewAlarm) { // Save the initial state of all controls so that we can later tell if they have changed saveState((event && (mTemplate || !event->isTemplate())) ? event : nullptr); contentsChanged(); // enable/disable OK button } // Note the current desktop so that the dialog can be shown on it. // If a main window is visible, the dialog will by KDE default always appear on its // desktop. If the user invokes the dialog via the system tray on a different desktop, // that can cause confusion. mDesktop = KWindowSystem::currentDesktop(); if (theApp()->windowFocusBroken()) { const QList children = findChildren(); foreach (QWidget* w, children) w->installEventFilter(this); } } EditAlarmDlg::~EditAlarmDlg() { delete mButtonBox; mButtonBox = nullptr; // prevent text edit contentsChanged() signal triggering a crash delete mSavedEvent; mWindowList.removeAll(this); } /****************************************************************************** * Return the number of instances. */ int EditAlarmDlg::instanceCount() { return mWindowList.count(); } /****************************************************************************** * Initialise the dialog controls from the specified event. */ void EditAlarmDlg::initValues(const KAEvent* event) { setReadOnly(mDesiredReadOnly); mChanged = false; mOnlyDeferred = false; mExpiredRecurrence = false; mLateCancel->showAutoClose(false); bool deferGroupVisible = false; if (event) { // Set the values to those for the specified event if (mTemplate) mTemplateName->setText(event->templateName()); bool recurs = event->recurs(); if ((recurs || event->repetition()) && !mTemplate && event->deferred()) { deferGroupVisible = true; mDeferDateTime = event->deferDateTime(); mDeferTimeLabel->setText(mDeferDateTime.formatLocale()); mDeferGroup->show(); } if (mTemplate) { // Editing a template int afterTime = event->isTemplate() ? event->templateAfterTime() : -1; bool noTime = !afterTime; bool useTime = !event->mainDateTime().isDateOnly(); RadioButton* button = noTime ? mTemplateDefaultTime : (afterTime > 0) ? mTemplateUseTimeAfter : useTime ? mTemplateUseTime : mTemplateAnyTime; button->setChecked(true); mTemplateTimeAfter->setValue(afterTime > 0 ? afterTime : 1); if (!noTime && useTime) mTemplateTime->setValue(event->mainDateTime().kDateTime().time()); else mTemplateTime->setValue(0); } else { if (event->isTemplate()) { // Initialising from an alarm template: use current date const KADateTime now = KADateTime::currentDateTime(Preferences::timeSpec()); int afterTime = event->templateAfterTime(); if (afterTime >= 0) { mTimeWidget->setDateTime(now.addSecs(afterTime * 60)); mTimeWidget->selectTimeFromNow(); } else { KADateTime dt = event->startDateTime().kDateTime(); dt.setTimeSpec(Preferences::timeSpec()); QDate d = now.date(); if (!dt.isDateOnly() && now.time() >= dt.time()) d = d.addDays(1); // alarm time has already passed, so use tomorrow dt.setDate(d); mTimeWidget->setDateTime(dt); } } else { mExpiredRecurrence = recurs && event->mainExpired(); mTimeWidget->setDateTime(recurs || event->category() == CalEvent::ARCHIVED ? event->startDateTime() : event->mainExpired() ? event->deferDateTime() : event->mainDateTime()); } } KAEvent::SubAction action = event->actionSubType(); AlarmText altext; if (event->commandScript()) altext.setScript(event->cleanText()); else altext.setText(event->cleanText()); setAction(action, altext); mLateCancel->setMinutes(event->lateCancel(), event->startDateTime().isDateOnly(), TimePeriod::HoursMinutes); if (mShowInKorganizer) mShowInKorganizer->setChecked(event->copyToKOrganizer()); type_initValues(event); mRecurrenceEdit->set(*event); // must be called after mTimeWidget is set up, to ensure correct date-only enabling mTabs->setTabText(mRecurPageIndex, recurText(*event)); } else { // Set the values to their defaults const KADateTime defaultTime = KADateTime::currentUtcDateTime().addSecs(60).toTimeSpec(Preferences::timeSpec()); if (mTemplate) { mTemplateDefaultTime->setChecked(true); mTemplateTime->setValue(0); mTemplateTimeAfter->setValue(1); } else mTimeWidget->setDateTime(defaultTime); mLateCancel->setMinutes((Preferences::defaultLateCancel() ? 1 : 0), false, TimePeriod::HoursMinutes); if (mShowInKorganizer) mShowInKorganizer->setChecked(Preferences::defaultCopyToKOrganizer()); type_initValues(nullptr); mRecurrenceEdit->setDefaults(defaultTime); // must be called after mTimeWidget is set up, to ensure correct date-only enabling slotRecurFrequencyChange(); // update the Recurrence text } if (mReminder && mTimeWidget) mReminder->setDefaultUnits(mTimeWidget->getDateTime(nullptr, false, false)); if (!deferGroupVisible && mDeferGroup) mDeferGroup->hide(); bool empty = AlarmCalendar::resources()->events(CalEvent::TEMPLATE).isEmpty(); if (mLoadTemplateButton) mLoadTemplateButton->setEnabled(!empty); } /****************************************************************************** * Initialise various values in the New Alarm dialogue. */ void EditAlarmDlg::setTime(const DateTime& start) { mTimeWidget->setDateTime(start); } void EditAlarmDlg::setRecurrence(const KARecurrence& recur, const KCalCore::Duration& subRepeatInterval, int subRepeatCount) { KAEvent event; event.setTime(mTimeWidget->getDateTime(nullptr, false, false)); event.setRecurrence(recur); event.setRepetition(Repetition(subRepeatInterval, subRepeatCount - 1)); mRecurrenceEdit->set(event); } void EditAlarmDlg::setRepeatAtLogin() { mRecurrenceEdit->setRepeatAtLogin(); } void EditAlarmDlg::setLateCancel(int minutes) { mLateCancel->setMinutes(minutes, mTimeWidget->getDateTime(nullptr, false, false).isDateOnly(), TimePeriod::HoursMinutes); } void EditAlarmDlg::setShowInKOrganizer(bool show) { mShowInKorganizer->setChecked(show); } /****************************************************************************** * Set the read-only status of all non-template controls. */ void EditAlarmDlg::setReadOnly(bool readOnly) { mReadOnly = readOnly; if (mTimeWidget) mTimeWidget->setReadOnly(readOnly); mLateCancel->setReadOnly(readOnly); if (mDeferChangeButton) { if (readOnly) mDeferChangeButton->hide(); else mDeferChangeButton->show(); } if (mShowInKorganizer) mShowInKorganizer->setReadOnly(readOnly); } /****************************************************************************** * Save the state of all controls. */ void EditAlarmDlg::saveState(const KAEvent* event) { delete mSavedEvent; mSavedEvent = nullptr; if (event) mSavedEvent = new KAEvent(*event); if (mTemplate) { mSavedTemplateName = mTemplateName->text(); mSavedTemplateTimeType = mTemplateTimeGroup->checkedButton(); mSavedTemplateTime = mTemplateTime->time(); mSavedTemplateAfterTime = mTemplateTimeAfter->value(); } checkText(mSavedTextFileCommandMessage, false); if (mTimeWidget) mSavedDateTime = mTimeWidget->getDateTime(nullptr, false, false); mSavedLateCancel = mLateCancel->minutes(); if (mShowInKorganizer) mSavedShowInKorganizer = mShowInKorganizer->isChecked(); mSavedRecurrenceType = mRecurrenceEdit->repeatType(); mSavedDeferTime = mDeferDateTime.kDateTime(); } /****************************************************************************** * Check whether any of the controls has changed state since the dialog was * first displayed. * Reply = true if any non-deferral controls have changed, or if it's a new event. * = false if no non-deferral controls have changed. In this case, * mOnlyDeferred indicates whether deferral controls may have changed. */ bool EditAlarmDlg::stateChanged() const { mChanged = true; mOnlyDeferred = false; if (!mSavedEvent) return true; QString textFileCommandMessage; checkText(textFileCommandMessage, false); if (mTemplate) { if (mSavedTemplateName != mTemplateName->text() || mSavedTemplateTimeType != mTemplateTimeGroup->checkedButton() || (mTemplateUseTime->isChecked() && mSavedTemplateTime != mTemplateTime->time()) || (mTemplateUseTimeAfter->isChecked() && mSavedTemplateAfterTime != mTemplateTimeAfter->value())) return true; } else { const KADateTime dt = mTimeWidget->getDateTime(nullptr, false, false); if (mSavedDateTime.timeSpec() != dt.timeSpec() || mSavedDateTime != dt) return true; } if (mSavedLateCancel != mLateCancel->minutes() || (mShowInKorganizer && mSavedShowInKorganizer != mShowInKorganizer->isChecked()) || textFileCommandMessage != mSavedTextFileCommandMessage || mSavedRecurrenceType != mRecurrenceEdit->repeatType()) return true; if (type_stateChanged()) return true; if (mRecurrenceEdit->stateChanged()) return true; if (mSavedEvent && mSavedEvent->deferred()) mOnlyDeferred = true; mChanged = false; return false; } /****************************************************************************** * Called whenever any of the controls changes state. * Enable or disable the OK button depending on whether any controls have a * different state from their initial state. */ void EditAlarmDlg::contentsChanged() { // Don't do anything if it's a new alarm or we're still initialising // (i.e. mSavedEvent null). if (mSavedEvent && mButtonBox && mButtonBox->button(QDialogButtonBox::Ok)) mButtonBox->button(QDialogButtonBox::Ok)->setEnabled(stateChanged() || mDeferDateTime.kDateTime() != mSavedDeferTime); } /****************************************************************************** * Get the currently entered dialog data. * The data is returned in the supplied KAEvent instance. * Reply = false if the only change has been to an existing deferral. */ bool EditAlarmDlg::getEvent(KAEvent& event, Akonadi::Collection& collection) { collection = mCollection; if (mChanged) { // It's a new event, or the edit controls have changed setEvent(event, mAlarmMessage, false); return true; } // Only the deferral time may have changed event = *mSavedEvent; if (mOnlyDeferred) { // Just modify the original event, to avoid expired recurring events // being returned as rubbish. if (mDeferDateTime.isValid()) event.defer(mDeferDateTime, event.reminderDeferral(), false); else event.cancelDefer(); } return false; } /****************************************************************************** * Extract the data in the dialog and set up a KAEvent from it. * If 'trial' is true, the event is set up for a simple one-off test, ignoring * recurrence, reminder, template etc. data. */ void EditAlarmDlg::setEvent(KAEvent& event, const QString& text, bool trial) { KADateTime dt; if (!trial) { if (!mTemplate) dt = mAlarmDateTime.effectiveKDateTime(); else if (mTemplateUseTime->isChecked()) dt = KADateTime(QDate(2000,1,1), mTemplateTime->time()); } int lateCancel = (trial || !mLateCancel->isEnabled()) ? 0 : mLateCancel->minutes(); type_setEvent(event, dt, text, lateCancel, trial); if (!trial) { if (mRecurrenceEdit->repeatType() != RecurrenceEdit::NO_RECUR) { mRecurrenceEdit->updateEvent(event, !mTemplate); const KADateTime now = KADateTime::currentDateTime(mAlarmDateTime.timeSpec()); bool dateOnly = mAlarmDateTime.isDateOnly(); if ((dateOnly && mAlarmDateTime.date() < now.date()) || (!dateOnly && mAlarmDateTime.kDateTime() < now)) { // A timed recurrence has an entered start date which has // already expired, so we must adjust the next repetition. event.setNextOccurrence(now); } mAlarmDateTime = event.startDateTime(); if (mDeferDateTime.isValid() && mDeferDateTime < mAlarmDateTime) { bool deferral = true; bool deferReminder = false; int reminder = mReminder ? mReminder->minutes() : 0; if (reminder) { DateTime remindTime = mAlarmDateTime.addMins(-reminder); if (mDeferDateTime >= remindTime) { if (remindTime > KADateTime::currentUtcDateTime()) deferral = false; // ignore deferral if it's after next reminder else if (mDeferDateTime > remindTime) deferReminder = true; // it's the reminder which is being deferred } } if (deferral) event.defer(mDeferDateTime, deferReminder, false); } } if (mTemplate) { int afterTime = mTemplateDefaultTime->isChecked() ? 0 : mTemplateUseTimeAfter->isChecked() ? mTemplateTimeAfter->value() : -1; event.setTemplate(mTemplateName->text(), afterTime); } } } /****************************************************************************** * Get the currently specified alarm flag bits. */ KAEvent::Flags EditAlarmDlg::getAlarmFlags() const { KAEvent::Flags flags(0); if (mShowInKorganizer && mShowInKorganizer->isEnabled() && mShowInKorganizer->isChecked()) flags |= KAEvent::COPY_KORGANIZER; if (mRecurrenceEdit->repeatType() == RecurrenceEdit::AT_LOGIN) flags |= KAEvent::REPEAT_AT_LOGIN; if (mTemplate ? mTemplateAnyTime->isChecked() : mAlarmDateTime.isDateOnly()) flags |= KAEvent::ANY_TIME; return flags; } /****************************************************************************** * Called when the dialog is displayed. * The first time through, sets the size to the same as the last time it was * displayed. */ void EditAlarmDlg::showEvent(QShowEvent* se) { QDialog::showEvent(se); if (!mDeferGroupHeight) { if (mDeferGroup) mDeferGroupHeight = mDeferGroup->height() + style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing); QSize s; if (KAlarm::readConfigWindowSize(mTemplate ? TEMPLATE_DIALOG_NAME : EDIT_DIALOG_NAME, s)) { bool defer = mDeferGroup && !mDeferGroup->isHidden(); s.setHeight(s.height() + (defer ? mDeferGroupHeight : 0)); if (!defer) mTabScrollGroup->setSized(); resize(s); } } slotResize(); KWindowSystem::setOnDesktop(winId(), mDesktop); // ensure it displays on the desktop expected by the user if (theApp()->needWindowFocusFix()) { QApplication::setActiveWindow(this); QTimer::singleShot(0, this, &EditAlarmDlg::focusFixTimer); } } /****************************************************************************** * Called when the window is first shown, to ensure that it initially becomes * the active window. * This is only required on Ubuntu's Unity desktop, which doesn't transfer * keyboard focus properly between Edit Alarm Dialog windows and MessageWin * windows. */ void EditAlarmDlg::focusFixTimer() { if (theApp()->needWindowFocusFix() && QApplication::focusWidget()->window() != this) { QApplication::setActiveWindow(this); QTimer::singleShot(0, this, &EditAlarmDlg::focusFixTimer); } } /****************************************************************************** * Called to detect when the mouse is pressed anywhere inside the window. * Activates this window if a MessageWin window is also active. * This is only required on Ubuntu's Unity desktop, which doesn't transfer * keyboard focus properly between Edit Alarm Dialog windows and MessageWin * windows. */ bool EditAlarmDlg::eventFilter(QObject*, QEvent* e) { if (theApp()->needWindowFocusFix()) { if (e->type() == QEvent::MouseButtonPress) QApplication::setActiveWindow(this); } return false; } /****************************************************************************** * Called when the dialog is closed. */ void EditAlarmDlg::closeEvent(QCloseEvent* ce) { Q_EMIT rejected(); QDialog::closeEvent(ce); } /****************************************************************************** * Update the tab sizes (again) and if the resized dialog height is greater * than the minimum, resize it again. This is necessary because (a) resizing * tabs doesn't always work properly the first time, and (b) resizing to the * minimum size hint doesn't always work either. */ void EditAlarmDlg::slotResize() { QSize s = mTabScrollGroup->adjustSize(true); s = minimumSizeHint(); if (height() > s.height()) { // Resize to slightly greater than the minimum height. // This is for some unknown reason necessary, since // sometimes resizing to the minimum height fails. resize(s.width(), s.height() + 2); } } /****************************************************************************** * Called when the dialog's size has changed. * Records the new size (adjusted to ignore the optional height of the deferred * time edit widget) in the config file. */ void EditAlarmDlg::resizeEvent(QResizeEvent* re) { if (isVisible() && mDeferGroupHeight) { QSize s = re->size(); s.setHeight(s.height() - (!mDeferGroup || mDeferGroup->isHidden() ? 0 : mDeferGroupHeight)); KAlarm::writeConfigWindowSize(mTemplate ? TEMPLATE_DIALOG_NAME : EDIT_DIALOG_NAME, s); } QDialog::resizeEvent(re); } /****************************************************************************** * Called when any button is clicked. */ void EditAlarmDlg::slotButtonClicked(QAbstractButton* button) { if (button == mTryButton) slotTry(); else if (button == mLoadTemplateButton) slotHelp(); else if (button == mMoreLessButton) slotDefault(); else if (button == mButtonBox->button(QDialogButtonBox::Ok)) { if (validate()) accept(); } else reject(); } /****************************************************************************** * Called when the OK button is clicked. * Validate the input data. */ bool EditAlarmDlg::validate() { if (!stateChanged()) { // No changes have been made except possibly to an existing deferral if (!mOnlyDeferred) reject(); return mOnlyDeferred; } RecurrenceEdit::RepeatType recurType = mRecurrenceEdit->repeatType(); if (mTimeWidget && mTabs->currentIndex() == mRecurPageIndex && recurType == RecurrenceEdit::AT_LOGIN) mTimeWidget->setDateTime(mRecurrenceEdit->endDateTime()); bool timedRecurrence = mRecurrenceEdit->isTimedRepeatType(); // does it recur other than at login? if (mTemplate) { // Check that the template name is not blank and is unique QString errmsg; QString name = mTemplateName->text(); if (name.isEmpty()) errmsg = i18nc("@info", "You must enter a name for the alarm template"); else if (name != mSavedTemplateName) { if (AlarmCalendar::resources()->templateEvent(name)) errmsg = i18nc("@info", "Template name is already in use"); } if (!errmsg.isEmpty()) { mTemplateName->setFocus(); KAMessageBox::sorry(this, errmsg); return false; } } else if (mTimeWidget) { QWidget* errWidget; mAlarmDateTime = mTimeWidget->getDateTime(nullptr, !timedRecurrence, false, &errWidget); if (errWidget) { // It's more than just an existing deferral being changed, so the time matters mTabs->setCurrentIndex(mMainPageIndex); errWidget->setFocus(); mTimeWidget->getDateTime(); // display the error message now return false; } } if (!type_validate(false)) return false; if (!mTemplate) { if (mChanged && mRecurrenceEdit->repeatType() != RecurrenceEdit::NO_RECUR) { // Check whether the start date/time must be adjusted // to match the recurrence specification. DateTime dt = mAlarmDateTime; // setEvent() changes mAlarmDateTime KAEvent event; setEvent(event, mAlarmMessage, false); mAlarmDateTime = dt; // restore KADateTime pre = dt.effectiveKDateTime(); bool dateOnly = dt.isDateOnly(); if (dateOnly) pre = pre.addDays(-1); else pre = pre.addSecs(-1); DateTime next; event.nextOccurrence(pre, next, KAEvent::IGNORE_REPETITION); if (next != dt) { QString prompt = dateOnly ? i18nc("@info The parameter is a date value", "The start date does not match the alarm's recurrence pattern, " "so it will be adjusted to the date of the next recurrence (%1).", QLocale().toString(next.date(), QLocale::ShortFormat)) : i18nc("@info The parameter is a date/time value", "The start date/time does not match the alarm's recurrence pattern, " "so it will be adjusted to the date/time of the next recurrence (%1).", QLocale().toString(next.effectiveDateTime(), QLocale::ShortFormat)); if (KAMessageBox::warningContinueCancel(this, prompt) != KMessageBox::Continue) return false; } } if (timedRecurrence) { KAEvent event; Akonadi::Collection c; getEvent(event, c); // this may adjust mAlarmDateTime const KADateTime now = KADateTime::currentDateTime(mAlarmDateTime.timeSpec()); bool dateOnly = mAlarmDateTime.isDateOnly(); if ((dateOnly && mAlarmDateTime.date() < now.date()) || (!dateOnly && mAlarmDateTime.kDateTime() < now)) { // A timed recurrence has an entered start date which // has already expired, so we must adjust it. if (event.nextOccurrence(now, mAlarmDateTime, KAEvent::ALLOW_FOR_REPETITION) == KAEvent::NO_OCCURRENCE) { KAMessageBox::sorry(this, i18nc("@info", "Recurrence has already expired")); return false; } if (event.workTimeOnly() && !event.nextTrigger(KAEvent::DISPLAY_TRIGGER).isValid()) { if (KAMessageBox::warningContinueCancel(this, i18nc("@info", "The alarm will never occur during working hours")) != KMessageBox::Continue) return false; } } } QString errmsg; QWidget* errWidget = mRecurrenceEdit->checkData(mAlarmDateTime.effectiveKDateTime(), errmsg); if (errWidget) { mTabs->setCurrentIndex(mRecurPageIndex); errWidget->setFocus(); KAMessageBox::sorry(this, errmsg); return false; } } if (recurType != RecurrenceEdit::NO_RECUR) { KAEvent recurEvent; int longestRecurMinutes = -1; int reminder = mReminder ? mReminder->minutes() : 0; if (reminder && !mReminder->isOnceOnly()) { mRecurrenceEdit->updateEvent(recurEvent, false); longestRecurMinutes = recurEvent.longestRecurrenceInterval().asSeconds() / 60; if (longestRecurMinutes && reminder >= longestRecurMinutes) { mTabs->setCurrentIndex(mMainPageIndex); mReminder->setFocusOnCount(); KAMessageBox::sorry(this, xi18nc("@info", "Reminder period must be less than the recurrence interval, unless %1 is checked.", Reminder::i18n_chk_FirstRecurrenceOnly())); return false; } } if (mRecurrenceEdit->subRepetition()) { if (longestRecurMinutes < 0) { mRecurrenceEdit->updateEvent(recurEvent, false); longestRecurMinutes = recurEvent.longestRecurrenceInterval().asSeconds() / 60; } if (longestRecurMinutes > 0 && recurEvent.repetition().intervalMinutes() * recurEvent.repetition().count() >= longestRecurMinutes - reminder) { KAMessageBox::sorry(this, i18nc("@info", "The duration of a repetition within the recurrence must be less than the recurrence interval minus any reminder period")); mRecurrenceEdit->activateSubRepetition(); // display the alarm repetition dialog again return false; } if (!recurEvent.repetition().isDaily() && ((mTemplate && mTemplateAnyTime->isChecked()) || (!mTemplate && mAlarmDateTime.isDateOnly()))) { KAMessageBox::sorry(this, i18nc("@info", "For a repetition within the recurrence, its period must be in units of days or weeks for a date-only alarm")); mRecurrenceEdit->activateSubRepetition(); // display the alarm repetition dialog again return false; } } } if (!checkText(mAlarmMessage)) return false; mCollection = Akonadi::Collection(); // An item ID = -2 indicates that the caller already // knows which collection to use. if (mCollectionItemId >= -1) { if (mCollectionItemId >= 0) { mCollection = AlarmCalendar::resources()->collectionForEvent(mCollectionItemId); if (mCollection.isValid()) { CalEvent::Type type = mTemplate ? CalEvent::TEMPLATE : CalEvent::ACTIVE; if (!(AkonadiModel::instance()->types(mCollection) & type)) mCollection = Akonadi::Collection(); // event may have expired while dialog was open } } bool cancelled = false; CalEvent::Type type = mTemplate ? CalEvent::TEMPLATE : CalEvent::ACTIVE; if (CollectionControlModel::isWritableEnabled(mCollection, type) <= 0) mCollection = CollectionControlModel::destination(type, this, false, &cancelled); if (!mCollection.isValid()) { if (!cancelled) KAMessageBox::sorry(this, i18nc("@info", "You must select a calendar to save the alarm in")); return false; } } return true; } /****************************************************************************** * Called when the Try button is clicked. * Display/execute the alarm immediately for the user to check its configuration. */ void EditAlarmDlg::slotTry() { QString text; if (checkText(text)) { if (!type_validate(true)) return; KAEvent event; setEvent(event, text, true); if (!mNewAlarm && !stateChanged()) { // It's an existing alarm which hasn't been changed yet: // enable KALARM_UID environment variable to be set. event.setEventId(mEventId); } type_aboutToTry(); void* result = theApp()->execAlarm(event, event.firstAlarm(), false, false); type_executedTry(text, result); } } /****************************************************************************** * Called when the Load Template button is clicked. * Prompt to select a template and initialise the dialog with its contents. */ void EditAlarmDlg::slotHelp() { KAEvent::Actions type; switch (mAlarmType) { case KAEvent::FILE: case KAEvent::MESSAGE: type = KAEvent::ACT_DISPLAY; break; case KAEvent::COMMAND: type = KAEvent::ACT_COMMAND; break; case KAEvent::EMAIL: type = KAEvent::ACT_EMAIL; break; case KAEvent::AUDIO: type = KAEvent::ACT_AUDIO; break; default: return; } // Use AutoQPointer to guard against crash on application exit while // the dialogue is still open. It prevents double deletion (both on // deletion of EditAlarmDlg, and on return from this function). AutoQPointer dlg = new TemplatePickDlg(type, this); if (dlg->exec() == QDialog::Accepted) { KAEvent event = dlg->selectedTemplate(); initValues(&event); } } /****************************************************************************** * Called when the More Options or Less Options buttons are clicked. * Show/hide the optional options and swap the More/Less buttons, and save the * new setting as the default from now on. */ void EditAlarmDlg::slotDefault() { showOptions(!mShowingMore); KConfigGroup config(KSharedConfig::openConfig(), EDIT_MORE_GROUP); config.writeEntry(EDIT_MORE_KEY, mShowingMore); } /****************************************************************************** * Show/hide the optional options and swap the More/Less buttons. */ void EditAlarmDlg::showOptions(bool more) { - qCDebug(KALARM_LOG) << (more ? "More" : "Less"); + qCDebug(KALARM_LOG) << "EditAlarmDlg::showOptions:" << (more ? "More" : "Less"); if (more) { mMoreOptions->show(); mMoreLessButton->setText(i18nc("@action:Button", "Less Options <<")); } else { mMoreOptions->hide(); mMoreLessButton->setText(i18nc("@action:button", "More Options >>")); } if (mTimeWidget) mTimeWidget->showMoreOptions(more); type_showOptions(more); mRecurrenceEdit->showMoreOptions(more); mShowingMore = more; QTimer::singleShot(0, this, &EditAlarmDlg::slotResize); } /****************************************************************************** * Called when the Change deferral button is clicked. */ void EditAlarmDlg::slotEditDeferral() { if (!mTimeWidget) return; bool limit = true; const Repetition repetition = mRecurrenceEdit->subRepetition(); DateTime start = mSavedEvent->recurs() ? (mExpiredRecurrence ? DateTime() : mSavedEvent->mainDateTime()) : mTimeWidget->getDateTime(nullptr, !repetition, !mExpiredRecurrence); if (!start.isValid()) { if (!mExpiredRecurrence) return; limit = false; } const KADateTime now = KADateTime::currentUtcDateTime(); if (limit) { if (repetition && start < now) { // Sub-repetition - find the time of the next one int repeatNum = repetition.isDaily() ? (start.daysTo(now) + repetition.intervalDays() - 1) / repetition.intervalDays() : (start.secsTo(now) + repetition.intervalSeconds() - 1) / repetition.intervalSeconds(); if (repeatNum > repetition.count()) { mTimeWidget->getDateTime(); // output the appropriate error message return; } start = KADateTime(repetition.duration(repeatNum).end(start.qDateTime())); } } bool deferred = mDeferDateTime.isValid(); // Use AutoQPointer to guard against crash on application exit while // the dialogue is still open. It prevents double deletion (both on // deletion of EditAlarmDlg, and on return from this function). AutoQPointer deferDlg = new DeferAlarmDlg((deferred ? mDeferDateTime : DateTime(now.addSecs(60).toTimeSpec(start.timeSpec()))), start.isDateOnly(), deferred, this); deferDlg->setObjectName(QStringLiteral("EditDeferDlg")); // used by LikeBack if (limit) { // Don't allow deferral past the next recurrence int reminder = mReminder ? mReminder->minutes() : 0; if (reminder) { DateTime remindTime = start.addMins(-reminder); if (KADateTime::currentUtcDateTime() < remindTime) start = remindTime; } deferDlg->setLimit(start.addSecs(-60)); } if (deferDlg->exec() == QDialog::Accepted) { mDeferDateTime = deferDlg->getDateTime(); mDeferTimeLabel->setText(mDeferDateTime.isValid() ? mDeferDateTime.formatLocale() : QString()); contentsChanged(); } } /****************************************************************************** * Called when the main page is shown. * Sets the focus widget to the first edit field. */ void EditAlarmDlg::slotShowMainPage() { if (!mMainPageShown) { if (mTemplateName) mTemplateName->setFocus(); mMainPageShown = true; } else { // Set scroll position to top, since it otherwise jumps randomly StackedScrollWidget* main = static_cast(mTabs->widget(0)); main->verticalScrollBar()->setValue(0); } if (mTimeWidget) { if (!mReadOnly && mRecurPageShown && mRecurrenceEdit->repeatType() == RecurrenceEdit::AT_LOGIN) mTimeWidget->setDateTime(mRecurrenceEdit->endDateTime()); if (mReadOnly || mRecurrenceEdit->isTimedRepeatType()) mTimeWidget->setMinDateTime(); // don't set a minimum date/time else mTimeWidget->setMinDateTimeIsCurrent(); // set the minimum date/time to track the clock } } /****************************************************************************** * Called when the recurrence edit page is shown. * The recurrence defaults are set to correspond to the start date. * The first time, for a new alarm, the recurrence end date is set according to * the alarm start time. */ void EditAlarmDlg::slotShowRecurrenceEdit() { mRecurPageIndex = mTabs->currentIndex(); if (!mReadOnly && !mTemplate) { mAlarmDateTime = mTimeWidget->getDateTime(nullptr, false, false); const KADateTime now = KADateTime::currentDateTime(mAlarmDateTime.timeSpec()); bool expired = (mAlarmDateTime.effectiveKDateTime() < now); if (mRecurSetDefaultEndDate) { mRecurrenceEdit->setDefaultEndDate(expired ? now.date() : mAlarmDateTime.date()); mRecurSetDefaultEndDate = false; } mRecurrenceEdit->setStartDate(mAlarmDateTime.date(), now.date()); if (mRecurrenceEdit->repeatType() == RecurrenceEdit::AT_LOGIN) mRecurrenceEdit->setEndDateTime(expired ? now : mAlarmDateTime.kDateTime()); } mRecurPageShown = true; } /****************************************************************************** * Called when the recurrence type selection changes. * Enables/disables date-only alarms as appropriate. * Enables/disables controls depending on at-login setting. */ void EditAlarmDlg::slotRecurTypeChange(int repeatType) { bool atLogin = (mRecurrenceEdit->repeatType() == RecurrenceEdit::AT_LOGIN); if (!mTemplate) { bool recurs = (mRecurrenceEdit->repeatType() != RecurrenceEdit::NO_RECUR); if (mDeferGroup) mDeferGroup->setEnabled(recurs); mTimeWidget->enableAnyTime(!recurs || repeatType != RecurrenceEdit::SUBDAILY); if (atLogin) { mAlarmDateTime = mTimeWidget->getDateTime(nullptr, false, false); mRecurrenceEdit->setEndDateTime(mAlarmDateTime.kDateTime()); } if (mReminder) mReminder->enableOnceOnly(recurs && !atLogin); } if (mReminder) mReminder->setAfterOnly(atLogin); mLateCancel->setEnabled(!atLogin); if (mShowInKorganizer) mShowInKorganizer->setEnabled(!atLogin); slotRecurFrequencyChange(); } /****************************************************************************** * Called when the recurrence frequency selection changes, or the sub- * repetition interval changes. * Updates the recurrence frequency text. */ void EditAlarmDlg::slotRecurFrequencyChange() { slotSetSubRepetition(); KAEvent event; mRecurrenceEdit->updateEvent(event, false); mTabs->setTabText(mRecurPageIndex, recurText(event)); } /****************************************************************************** * Called when the Repetition within Recurrence button has been pressed to * display the sub-repetition dialog. * Alarm repetition has the following restrictions: * 1) Not allowed for a repeat-at-login alarm * 2) For a date-only alarm, the repeat interval must be a whole number of days. * 3) The overall repeat duration must be less than the recurrence interval. */ void EditAlarmDlg::slotSetSubRepetition() { bool dateOnly = mTemplate ? mTemplateAnyTime->isChecked() : mTimeWidget->anyTime(); mRecurrenceEdit->setSubRepetition((mReminder ? mReminder->minutes() : 0), dateOnly); } /****************************************************************************** * Called when one of the template time radio buttons is clicked, * to enable or disable the template time entry spin boxes. */ void EditAlarmDlg::slotTemplateTimeType(QAbstractButton*) { mTemplateTime->setEnabled(mTemplateUseTime->isChecked()); mTemplateTimeAfter->setEnabled(mTemplateUseTimeAfter->isChecked()); } /****************************************************************************** * Called when the "Any time" checkbox is toggled in the date/time widget. * Sets the advance reminder and late cancel units to days if any time is checked. */ void EditAlarmDlg::slotAnyTimeToggled(bool anyTime) { if (mReminder && mReminder->isReminder()) mReminder->setDateOnly(anyTime); mLateCancel->setDateOnly(anyTime); } bool EditAlarmDlg::dateOnly() const { return mTimeWidget ? mTimeWidget->anyTime() : mTemplateAnyTime->isChecked(); } bool EditAlarmDlg::isTimedRecurrence() const { return mRecurrenceEdit->isTimedRepeatType(); } void EditAlarmDlg::showMainPage() { mTabs->setCurrentIndex(mMainPageIndex); } #include "moc_editdlg.cpp" #include "moc_editdlg_p.cpp" // vim: et sw=4: diff --git a/src/editdlgtypes.cpp b/src/editdlgtypes.cpp index 1d7eb098..6495a229 100644 --- a/src/editdlgtypes.cpp +++ b/src/editdlgtypes.cpp @@ -1,1876 +1,1876 @@ /* * editdlgtypes.cpp - dialogs to create or edit alarm or alarm template types * Program: kalarm * Copyright © 2001-2018 by 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 "editdlgtypes.h" #include "editdlg_p.h" #include "autoqpointer.h" #include "buttongroup.h" #include "checkbox.h" #include "colourbutton.h" #include "emailidcombo.h" #include "fontcolourbutton.h" #include "functions.h" #include "kalarmapp.h" #include "kamail.h" #include "latecancel.h" #include "lineedit.h" #include "mainwindow.h" #include "messagebox.h" #include "messagewin.h" #include "pickfileradio.h" #include "preferences.h" #include "radiobutton.h" #include "reminder.h" #include "shellprocess.h" #include "soundpicker.h" #include "sounddlg.h" #include "specialactions.h" #include "templatepickdlg.h" #include "timespinbox.h" #include #include #include #include using namespace KCalCore; #include #include #include #include #include #include #include #include #include #include #include #include #include "kalarm_debug.h" using namespace KAlarmCal; enum { tTEXT, tFILE, tCOMMAND }; // order of mTypeCombo items /*============================================================================= = Class PickLogFileRadio =============================================================================*/ class PickLogFileRadio : public PickFileRadio { public: PickLogFileRadio(QPushButton* b, LineEdit* e, const QString& text, ButtonGroup* group, QWidget* parent) : PickFileRadio(b, e, text, group, parent) { } QString pickFile() override // called when browse button is pressed to select a log file { return KAlarm::browseFile(i18nc("@title:window", "Choose Log File"), mDefaultDir, fileEdit()->text(), QString(), false, parentWidget()); } private: QString mDefaultDir; // default directory for log file browse button }; /*============================================================================= = Class EditDisplayAlarmDlg = Dialog to edit display alarms. =============================================================================*/ QString EditDisplayAlarmDlg::i18n_chk_ConfirmAck() { return i18nc("@option:check", "Confirm acknowledgment"); } /****************************************************************************** * Constructor. * Parameters: * Template = true to edit/create an alarm template * = false to edit/create an alarm. * event != to initialise the dialog to show the specified event's data. */ EditDisplayAlarmDlg::EditDisplayAlarmDlg(bool Template, QWidget* parent, GetResourceType getResource) : EditAlarmDlg(Template, KAEvent::MESSAGE, parent, getResource), mSpecialActionsButton(nullptr), mReminderDeferral(false), mReminderArchived(false) { - qCDebug(KALARM_LOG) << "New"; + qCDebug(KALARM_LOG) << "EditDisplayAlarmDlg: New"; init(nullptr); } EditDisplayAlarmDlg::EditDisplayAlarmDlg(bool Template, const KAEvent* event, bool newAlarm, QWidget* parent, GetResourceType getResource, bool readOnly) : EditAlarmDlg(Template, event, newAlarm, parent, getResource, readOnly), mSpecialActionsButton(nullptr), mReminderDeferral(false), mReminderArchived(false) { - qCDebug(KALARM_LOG) << "Event.id()"; + qCDebug(KALARM_LOG) << "EditDisplayAlarmDlg: Event.id()"; init(event); } /****************************************************************************** * Return the window caption. */ QString EditDisplayAlarmDlg::type_caption() const { return isTemplate() ? (isNewAlarm() ? i18nc("@title:window", "New Display Alarm Template") : i18nc("@title:window", "Edit Display Alarm Template")) : (isNewAlarm() ? i18nc("@title:window", "New Display Alarm") : i18nc("@title:window", "Edit Display Alarm")); } /****************************************************************************** * Set up the dialog controls common to display alarms. */ void EditDisplayAlarmDlg::type_init(QWidget* parent, QVBoxLayout* frameLayout) { // Display type combo box QWidget* box = new QWidget(parent); // to group widgets for QWhatsThis text QHBoxLayout* boxHLayout = new QHBoxLayout(box); boxHLayout->setMargin(0); QLabel* label = new QLabel(i18nc("@label:listbox", "Display type:"), box); boxHLayout->addWidget(label); label->setFixedSize(label->sizeHint()); mTypeCombo = new ComboBox(box); boxHLayout->addWidget(mTypeCombo); QString textItem = i18nc("@item:inlistbox", "Text message"); QString fileItem = i18nc("@item:inlistbox", "File contents"); QString commandItem = i18nc("@item:inlistbox", "Command output"); mTypeCombo->addItem(textItem); // index = tTEXT mTypeCombo->addItem(fileItem); // index = tFILE mTypeCombo->addItem(commandItem); // index = tCOMMAND mTypeCombo->setFixedSize(mTypeCombo->sizeHint()); mTypeCombo->setCurrentIndex(-1); // ensure slotAlarmTypeChanged() is called when index is set if (!ShellProcess::authorised()) { // User not authorised to issue shell commands - disable Command Output option QStandardItemModel* model = qobject_cast(mTypeCombo->model()); if (model) { QModelIndex index = model->index(2, mTypeCombo->modelColumn(), mTypeCombo->rootModelIndex()); QStandardItem* item = model->itemFromIndex(index); if (item) item->setEnabled(false); } } connect(mTypeCombo, static_cast(&ComboBox::currentIndexChanged), this, &EditDisplayAlarmDlg::slotAlarmTypeChanged); connect(mTypeCombo, static_cast(&ComboBox::currentIndexChanged), this, &EditDisplayAlarmDlg::contentsChanged); label->setBuddy(mTypeCombo); box->setWhatsThis(xi18nc("@info:whatsthis", "Select what the alarm should display:" "%1: the alarm will display the text message you type in." "%2: the alarm will display the contents of a text or image file." "%3: the alarm will display the output from a command.", textItem, fileItem, commandItem)); boxHLayout->setStretchFactor(new QWidget(box), 1); // left adjust the control frameLayout->addWidget(box); // Text message edit box mTextMessageEdit = new TextEdit(parent); mTextMessageEdit->setLineWrapMode(KTextEdit::NoWrap); mTextMessageEdit->setWhatsThis(i18nc("@info:whatsthis", "Enter the text of the alarm message. It may be multi-line.")); connect(mTextMessageEdit, &TextEdit::textChanged, this, &EditDisplayAlarmDlg::contentsChanged); frameLayout->addWidget(mTextMessageEdit); // File name edit box mFileBox = new QWidget(parent); frameLayout->addWidget(mFileBox); QHBoxLayout* fileBoxHLayout = new QHBoxLayout(mFileBox); fileBoxHLayout->setMargin(0); fileBoxHLayout->setSpacing(0); mFileMessageEdit = new LineEdit(LineEdit::Url, mFileBox); fileBoxHLayout->addWidget(mFileMessageEdit); mFileMessageEdit->setAcceptDrops(true); mFileMessageEdit->setWhatsThis(i18nc("@info:whatsthis", "Enter the name or URL of a text or image file to display.")); connect(mFileMessageEdit, &LineEdit::textChanged, this, &EditDisplayAlarmDlg::contentsChanged); // File browse button mFileBrowseButton = new QPushButton(mFileBox); fileBoxHLayout->addWidget(mFileBrowseButton); mFileBrowseButton->setIcon(QIcon::fromTheme(QStringLiteral("document-open"))); int size = mFileBrowseButton->sizeHint().height(); mFileBrowseButton->setFixedSize(size, size); mFileBrowseButton->setToolTip(i18nc("@info:tooltip", "Choose a file")); mFileBrowseButton->setWhatsThis(i18nc("@info:whatsthis", "Select a text or image file to display.")); connect(mFileBrowseButton, &QPushButton::clicked, this, &EditDisplayAlarmDlg::slotPickFile); // Command type checkbox and edit box mCmdEdit = new CommandEdit(parent); connect(mCmdEdit, &CommandEdit::scriptToggled, this, &EditDisplayAlarmDlg::slotCmdScriptToggled); connect(mCmdEdit, &CommandEdit::changed, this, &EditDisplayAlarmDlg::contentsChanged); frameLayout->addWidget(mCmdEdit); // Sound checkbox and file selector QHBoxLayout* hlayout = new QHBoxLayout(); hlayout->setMargin(0); frameLayout->addLayout(hlayout); mSoundPicker = new SoundPicker(parent); mSoundPicker->setFixedSize(mSoundPicker->sizeHint()); connect(mSoundPicker, &SoundPicker::changed, this, &EditDisplayAlarmDlg::contentsChanged); hlayout->addWidget(mSoundPicker); hlayout->addSpacing(2 * style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); hlayout->addStretch(); // Font and colour choice button and sample text mFontColourButton = new FontColourButton(parent); mFontColourButton->setMaximumHeight(mFontColourButton->sizeHint().height() * 3/2); hlayout->addWidget(mFontColourButton); connect(mFontColourButton, &FontColourButton::selected, this, &EditDisplayAlarmDlg::setColours); connect(mFontColourButton, &FontColourButton::selected, this, &EditDisplayAlarmDlg::contentsChanged); if (ShellProcess::authorised()) // don't display if shell commands not allowed (e.g. kiosk mode) { // Special actions button mSpecialActionsButton = new SpecialActionsButton(false, parent); mSpecialActionsButton->setFixedSize(mSpecialActionsButton->sizeHint()); connect(mSpecialActionsButton, &SpecialActionsButton::selected, this, &EditDisplayAlarmDlg::contentsChanged); frameLayout->addWidget(mSpecialActionsButton, 0, Qt::AlignRight); } // Top-adjust the controls mFilePadding = new QWidget(parent); hlayout = new QHBoxLayout(mFilePadding); hlayout->setMargin(0); hlayout->setSpacing(0); frameLayout->addWidget(mFilePadding); frameLayout->setStretchFactor(mFilePadding, 1); } /****************************************************************************** * Create a reminder control. */ Reminder* EditDisplayAlarmDlg::createReminder(QWidget* parent) { return new Reminder(i18nc("@info:whatsthis", "Check to additionally display a reminder in advance of or after the main alarm time(s)."), xi18nc("@info:whatsthis", "Enter how long in advance of or after the main alarm to display a reminder alarm.%1", TimeSpinBox::shiftWhatsThis()), i18nc("@info:whatsthis", "Select whether the reminder should be triggered before or after the main alarm"), true, true, parent); } /****************************************************************************** * Create an "acknowledgement confirmation required" checkbox. */ CheckBox* EditDisplayAlarmDlg::createConfirmAckCheckbox(QWidget* parent) { CheckBox* confirmAck = new CheckBox(i18n_chk_ConfirmAck(), parent); confirmAck->setWhatsThis(i18nc("@info:whatsthis", "Check to be prompted for confirmation when you acknowledge the alarm.")); return confirmAck; } /****************************************************************************** * Initialise the dialog controls from the specified event. */ void EditDisplayAlarmDlg::type_initValues(const KAEvent* event) { mKMailSerialNumber = 0; lateCancel()->showAutoClose(true); if (event) { if (mAlarmType == KAEvent::MESSAGE && event->kmailSerialNumber() && AlarmText::checkIfEmail(event->cleanText())) mKMailSerialNumber = event->kmailSerialNumber(); lateCancel()->setAutoClose(event->autoClose()); if (event->useDefaultFont()) mFontColourButton->setDefaultFont(); else mFontColourButton->setFont(event->font()); mFontColourButton->setBgColour(event->bgColour()); mFontColourButton->setFgColour(event->fgColour()); setColours(event->fgColour(), event->bgColour()); mConfirmAck->setChecked(event->confirmAck()); bool recurs = event->recurs(); int reminderMins = event->reminderMinutes(); if (reminderMins > 0 && !event->reminderActive()) reminderMins = 0; // don't show advance reminder which has already passed if (!reminderMins) { if (event->reminderDeferral() && !recurs) { reminderMins = event->deferDateTime().minsTo(event->mainDateTime()); mReminderDeferral = true; } else if (event->reminderMinutes() && recurs) { reminderMins = event->reminderMinutes(); mReminderArchived = true; } } reminder()->setMinutes(reminderMins, dateOnly()); reminder()->setOnceOnly(event->reminderOnceOnly()); reminder()->enableOnceOnly(recurs); if (mSpecialActionsButton) mSpecialActionsButton->setActions(event->preAction(), event->postAction(), event->extraActionOptions()); Preferences::SoundType soundType = event->speak() ? Preferences::Sound_Speak : event->beep() ? Preferences::Sound_Beep : !event->audioFile().isEmpty() ? Preferences::Sound_File : Preferences::Sound_None; mSoundPicker->set(soundType, event->audioFile(), event->soundVolume(), event->fadeVolume(), event->fadeSeconds(), event->repeatSoundPause()); } else { // Set the values to their defaults if (!ShellProcess::authorised()) { // Don't allow shell commands in kiosk mode if (mSpecialActionsButton) mSpecialActionsButton->setEnabled(false); } lateCancel()->setAutoClose(Preferences::defaultAutoClose()); mTypeCombo->setCurrentIndex(0); mFontColourButton->setDefaultFont(); mFontColourButton->setBgColour(Preferences::defaultBgColour()); mFontColourButton->setFgColour(Preferences::defaultFgColour()); setColours(Preferences::defaultFgColour(), Preferences::defaultBgColour()); mConfirmAck->setChecked(Preferences::defaultConfirmAck()); reminder()->setMinutes(0, false); reminder()->enableOnceOnly(isTimedRecurrence()); // must be called after mRecurrenceEdit is set up if (mSpecialActionsButton) { KAEvent::ExtraActionOptions opts(nullptr); if (Preferences::defaultExecPreActionOnDeferral()) opts |= KAEvent::ExecPreActOnDeferral; if (Preferences::defaultCancelOnPreActionError()) opts |= KAEvent::CancelOnPreActError; if (Preferences::defaultDontShowPreActionError()) opts |= KAEvent::DontShowPreActError; mSpecialActionsButton->setActions(Preferences::defaultPreAction(), Preferences::defaultPostAction(), opts); } mSoundPicker->set(Preferences::defaultSoundType(), Preferences::defaultSoundFile(), Preferences::defaultSoundVolume(), -1, 0, (Preferences::defaultSoundRepeat() ? 0 : -1)); } } /****************************************************************************** * Called when the More/Less Options button is clicked. * Show/hide the optional options. */ void EditDisplayAlarmDlg::type_showOptions(bool more) { if (mSpecialActionsButton) { if (more) mSpecialActionsButton->show(); else mSpecialActionsButton->hide(); } } /****************************************************************************** * Called when the font/color button has been clicked. * Set the colors in the message text entry control. */ void EditDisplayAlarmDlg::setColours(const QColor& fgColour, const QColor& bgColour) { QPalette pal = mTextMessageEdit->palette(); pal.setColor(mTextMessageEdit->backgroundRole(), bgColour); pal.setColor(QPalette::Text, fgColour); mTextMessageEdit->setPalette(pal); pal = mTextMessageEdit->viewport()->palette(); pal.setColor(mTextMessageEdit->viewport()->backgroundRole(), bgColour); pal.setColor(QPalette::Text, fgColour); mTextMessageEdit->viewport()->setPalette(pal); // Change the color of existing text QTextCursor cursor = mTextMessageEdit->textCursor(); mTextMessageEdit->selectAll(); mTextMessageEdit->setTextColor(fgColour); mTextMessageEdit->setTextCursor(cursor); } /****************************************************************************** * Set the dialog's action and the action's text. */ void EditDisplayAlarmDlg::setAction(KAEvent::SubAction action, const AlarmText& alarmText) { QString text = alarmText.displayText(); switch (action) { case KAEvent::MESSAGE: mTypeCombo->setCurrentIndex(tTEXT); mTextMessageEdit->setPlainText(text); mKMailSerialNumber = alarmText.isEmail() ? alarmText.kmailSerialNumber() : 0; break; case KAEvent::FILE: mTypeCombo->setCurrentIndex(tFILE); mFileMessageEdit->setText(text); break; case KAEvent::COMMAND: mTypeCombo->setCurrentIndex(tCOMMAND); mCmdEdit->setText(alarmText); break; default: Q_ASSERT(0); break; } } /****************************************************************************** * Initialise various values in the New Alarm dialogue. */ void EditDisplayAlarmDlg::setBgColour(const QColor& colour) { mFontColourButton->setBgColour(colour); setColours(mFontColourButton->fgColour(), colour); } void EditDisplayAlarmDlg::setFgColour(const QColor& colour) { mFontColourButton->setFgColour(colour); setColours(colour, mFontColourButton->bgColour()); } void EditDisplayAlarmDlg::setConfirmAck(bool confirm) { mConfirmAck->setChecked(confirm); } void EditDisplayAlarmDlg::setAutoClose(bool close) { lateCancel()->setAutoClose(close); } void EditDisplayAlarmDlg::setAudio(Preferences::SoundType type, const QString& file, float volume, int repeatPause) { mSoundPicker->set(type, file, volume, -1, 0, repeatPause); } void EditDisplayAlarmDlg::setReminder(int minutes, bool onceOnly) { reminder()->setMinutes(minutes, dateOnly()); reminder()->setOnceOnly(onceOnly); reminder()->enableOnceOnly(isTimedRecurrence()); } /****************************************************************************** * Set the read-only status of all non-template controls. */ void EditDisplayAlarmDlg::setReadOnly(bool readOnly) { mTypeCombo->setReadOnly(readOnly); mTextMessageEdit->setReadOnly(readOnly); mFileMessageEdit->setReadOnly(readOnly); mCmdEdit->setReadOnly(readOnly); mFontColourButton->setReadOnly(readOnly); mSoundPicker->setReadOnly(readOnly); mConfirmAck->setReadOnly(readOnly); reminder()->setReadOnly(readOnly); if (mSpecialActionsButton) mSpecialActionsButton->setReadOnly(readOnly); if (readOnly) mFileBrowseButton->hide(); else mFileBrowseButton->show(); EditAlarmDlg::setReadOnly(readOnly); } /****************************************************************************** * Save the state of all controls. */ void EditDisplayAlarmDlg::saveState(const KAEvent* event) { EditAlarmDlg::saveState(event); mSavedType = mTypeCombo->currentIndex(); mSavedCmdScript = mCmdEdit->isScript(); mSavedSoundType = mSoundPicker->sound(); mSavedSoundFile = mSoundPicker->file(); mSavedSoundVolume = mSoundPicker->volume(mSavedSoundFadeVolume, mSavedSoundFadeSeconds); mSavedRepeatPause = mSoundPicker->repeatPause(); mSavedConfirmAck = mConfirmAck->isChecked(); mSavedFont = mFontColourButton->font(); mSavedFgColour = mFontColourButton->fgColour(); mSavedBgColour = mFontColourButton->bgColour(); mSavedReminder = reminder()->minutes(); mSavedOnceOnly = reminder()->isOnceOnly(); mSavedAutoClose = lateCancel()->isAutoClose(); if (mSpecialActionsButton) { mSavedPreAction = mSpecialActionsButton->preAction(); mSavedPostAction = mSpecialActionsButton->postAction(); mSavedPreActionOptions = mSpecialActionsButton->options(); } } /****************************************************************************** * Check whether any of the controls has changed state since the dialog was * first displayed. * Reply = true if any controls have changed, or if it's a new event. * = false if no controls have changed. */ bool EditDisplayAlarmDlg::type_stateChanged() const { if (mSavedType != mTypeCombo->currentIndex() || mSavedCmdScript != mCmdEdit->isScript() || mSavedSoundType != mSoundPicker->sound() || mSavedConfirmAck != mConfirmAck->isChecked() || mSavedFont != mFontColourButton->font() || mSavedFgColour != mFontColourButton->fgColour() || mSavedBgColour != mFontColourButton->bgColour() || mSavedReminder != reminder()->minutes() || mSavedOnceOnly != reminder()->isOnceOnly() || mSavedAutoClose != lateCancel()->isAutoClose()) return true; if (mSpecialActionsButton) { if (mSavedPreAction != mSpecialActionsButton->preAction() || mSavedPostAction != mSpecialActionsButton->postAction() || mSavedPreActionOptions != mSpecialActionsButton->options()) return true; } if (mSavedSoundType == Preferences::Sound_File) { if (mSavedSoundFile != mSoundPicker->file()) return true; if (!mSavedSoundFile.isEmpty()) { float fadeVolume; int fadeSecs; if (mSavedRepeatPause != mSoundPicker->repeatPause() || mSavedSoundVolume != mSoundPicker->volume(fadeVolume, fadeSecs) || mSavedSoundFadeVolume != fadeVolume || mSavedSoundFadeSeconds != fadeSecs) return true; } } return false; } /****************************************************************************** * Extract the data in the dialog specific to the alarm type and set up a * KAEvent from it. */ void EditDisplayAlarmDlg::type_setEvent(KAEvent& event, const KADateTime& dt, const QString& text, int lateCancel, bool trial) { KAEvent::SubAction type; switch (mTypeCombo->currentIndex()) { case tFILE: type = KAEvent::FILE; break; case tCOMMAND: type = KAEvent::COMMAND; break; default: case tTEXT: type = KAEvent::MESSAGE; break; } event.set(dt, text, mFontColourButton->bgColour(), mFontColourButton->fgColour(), mFontColourButton->font(), type, lateCancel, getAlarmFlags()); if (type == KAEvent::MESSAGE) { if (AlarmText::checkIfEmail(text)) event.setKMailSerialNumber(mKMailSerialNumber); } float fadeVolume; int fadeSecs; float volume = mSoundPicker->volume(fadeVolume, fadeSecs); int repeatPause = mSoundPicker->repeatPause(); event.setAudioFile(mSoundPicker->file().toDisplayString(), volume, fadeVolume, fadeSecs, repeatPause); if (!trial && reminder()->isEnabled()) event.setReminder(reminder()->minutes(), reminder()->isOnceOnly()); if (mSpecialActionsButton && mSpecialActionsButton->isEnabled()) event.setActions(mSpecialActionsButton->preAction(), mSpecialActionsButton->postAction(), mSpecialActionsButton->options()); } /****************************************************************************** * Get the currently specified alarm flag bits. */ KAEvent::Flags EditDisplayAlarmDlg::getAlarmFlags() const { bool cmd = (mTypeCombo->currentIndex() == tCOMMAND); KAEvent::Flags flags = EditAlarmDlg::getAlarmFlags(); if (mSoundPicker->sound() == Preferences::Sound_Beep) flags |= KAEvent::BEEP; if (mSoundPicker->sound() == Preferences::Sound_Speak) flags |= KAEvent::SPEAK; if (mSoundPicker->repeatPause() >= 0) flags |= KAEvent::REPEAT_SOUND; if (mConfirmAck->isChecked()) flags |= KAEvent::CONFIRM_ACK; if (lateCancel()->isAutoClose()) flags |= KAEvent::AUTO_CLOSE; if (mFontColourButton->defaultFont()) flags |= KAEvent::DEFAULT_FONT; if (cmd) flags |= KAEvent::DISPLAY_COMMAND; if (cmd && mCmdEdit->isScript()) flags |= KAEvent::SCRIPT; return flags; } /****************************************************************************** * Called when one of the alarm display type combo box is changed, to display * the appropriate set of controls for that action type. */ void EditDisplayAlarmDlg::slotAlarmTypeChanged(int index) { QWidget* focus = nullptr; switch (index) { case tTEXT: // text message mFileBox->hide(); mFilePadding->hide(); mCmdEdit->hide(); mTextMessageEdit->show(); mSoundPicker->showSpeak(true); mTryButton->setWhatsThis(i18nc("@info:whatsthis", "Display the alarm message now")); focus = mTextMessageEdit; break; case tFILE: // file contents mTextMessageEdit->hide(); mFileBox->show(); mFilePadding->show(); mCmdEdit->hide(); mSoundPicker->showSpeak(false); mTryButton->setWhatsThis(i18nc("@info:whatsthis", "Display the file now")); mFileMessageEdit->setNoSelect(); focus = mFileMessageEdit; break; case tCOMMAND: // command output mTextMessageEdit->hide(); mFileBox->hide(); slotCmdScriptToggled(mCmdEdit->isScript()); // show/hide mFilePadding mCmdEdit->show(); mSoundPicker->showSpeak(true); mTryButton->setWhatsThis(i18nc("@info:whatsthis", "Display the command output now")); focus = mCmdEdit; break; } if (focus) focus->setFocus(); } /****************************************************************************** * Called when the file browse button is pressed to select a file to display. */ void EditDisplayAlarmDlg::slotPickFile() { static QString defaultDir; // default directory for file browse button QString file = KAlarm::browseFile(i18nc("@title:window", "Choose Text or Image File to Display"), defaultDir, mFileMessageEdit->text(), QString(), true, this); if (!file.isEmpty()) { mFileMessageEdit->setText(KAlarm::pathOrUrl(file)); contentsChanged(); } } /****************************************************************************** * Called when one of the command type radio buttons is clicked, * to display the appropriate edit field. */ void EditDisplayAlarmDlg::slotCmdScriptToggled(bool on) { if (on) mFilePadding->hide(); else mFilePadding->show(); } /****************************************************************************** * Clean up the alarm text, and if it's a file, check whether it's valid. */ bool EditDisplayAlarmDlg::checkText(QString& result, bool showErrorMessage) const { switch (mTypeCombo->currentIndex()) { case tTEXT: result = mTextMessageEdit->toPlainText(); break; case tFILE: { QString alarmtext = mFileMessageEdit->text().trimmed(); QUrl url; KAlarm::FileErr err = KAlarm::checkFileExists(alarmtext, url); if (err == KAlarm::FileErr_None) { KFileItem fi(url); switch (KAlarm::fileType(fi.currentMimeType())) { case KAlarm::TextFormatted: case KAlarm::TextPlain: case KAlarm::TextApplication: case KAlarm::Image: break; default: err = KAlarm::FileErr_NotTextImage; break; } } if (err != KAlarm::FileErr_None && showErrorMessage) { mFileMessageEdit->setFocus(); if (!KAlarm::showFileErrMessage(alarmtext, err, KAlarm::FileErr_BlankDisplay, const_cast(this))) return false; } result = alarmtext; break; } case tCOMMAND: result = mCmdEdit->text(const_cast(this), showErrorMessage); if (result.isEmpty()) return false; break; } return true; } /*============================================================================= = Class EditCommandAlarmDlg = Dialog to edit command alarms. =============================================================================*/ QString EditCommandAlarmDlg::i18n_chk_EnterScript() { return i18nc("@option:check", "Enter a script"); } QString EditCommandAlarmDlg::i18n_radio_ExecInTermWindow() { return i18nc("@option:radio", "Execute in terminal window"); } QString EditCommandAlarmDlg::i18n_chk_ExecInTermWindow() { return i18nc("@option:check", "Execute in terminal window"); } /****************************************************************************** * Constructor. * Parameters: * Template = true to edit/create an alarm template * = false to edit/create an alarm. * event != to initialise the dialog to show the specified event's data. */ EditCommandAlarmDlg::EditCommandAlarmDlg(bool Template, QWidget* parent, GetResourceType getResource) : EditAlarmDlg(Template, KAEvent::COMMAND, parent, getResource) { - qCDebug(KALARM_LOG) << "New"; + qCDebug(KALARM_LOG) << "EditCommandAlarmDlg: New"; init(nullptr); } EditCommandAlarmDlg::EditCommandAlarmDlg(bool Template, const KAEvent* event, bool newAlarm, QWidget* parent, GetResourceType getResource, bool readOnly) : EditAlarmDlg(Template, event, newAlarm, parent, getResource, readOnly) { - qCDebug(KALARM_LOG) << "Event.id()"; + qCDebug(KALARM_LOG) << "EditCommandAlarmDlg: Event.id()"; init(event); } /****************************************************************************** * Return the window caption. */ QString EditCommandAlarmDlg::type_caption() const { return isTemplate() ? (isNewAlarm() ? i18nc("@title:window", "New Command Alarm Template") : i18nc("@title:window", "Edit Command Alarm Template")) : (isNewAlarm() ? i18nc("@title:window", "New Command Alarm") : i18nc("@title:window", "Edit Command Alarm")); } /****************************************************************************** * Set up the command alarm dialog controls. */ void EditCommandAlarmDlg::type_init(QWidget* parent, QVBoxLayout* frameLayout) { mTryButton->setWhatsThis(i18nc("@info:whatsthis", "Execute the specified command now")); mCmdEdit = new CommandEdit(parent); connect(mCmdEdit, &CommandEdit::scriptToggled, this, &EditCommandAlarmDlg::slotCmdScriptToggled); connect(mCmdEdit, &CommandEdit::changed, this, &EditCommandAlarmDlg::contentsChanged); frameLayout->addWidget(mCmdEdit); // What to do with command output mCmdOutputBox = new QGroupBox(i18nc("@title:group", "Command Output"), parent); frameLayout->addWidget(mCmdOutputBox); QVBoxLayout* vlayout = new QVBoxLayout(mCmdOutputBox); vlayout->setMargin(style()->pixelMetric(QStyle::PM_DefaultChildMargin)); vlayout->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); mCmdOutputGroup = new ButtonGroup(mCmdOutputBox); connect(mCmdOutputGroup, &ButtonGroup::buttonSet, this, &EditCommandAlarmDlg::contentsChanged); // Execute in terminal window mCmdExecInTerm = new RadioButton(i18n_radio_ExecInTermWindow(), mCmdOutputBox); mCmdExecInTerm->setFixedSize(mCmdExecInTerm->sizeHint()); mCmdExecInTerm->setWhatsThis(i18nc("@info:whatsthis", "Check to execute the command in a terminal window")); mCmdOutputGroup->addButton(mCmdExecInTerm, Preferences::Log_Terminal); vlayout->addWidget(mCmdExecInTerm, 0, Qt::AlignLeft); // Log file name edit box QWidget* box = new QWidget(mCmdOutputBox); QHBoxLayout* boxHLayout = new QHBoxLayout(box); boxHLayout->setMargin(0); boxHLayout->setSpacing(0); (new QWidget(box))->setFixedWidth(mCmdExecInTerm->style()->pixelMetric(QStyle::PM_ExclusiveIndicatorWidth)); // indent the edit box mCmdLogFileEdit = new LineEdit(LineEdit::Url, box); boxHLayout->addWidget(mCmdLogFileEdit); mCmdLogFileEdit->setAcceptDrops(true); mCmdLogFileEdit->setWhatsThis(i18nc("@info:whatsthis", "Enter the name or path of the log file.")); connect(mCmdLogFileEdit, &LineEdit::textChanged, this, &EditCommandAlarmDlg::contentsChanged); // Log file browse button. // The file browser dialog is activated by the PickLogFileRadio class. QPushButton* browseButton = new QPushButton(box); boxHLayout->addWidget(browseButton); browseButton->setIcon(QIcon::fromTheme(QStringLiteral("document-open"))); int size = browseButton->sizeHint().height(); browseButton->setFixedSize(size, size); browseButton->setToolTip(i18nc("@info:tooltip", "Choose a file")); browseButton->setWhatsThis(i18nc("@info:whatsthis", "Select a log file.")); // Log output to file mCmdLogToFile = new PickLogFileRadio(browseButton, mCmdLogFileEdit, i18nc("@option:radio", "Log to file"), mCmdOutputGroup, mCmdOutputBox); mCmdLogToFile->setFixedSize(mCmdLogToFile->sizeHint()); mCmdLogToFile->setWhatsThis(i18nc("@info:whatsthis", "Check to log the command output to a local file. The output will be appended to any existing contents of the file.")); connect(mCmdLogToFile, &PickLogFileRadio::fileChanged, this, &EditCommandAlarmDlg::contentsChanged); mCmdOutputGroup->addButton(mCmdLogToFile, Preferences::Log_File); vlayout->addWidget(mCmdLogToFile, 0, Qt::AlignLeft); vlayout->addWidget(box); // Discard output mCmdDiscardOutput = new RadioButton(i18nc("@option:radio", "Discard"), mCmdOutputBox); mCmdDiscardOutput->setFixedSize(mCmdDiscardOutput->sizeHint()); mCmdDiscardOutput->setWhatsThis(i18nc("@info:whatsthis", "Check to discard command output.")); mCmdOutputGroup->addButton(mCmdDiscardOutput, Preferences::Log_Discard); vlayout->addWidget(mCmdDiscardOutput, 0, Qt::AlignLeft); // Top-adjust the controls mCmdPadding = new QWidget(parent); QHBoxLayout* hlayout = new QHBoxLayout(mCmdPadding); hlayout->setMargin(0); hlayout->setSpacing(0); frameLayout->addWidget(mCmdPadding); frameLayout->setStretchFactor(mCmdPadding, 1); } /****************************************************************************** * Initialise the dialog controls from the specified event. */ void EditCommandAlarmDlg::type_initValues(const KAEvent* event) { if (event) { // Set the values to those for the specified event RadioButton* logType = event->commandXterm() ? mCmdExecInTerm : !event->logFile().isEmpty() ? mCmdLogToFile : mCmdDiscardOutput; if (logType == mCmdLogToFile) mCmdLogFileEdit->setText(event->logFile()); // set file name before setting radio button logType->setChecked(true); } else { // Set the values to their defaults mCmdEdit->setScript(Preferences::defaultCmdScript()); mCmdLogFileEdit->setText(Preferences::defaultCmdLogFile()); // set file name before setting radio button mCmdOutputGroup->setButton(Preferences::defaultCmdLogType()); } slotCmdScriptToggled(mCmdEdit->isScript()); } /****************************************************************************** * Called when the More/Less Options button is clicked. * Show/hide the optional options. */ void EditCommandAlarmDlg::type_showOptions(bool more) { if (more) mCmdOutputBox->show(); else mCmdOutputBox->hide(); } /****************************************************************************** * Set the dialog's action and the action's text. */ void EditCommandAlarmDlg::setAction(KAEvent::SubAction action, const AlarmText& alarmText) { Q_UNUSED(action); Q_ASSERT(action == KAEvent::COMMAND); mCmdEdit->setText(alarmText); } /****************************************************************************** * Set the read-only status of all non-template controls. */ void EditCommandAlarmDlg::setReadOnly(bool readOnly) { if (!isTemplate() && !ShellProcess::authorised()) readOnly = true; // don't allow editing of existing command alarms in kiosk mode mCmdEdit->setReadOnly(readOnly); mCmdExecInTerm->setReadOnly(readOnly); mCmdLogToFile->setReadOnly(readOnly); mCmdDiscardOutput->setReadOnly(readOnly); EditAlarmDlg::setReadOnly(readOnly); } /****************************************************************************** * Save the state of all controls. */ void EditCommandAlarmDlg::saveState(const KAEvent* event) { EditAlarmDlg::saveState(event); mSavedCmdScript = mCmdEdit->isScript(); mSavedCmdOutputRadio = mCmdOutputGroup->checkedButton(); mSavedCmdLogFile = mCmdLogFileEdit->text(); } /****************************************************************************** * Check whether any of the controls has changed state since the dialog was * first displayed. * Reply = true if any controls have changed, or if it's a new event. * = false if no controls have changed. */ bool EditCommandAlarmDlg::type_stateChanged() const { if (mSavedCmdScript != mCmdEdit->isScript() || mSavedCmdOutputRadio != mCmdOutputGroup->checkedButton()) return true; if (mCmdOutputGroup->checkedButton() == mCmdLogToFile) { if (mSavedCmdLogFile != mCmdLogFileEdit->text()) return true; } return false; } /****************************************************************************** * Extract the data in the dialog specific to the alarm type and set up a * KAEvent from it. */ void EditCommandAlarmDlg::type_setEvent(KAEvent& event, const KADateTime& dt, const QString& text, int lateCancel, bool trial) { Q_UNUSED(trial); event.set(dt, text, QColor(), QColor(), QFont(), KAEvent::COMMAND, lateCancel, getAlarmFlags()); if (mCmdOutputGroup->checkedButton() == mCmdLogToFile) event.setLogFile(mCmdLogFileEdit->text()); } /****************************************************************************** * Get the currently specified alarm flag bits. */ KAEvent::Flags EditCommandAlarmDlg::getAlarmFlags() const { KAEvent::Flags flags = EditAlarmDlg::getAlarmFlags(); if (mCmdEdit->isScript()) flags |= KAEvent::SCRIPT; if (mCmdOutputGroup->checkedButton() == mCmdExecInTerm) flags |= KAEvent::EXEC_IN_XTERM; return flags; } /****************************************************************************** * Validate and convert command alarm data. */ bool EditCommandAlarmDlg::type_validate(bool trial) { Q_UNUSED(trial); if (mCmdOutputGroup->checkedButton() == mCmdLogToFile) { // Validate the log file name QString file = mCmdLogFileEdit->text(); QFileInfo info(file); QDir::setCurrent(QDir::homePath()); bool err = file.isEmpty() || info.isDir(); if (!err) { if (info.exists()) { err = !info.isWritable(); } else { QFileInfo dirinfo(info.absolutePath()); // get absolute directory path err = (!dirinfo.isDir() || !dirinfo.isWritable()); } } if (err) { showMainPage(); mCmdLogFileEdit->setFocus(); KAMessageBox::sorry(this, i18nc("@info", "Log file must be the name or path of a local file, with write permission.")); return false; } // Convert the log file to an absolute path mCmdLogFileEdit->setText(info.absoluteFilePath()); } else if (mCmdOutputGroup->checkedButton() == mCmdExecInTerm) { if (KAMessageBox::warningContinueCancel(this, xi18nc("@info", "No terminal is selected for command alarms." "Please set it in the KAlarm Configuration dialog.")) != KMessageBox::Continue) return false; } return true; } /****************************************************************************** * Called when the Try action has been executed. * Tell the user the result of the Try action. */ void EditCommandAlarmDlg::type_executedTry(const QString& text, void* result) { ShellProcess* proc = (ShellProcess*)result; if (proc && proc != (void*)-1 && mCmdOutputGroup->checkedButton() != mCmdExecInTerm) { theApp()->commandMessage(proc, this); KAMessageBox::information(this, xi18nc("@info", "Command executed: %1", text)); theApp()->commandMessage(proc, nullptr); } } /****************************************************************************** * Called when one of the command type radio buttons is clicked, * to display the appropriate edit field. */ void EditCommandAlarmDlg::slotCmdScriptToggled(bool on) { if (on) mCmdPadding->hide(); else mCmdPadding->show(); } /****************************************************************************** * Clean up the alarm text. */ bool EditCommandAlarmDlg::checkText(QString& result, bool showErrorMessage) const { result = mCmdEdit->text(const_cast(this), showErrorMessage); if (result.isEmpty()) return false; return true; } /*============================================================================= = Class EditEmailAlarmDlg = Dialog to edit email alarms. =============================================================================*/ QString EditEmailAlarmDlg::i18n_chk_CopyEmailToSelf() { return i18nc("@option:check", "Copy email to self"); } /****************************************************************************** * Constructor. * Parameters: * Template = true to edit/create an alarm template * = false to edit/create an alarm. * event != to initialise the dialog to show the specified event's data. */ EditEmailAlarmDlg::EditEmailAlarmDlg(bool Template, QWidget* parent, GetResourceType getResource) : EditAlarmDlg(Template, KAEvent::EMAIL, parent, getResource), mEmailRemoveButton(nullptr) { - qCDebug(KALARM_LOG) << "New"; + qCDebug(KALARM_LOG) << "EditEmailAlarmDlg: New"; init(nullptr); } EditEmailAlarmDlg::EditEmailAlarmDlg(bool Template, const KAEvent* event, bool newAlarm, QWidget* parent, GetResourceType getResource, bool readOnly) : EditAlarmDlg(Template, event, newAlarm, parent, getResource, readOnly), mEmailRemoveButton(nullptr) { - qCDebug(KALARM_LOG) << "Event.id()"; + qCDebug(KALARM_LOG) << "EditEmailAlarmDlg: Event.id()"; init(event); } /****************************************************************************** * Return the window caption. */ QString EditEmailAlarmDlg::type_caption() const { return isTemplate() ? (isNewAlarm() ? i18nc("@title:window", "New Email Alarm Template") : i18nc("@title:window", "Edit Email Alarm Template")) : (isNewAlarm() ? i18nc("@title:window", "New Email Alarm") : i18nc("@title:window", "Edit Email Alarm")); } /****************************************************************************** * Set up the email alarm dialog controls. */ void EditEmailAlarmDlg::type_init(QWidget* parent, QVBoxLayout* frameLayout) { mTryButton->setWhatsThis(i18nc("@info:whatsthis", "Send the email to the specified addressees now")); QGridLayout* grid = new QGridLayout(); grid->setMargin(0); grid->setColumnStretch(1, 1); frameLayout->addLayout(grid); mEmailFromList = nullptr; if (Preferences::emailFrom() == Preferences::MAIL_FROM_KMAIL) { // Email sender identity QLabel* label = new QLabel(i18nc("@label:listbox 'From' email address", "From:"), parent); label->setFixedSize(label->sizeHint()); grid->addWidget(label, 0, 0); mEmailFromList = new EmailIdCombo(Identities::identityManager(), parent); mEmailFromList->setMinimumSize(mEmailFromList->sizeHint()); label->setBuddy(mEmailFromList); mEmailFromList->setWhatsThis(i18nc("@info:whatsthis", "Your email identity, used to identify you as the sender when sending email alarms.")); connect(mEmailFromList, &EmailIdCombo::identityChanged, this, &EditEmailAlarmDlg::contentsChanged); grid->addWidget(mEmailFromList, 0, 1, 1, 2); } // Email recipients QLabel* label = new QLabel(i18nc("@label:textbox Email addressee", "To:"), parent); label->setFixedSize(label->sizeHint()); grid->addWidget(label, 1, 0); mEmailToEdit = new LineEdit(LineEdit::Emails, parent); mEmailToEdit->setMinimumSize(mEmailToEdit->sizeHint()); mEmailToEdit->setWhatsThis(i18nc("@info:whatsthis", "Enter the addresses of the email recipients. Separate multiple addresses by " "commas or semicolons.")); connect(mEmailToEdit, &LineEdit::textChanged, this, &EditEmailAlarmDlg::contentsChanged); grid->addWidget(mEmailToEdit, 1, 1); mEmailAddressButton = new QPushButton(parent); mEmailAddressButton->setIcon(QIcon::fromTheme(QStringLiteral("help-contents"))); int size = mEmailAddressButton->sizeHint().height(); mEmailAddressButton->setFixedSize(size, size); connect(mEmailAddressButton, &QPushButton::clicked, this, &EditEmailAlarmDlg::openAddressBook); mEmailAddressButton->setToolTip(i18nc("@info:tooltip", "Open address book")); mEmailAddressButton->setWhatsThis(i18nc("@info:whatsthis", "Select email addresses from your address book.")); grid->addWidget(mEmailAddressButton, 1, 2); // Email subject label = new QLabel(i18nc("@label:textbox Email subject", "Subject:"), parent); label->setFixedSize(label->sizeHint()); grid->addWidget(label, 2, 0); mEmailSubjectEdit = new LineEdit(parent); mEmailSubjectEdit->setMinimumSize(mEmailSubjectEdit->sizeHint()); label->setBuddy(mEmailSubjectEdit); mEmailSubjectEdit->setWhatsThis(i18nc("@info:whatsthis", "Enter the email subject.")); connect(mEmailSubjectEdit, &LineEdit::textChanged, this, &EditEmailAlarmDlg::contentsChanged); grid->addWidget(mEmailSubjectEdit, 2, 1, 1, 2); // Email body mEmailMessageEdit = new TextEdit(parent); mEmailMessageEdit->setWhatsThis(i18nc("@info:whatsthis", "Enter the email message.")); connect(mEmailMessageEdit, &TextEdit::textChanged, this, &EditEmailAlarmDlg::contentsChanged); frameLayout->addWidget(mEmailMessageEdit); // Email attachments grid = new QGridLayout(); grid->setMargin(0); frameLayout->addLayout(grid); label = new QLabel(i18nc("@label:listbox", "Attachments:"), parent); label->setFixedSize(label->sizeHint()); grid->addWidget(label, 0, 0); mEmailAttachList = new QComboBox(parent); mEmailAttachList->setEditable(true); mEmailAttachList->setMinimumSize(mEmailAttachList->sizeHint()); if (mEmailAttachList->lineEdit()) mEmailAttachList->lineEdit()->setReadOnly(true); label->setBuddy(mEmailAttachList); mEmailAttachList->setWhatsThis(i18nc("@info:whatsthis", "Files to send as attachments to the email.")); grid->addWidget(mEmailAttachList, 0, 1); grid->setColumnStretch(1, 1); mEmailAddAttachButton = new QPushButton(i18nc("@action:button", "Add..."), parent); connect(mEmailAddAttachButton, &QPushButton::clicked, this, &EditEmailAlarmDlg::slotAddAttachment); mEmailAddAttachButton->setWhatsThis(i18nc("@info:whatsthis", "Add an attachment to the email.")); grid->addWidget(mEmailAddAttachButton, 0, 2); mEmailRemoveButton = new QPushButton(i18nc("@action:button", "Remove"), parent); connect(mEmailRemoveButton, &QPushButton::clicked, this, &EditEmailAlarmDlg::slotRemoveAttachment); mEmailRemoveButton->setWhatsThis(i18nc("@info:whatsthis", "Remove the highlighted attachment from the email.")); grid->addWidget(mEmailRemoveButton, 1, 2); // BCC email to sender mEmailBcc = new CheckBox(i18n_chk_CopyEmailToSelf(), parent); mEmailBcc->setFixedSize(mEmailBcc->sizeHint()); mEmailBcc->setWhatsThis(i18nc("@info:whatsthis", "If checked, the email will be blind copied to you.")); connect(mEmailBcc, &CheckBox::toggled, this, &EditEmailAlarmDlg::contentsChanged); grid->addWidget(mEmailBcc, 1, 0, 1, 2, Qt::AlignLeft); } /****************************************************************************** * Initialise the dialog controls from the specified event. */ void EditEmailAlarmDlg::type_initValues(const KAEvent* event) { if (event) { // Set the values to those for the specified event mEmailAttachList->addItems(event->emailAttachments()); mEmailToEdit->setText(event->emailAddresses(QStringLiteral(", "))); mEmailSubjectEdit->setText(event->emailSubject()); mEmailBcc->setChecked(event->emailBcc()); if (mEmailFromList) mEmailFromList->setCurrentIdentity(event->emailFromId()); } else { // Set the values to their defaults mEmailBcc->setChecked(Preferences::defaultEmailBcc()); } attachmentEnable(); } /****************************************************************************** * Enable/disable controls depending on whether any attachments are entered. */ void EditEmailAlarmDlg::attachmentEnable() { bool enable = mEmailAttachList->count(); mEmailAttachList->setEnabled(enable); if (mEmailRemoveButton) mEmailRemoveButton->setEnabled(enable); } /****************************************************************************** * Set the dialog's action and the action's text. */ void EditEmailAlarmDlg::setAction(KAEvent::SubAction action, const AlarmText& alarmText) { Q_UNUSED(action); Q_ASSERT(action == KAEvent::EMAIL); if (alarmText.isEmail()) { mEmailToEdit->setText(alarmText.to()); mEmailSubjectEdit->setText(alarmText.subject()); mEmailMessageEdit->setPlainText(alarmText.body()); } else mEmailMessageEdit->setPlainText(alarmText.displayText()); } /****************************************************************************** * Initialise various values in the New Alarm dialogue. */ void EditEmailAlarmDlg::setEmailFields(uint fromID, const KCalCore::Person::List& addresses, const QString& subject, const QStringList& attachments) { if (fromID && mEmailFromList) mEmailFromList->setCurrentIdentity(fromID); if (!addresses.isEmpty()) mEmailToEdit->setText(KAEvent::joinEmailAddresses(addresses, QStringLiteral(", "))); if (!subject.isEmpty()) mEmailSubjectEdit->setText(subject); if (!attachments.isEmpty()) { mEmailAttachList->addItems(attachments); attachmentEnable(); } } void EditEmailAlarmDlg::setBcc(bool bcc) { mEmailBcc->setChecked(bcc); } /****************************************************************************** * Set the read-only status of all non-template controls. */ void EditEmailAlarmDlg::setReadOnly(bool readOnly) { mEmailToEdit->setReadOnly(readOnly); mEmailSubjectEdit->setReadOnly(readOnly); mEmailMessageEdit->setReadOnly(readOnly); mEmailBcc->setReadOnly(readOnly); if (mEmailFromList) mEmailFromList->setReadOnly(readOnly); if (readOnly) { mEmailAddressButton->hide(); mEmailAddAttachButton->hide(); mEmailRemoveButton->hide(); } else { mEmailAddressButton->show(); mEmailAddAttachButton->show(); mEmailRemoveButton->show(); } EditAlarmDlg::setReadOnly(readOnly); } /****************************************************************************** * Save the state of all controls. */ void EditEmailAlarmDlg::saveState(const KAEvent* event) { EditAlarmDlg::saveState(event); if (mEmailFromList) mSavedEmailFrom = mEmailFromList->currentIdentityName(); mSavedEmailTo = mEmailToEdit->text(); mSavedEmailSubject = mEmailSubjectEdit->text(); mSavedEmailAttach.clear(); for (int i = 0, end = mEmailAttachList->count(); i < end; ++i) mSavedEmailAttach += mEmailAttachList->itemText(i); mSavedEmailBcc = mEmailBcc->isChecked(); } /****************************************************************************** * Check whether any of the controls has changed state since the dialog was * first displayed. * Reply = true if any controls have changed, or if it's a new event. * = false if no controls have changed. */ bool EditEmailAlarmDlg::type_stateChanged() const { QStringList emailAttach; for (int i = 0, end = mEmailAttachList->count(); i < end; ++i) emailAttach += mEmailAttachList->itemText(i); if ((mEmailFromList && mSavedEmailFrom != mEmailFromList->currentIdentityName()) || mSavedEmailTo != mEmailToEdit->text() || mSavedEmailSubject != mEmailSubjectEdit->text() || mSavedEmailAttach != emailAttach || mSavedEmailBcc != mEmailBcc->isChecked()) return true; return false; } /****************************************************************************** * Extract the data in the dialog specific to the alarm type and set up a * KAEvent from it. */ void EditEmailAlarmDlg::type_setEvent(KAEvent& event, const KADateTime& dt, const QString& text, int lateCancel, bool trial) { Q_UNUSED(trial); event.set(dt, text, QColor(), QColor(), QFont(), KAEvent::EMAIL, lateCancel, getAlarmFlags()); uint from = mEmailFromList ? mEmailFromList->currentIdentity() : 0; event.setEmail(from, mEmailAddresses, mEmailSubjectEdit->text(), mEmailAttachments); } /****************************************************************************** * Get the currently specified alarm flag bits. */ KAEvent::Flags EditEmailAlarmDlg::getAlarmFlags() const { KAEvent::Flags flags = EditAlarmDlg::getAlarmFlags(); if (mEmailBcc->isChecked()) flags |= KAEvent::EMAIL_BCC; return flags; } /****************************************************************************** * Convert the email addresses to a list, and validate them. Convert the email * attachments to a list. */ bool EditEmailAlarmDlg::type_validate(bool trial) { QString addrs = mEmailToEdit->text(); if (addrs.isEmpty()) mEmailAddresses.clear(); else { QString bad = KAMail::convertAddresses(addrs, mEmailAddresses); if (!bad.isEmpty()) { mEmailToEdit->setFocus(); KAMessageBox::error(this, xi18nc("@info", "Invalid email address: %1", bad)); return false; } } if (mEmailAddresses.isEmpty()) { mEmailToEdit->setFocus(); KAMessageBox::error(this, i18nc("@info", "No email address specified")); return false; } mEmailAttachments.clear(); for (int i = 0, end = mEmailAttachList->count(); i < end; ++i) { QString att = mEmailAttachList->itemText(i); switch (KAMail::checkAttachment(att)) { case 1: mEmailAttachments.append(att); break; case 0: break; // empty case -1: mEmailAttachList->setFocus(); KAMessageBox::error(this, xi18nc("@info", "Invalid email attachment: %1", att)); return false; } } if (trial && KAMessageBox::warningContinueCancel(this, i18nc("@info", "Do you really want to send the email now to the specified recipient(s)?"), i18nc("@action:button", "Confirm Email"), KGuiItem(i18nc("@action:button", "Send"))) != KMessageBox::Continue) return false; return true; } /****************************************************************************** * Called when the Try action is about to be executed. */ void EditEmailAlarmDlg::type_aboutToTry() { // Disconnect any previous connections, to prevent multiple messages being output disconnect(theApp(), &KAlarmApp::execAlarmSuccess, this, &EditEmailAlarmDlg::slotTrySuccess); connect(theApp(), &KAlarmApp::execAlarmSuccess, this, &EditEmailAlarmDlg::slotTrySuccess); } /****************************************************************************** * Tell the user the result of the Try action. */ void EditEmailAlarmDlg::slotTrySuccess() { disconnect(theApp(), &KAlarmApp::execAlarmSuccess, this, &EditEmailAlarmDlg::slotTrySuccess); QString msg; QString to = KAEvent::joinEmailAddresses(mEmailAddresses, QStringLiteral("")); to.replace(QLatin1Char('<'), QStringLiteral("<")); to.replace(QLatin1Char('>'), QStringLiteral(">")); if (mEmailBcc->isChecked()) msg = QLatin1String("") + xi18nc("@info", "Email sent to:%1Bcc: %2", to, Preferences::emailBccAddress()) + QLatin1String(""); else msg = QLatin1String("") + xi18nc("@info", "Email sent to:%1", to) + QLatin1String(""); KAMessageBox::information(this, msg); } /****************************************************************************** * Get a selection from the Address Book. */ void EditEmailAlarmDlg::openAddressBook() { // Use AutoQPointer to guard against crash on application exit while // the dialogue is still open. It prevents double deletion (both on // deletion of MainWindow, and on return from this function). AutoQPointer dlg = new Akonadi::EmailAddressSelectionDialog(this); if (dlg->exec() != QDialog::Accepted) return; Akonadi::EmailAddressSelection::List selections = dlg->selectedAddresses(); if (selections.isEmpty()) return; Person person(selections.first().name(), selections.first().email()); QString addrs = mEmailToEdit->text().trimmed(); if (!addrs.isEmpty()) addrs += QLatin1String(", "); addrs += person.fullName(); mEmailToEdit->setText(addrs); } /****************************************************************************** * Select a file to attach to the email. */ void EditEmailAlarmDlg::slotAddAttachment() { QString url = KAlarm::browseFile(i18nc("@title:window", "Choose File to Attach"), mAttachDefaultDir, QString(), QString(), true, this); if (!url.isEmpty()) { mEmailAttachList->addItem(url); mEmailAttachList->setCurrentIndex(mEmailAttachList->count() - 1); // select the new item mEmailRemoveButton->setEnabled(true); mEmailAttachList->setEnabled(true); contentsChanged(); } } /****************************************************************************** * Remove the currently selected attachment from the email. */ void EditEmailAlarmDlg::slotRemoveAttachment() { int item = mEmailAttachList->currentIndex(); mEmailAttachList->removeItem(item); int count = mEmailAttachList->count(); if (item >= count) mEmailAttachList->setCurrentIndex(count - 1); if (!count) { mEmailRemoveButton->setEnabled(false); mEmailAttachList->setEnabled(false); } contentsChanged(); } /****************************************************************************** * Clean up the alarm text. */ bool EditEmailAlarmDlg::checkText(QString& result, bool showErrorMessage) const { Q_UNUSED(showErrorMessage); result = mEmailMessageEdit->toPlainText(); return true; } /*============================================================================= = Class EditAudioAlarmDlg = Dialog to edit audio alarms with no display window. =============================================================================*/ /****************************************************************************** * Constructor. * Parameters: * Template = true to edit/create an alarm template * = false to edit/create an alarm. * event != to initialise the dialog to show the specified event's data. */ EditAudioAlarmDlg::EditAudioAlarmDlg(bool Template, QWidget* parent, GetResourceType getResource) : EditAlarmDlg(Template, KAEvent::AUDIO, parent, getResource), mMessageWin(nullptr) { - qCDebug(KALARM_LOG) << "New"; + qCDebug(KALARM_LOG) << "EditAudioAlarmDlg: New"; init(nullptr); } EditAudioAlarmDlg::EditAudioAlarmDlg(bool Template, const KAEvent* event, bool newAlarm, QWidget* parent, GetResourceType getResource, bool readOnly) : EditAlarmDlg(Template, event, newAlarm, parent, getResource, readOnly), mMessageWin(nullptr) { - qCDebug(KALARM_LOG) << "Event.id()"; + qCDebug(KALARM_LOG) << "EditAudioAlarmDlg: Event.id()"; init(event); mTryButton->setEnabled(!MessageWin::isAudioPlaying()); connect(theApp(), &KAlarmApp::audioPlaying, this, &EditAudioAlarmDlg::slotAudioPlaying); } /****************************************************************************** * Return the window caption. */ QString EditAudioAlarmDlg::type_caption() const { return isTemplate() ? (isNewAlarm() ? i18nc("@title:window", "New Audio Alarm Template") : i18nc("@title:window", "Edit Audio Alarm Template")) : (isNewAlarm() ? i18nc("@title:window", "New Audio Alarm") : i18nc("@title:window", "Edit Audio Alarm")); } /****************************************************************************** * Set up the dialog controls common to display alarms. */ void EditAudioAlarmDlg::type_init(QWidget* parent, QVBoxLayout* frameLayout) { // File name edit box mSoundConfig = new SoundWidget(false, true, parent); if (isTemplate()) mSoundConfig->setAllowEmptyFile(); connect(mSoundConfig, &SoundWidget::changed, this, &EditAudioAlarmDlg::contentsChanged); frameLayout->addWidget(mSoundConfig); // Top-adjust the controls mPadding = new QWidget(parent); QHBoxLayout* hlayout = new QHBoxLayout(mPadding); hlayout->setMargin(0); hlayout->setSpacing(0); frameLayout->addWidget(mPadding); frameLayout->setStretchFactor(mPadding, 1); } /****************************************************************************** * Initialise the dialog controls from the specified event. */ void EditAudioAlarmDlg::type_initValues(const KAEvent* event) { if (event) { mSoundConfig->set(event->audioFile(), event->soundVolume(), event->fadeVolume(), event->fadeSeconds(), (event->flags() & KAEvent::REPEAT_SOUND) ? event->repeatSoundPause() : -1); } else { // Set the values to their defaults mSoundConfig->set(Preferences::defaultSoundFile(), Preferences::defaultSoundVolume(), -1, 0, (Preferences::defaultSoundRepeat() ? 0 : -1)); } } /****************************************************************************** * Initialise various values in the New Alarm dialogue. */ void EditAudioAlarmDlg::setAudio(const QString& file, float volume) { mSoundConfig->set(file, volume); } /****************************************************************************** * Set the dialog's action and the action's text. */ void EditAudioAlarmDlg::setAction(KAEvent::SubAction action, const AlarmText& alarmText) { Q_UNUSED(action); Q_ASSERT(action == KAEvent::AUDIO); mSoundConfig->set(alarmText.displayText(), Preferences::defaultSoundVolume()); } /****************************************************************************** * Set the read-only status of all non-template controls. */ void EditAudioAlarmDlg::setReadOnly(bool readOnly) { mSoundConfig->setReadOnly(readOnly); EditAlarmDlg::setReadOnly(readOnly); } /****************************************************************************** * Save the state of all controls. */ void EditAudioAlarmDlg::saveState(const KAEvent* event) { EditAlarmDlg::saveState(event); mSavedFile = mSoundConfig->fileName(); mSoundConfig->getVolume(mSavedVolume, mSavedFadeVolume, mSavedFadeSeconds); mSavedRepeatPause = mSoundConfig->repeatPause(); } /****************************************************************************** * Check whether any of the controls has changed state since the dialog was * first displayed. * Reply = true if any controls have changed, or if it's a new event. * = false if no controls have changed. */ bool EditAudioAlarmDlg::type_stateChanged() const { if (mSavedFile != mSoundConfig->fileName()) return true; if (!mSavedFile.isEmpty() || isTemplate()) { float volume, fadeVolume; int fadeSecs; mSoundConfig->getVolume(volume, fadeVolume, fadeSecs); if (mSavedRepeatPause != mSoundConfig->repeatPause() || mSavedVolume != volume || mSavedFadeVolume != fadeVolume || mSavedFadeSeconds != fadeSecs) return true; } return false; } /****************************************************************************** * Extract the data in the dialog specific to the alarm type and set up a * KAEvent from it. */ void EditAudioAlarmDlg::type_setEvent(KAEvent& event, const KADateTime& dt, const QString& text, int lateCancel, bool trial) { Q_UNUSED(text); Q_UNUSED(trial); event.set(dt, QString(), QColor(), QColor(), QFont(), KAEvent::AUDIO, lateCancel, getAlarmFlags()); float volume, fadeVolume; int fadeSecs; mSoundConfig->getVolume(volume, fadeVolume, fadeSecs); int repeatPause = mSoundConfig->repeatPause(); QUrl url; mSoundConfig->file(url, false); event.setAudioFile(url.toString(), volume, fadeVolume, fadeSecs, repeatPause, isTemplate()); } /****************************************************************************** * Get the currently specified alarm flag bits. */ KAEvent::Flags EditAudioAlarmDlg::getAlarmFlags() const { KAEvent::Flags flags = EditAlarmDlg::getAlarmFlags(); if (mSoundConfig->repeatPause() >= 0) flags |= KAEvent::REPEAT_SOUND; return flags; } /****************************************************************************** * Check whether the file name is valid. */ bool EditAudioAlarmDlg::checkText(QString& result, bool showErrorMessage) const { QUrl url; if (!mSoundConfig->file(url, showErrorMessage)) { result.clear(); return false; } result = url.isLocalFile() ? url.toLocalFile() : url.toString(); return true; } /****************************************************************************** * Called when the Try button is clicked. * If the audio file is currently playing (as a result of previously clicking * the Try button), cancel playback. Otherwise, play the audio file. */ void EditAudioAlarmDlg::slotTry() { if (!MessageWin::isAudioPlaying()) EditAlarmDlg::slotTry(); // play the audio file else if (mMessageWin) { mMessageWin->stopAudio(); mMessageWin = nullptr; } } /****************************************************************************** * Called when the Try action has been executed. */ void EditAudioAlarmDlg::type_executedTry(const QString&, void* result) { mMessageWin = (MessageWin*)result; // note which MessageWin controls the audio playback if (mMessageWin) { slotAudioPlaying(true); connect(mMessageWin, &MessageWin::destroyed, this, &EditAudioAlarmDlg::audioWinDestroyed); } } /****************************************************************************** * Called when audio playing starts or stops. * Enable/disable/toggle the Try button. */ void EditAudioAlarmDlg::slotAudioPlaying(bool playing) { if (!playing) { // Nothing is playing, so enable the Try button mTryButton->setEnabled(true); mTryButton->setCheckable(false); mTryButton->setChecked(false); mMessageWin = nullptr; } else if (mMessageWin) { // The test sound file is playing, so enable the Try button and depress it mTryButton->setEnabled(true); mTryButton->setCheckable(true); mTryButton->setChecked(true); } else { // An alarm is playing, so disable the Try button mTryButton->setEnabled(false); mTryButton->setCheckable(false); mTryButton->setChecked(false); } } /*============================================================================= = Class CommandEdit = A widget to allow entry of a command or a command script. =============================================================================*/ CommandEdit::CommandEdit(QWidget* parent) : QWidget(parent) { QVBoxLayout* vlayout = new QVBoxLayout(this); vlayout->setMargin(0); vlayout->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); mTypeScript = new CheckBox(EditCommandAlarmDlg::i18n_chk_EnterScript(), this); mTypeScript->setFixedSize(mTypeScript->sizeHint()); mTypeScript->setWhatsThis(i18nc("@info:whatsthis", "Check to enter the contents of a script instead of a shell command line")); connect(mTypeScript, &CheckBox::toggled, this, &CommandEdit::slotCmdScriptToggled); connect(mTypeScript, &CheckBox::toggled, this, &CommandEdit::changed); vlayout->addWidget(mTypeScript, 0, Qt::AlignLeft); mCommandEdit = new LineEdit(LineEdit::Url, this); mCommandEdit->setWhatsThis(i18nc("@info:whatsthis", "Enter a shell command to execute.")); connect(mCommandEdit, &LineEdit::textChanged, this, &CommandEdit::changed); vlayout->addWidget(mCommandEdit); mScriptEdit = new TextEdit(this); mScriptEdit->setWhatsThis(i18nc("@info:whatsthis", "Enter the contents of a script to execute")); connect(mScriptEdit, &TextEdit::textChanged, this, &CommandEdit::changed); vlayout->addWidget(mScriptEdit); slotCmdScriptToggled(mTypeScript->isChecked()); } /****************************************************************************** * Initialise the widget controls from the specified event. */ void CommandEdit::setScript(bool script) { mTypeScript->setChecked(script); } bool CommandEdit::isScript() const { return mTypeScript->isChecked(); } /****************************************************************************** * Set the widget's text. */ void CommandEdit::setText(const AlarmText& alarmText) { QString text = alarmText.displayText(); bool script = alarmText.isScript(); mTypeScript->setChecked(script); if (script) mScriptEdit->setPlainText(text); else mCommandEdit->setText(KAlarm::pathOrUrl(text)); } /****************************************************************************** * Return the widget's text. */ QString CommandEdit::text() const { QString result; if (mTypeScript->isChecked()) result = mScriptEdit->toPlainText(); else result = mCommandEdit->text(); return result.trimmed(); } /****************************************************************************** * Return the alarm text. * If 'showErrorMessage' is true and the text is empty, an error message is * displayed. */ QString CommandEdit::text(EditAlarmDlg* dlg, bool showErrorMessage) const { QString result = text(); if (showErrorMessage && result.isEmpty()) KAMessageBox::sorry(dlg, i18nc("@info", "Please enter a command or script to execute")); return result; } /****************************************************************************** * Set the read-only status of all controls. */ void CommandEdit::setReadOnly(bool readOnly) { mTypeScript->setReadOnly(readOnly); mCommandEdit->setReadOnly(readOnly); mScriptEdit->setReadOnly(readOnly); } /****************************************************************************** * Called when one of the command type radio buttons is clicked, * to display the appropriate edit field. */ void CommandEdit::slotCmdScriptToggled(bool on) { if (on) { mCommandEdit->hide(); mScriptEdit->show(); mScriptEdit->setFocus(); } else { mScriptEdit->hide(); mCommandEdit->show(); mCommandEdit->setFocus(); } Q_EMIT scriptToggled(on); } /****************************************************************************** * Returns the minimum size of the widget. */ QSize CommandEdit::minimumSizeHint() const { QSize t(mTypeScript->minimumSizeHint()); QSize s(mCommandEdit->minimumSizeHint().expandedTo(mScriptEdit->minimumSizeHint())); s.setHeight(s.height() + style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing) + t.height()); if (s.width() < t.width()) s.setWidth(t.width()); return s; } /*============================================================================= = Class TextEdit = A text edit field with a minimum height of 3 text lines. =============================================================================*/ TextEdit::TextEdit(QWidget* parent) : KTextEdit(parent) { QSize tsize = sizeHint(); tsize.setHeight(fontMetrics().lineSpacing()*13/4 + 2*frameWidth()); setMinimumSize(tsize); } void TextEdit::dragEnterEvent(QDragEnterEvent* e) { if (KCalUtils::ICalDrag::canDecode(e->mimeData())) e->ignore(); // don't accept "text/calendar" objects KTextEdit::dragEnterEvent(e); } // vim: et sw=4: diff --git a/src/eventlistview.cpp b/src/eventlistview.cpp index b143cd3d..a2dea60c 100644 --- a/src/eventlistview.cpp +++ b/src/eventlistview.cpp @@ -1,239 +1,239 @@ /* * eventlistview.cpp - base class for widget showing list of alarms * Program: kalarm * Copyright © 2007-2013 by 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) { 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(); 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(); 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(); 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()); QVariant value = model()->data(index, Qt::ToolTipRole); if (qVariantCanConvert(value)) { 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 int margin = QApplication::style()->pixelMetric(QStyle::PM_FocusFrameHMargin) + 1; int left = columnViewportPosition(index.column()) + margin; 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); + qCDebug(KALARM_LOG) << "EventListDelegate::editorEvent"; ItemListModel* itemModel = qobject_cast(model); if (!itemModel) - qCCritical(KALARM_LOG) << "Invalid cast to ItemListModel*"; + 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/functions.cpp b/src/functions.cpp index 83d53564..b498722a 100644 --- a/src/functions.cpp +++ b/src/functions.cpp @@ -1,2012 +1,2012 @@ /* * functions.cpp - miscellaneous functions * Program: kalarm * Copyright © 2001-2019 by David Jarvie * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kalarm.h" //krazy:exclude=includes (kalarm.h must be first) #include "functions.h" #include "functions_p.h" #include "collectionmodel.h" #include "collectionsearch.h" #include "alarmcalendar.h" #include "alarmtime.h" #include "autoqpointer.h" #include "alarmlistview.h" #include "editdlg.h" #include "kalarmapp.h" #include "kamail.h" #include "mainwindow.h" #include "messagebox.h" #include "messagewin.h" #include "preferences.h" #include "shellprocess.h" #include "templatelistview.h" #include "templatemenuaction.h" #include "config-kalarm.h" #include #include #include #include #include #include using namespace KCalCore; #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if KDEPIM_HAVE_X11 #include #include #include #include #include #include #endif #include #include #include #include #include #include #include #include #include #include #include "kalarm_debug.h" #if KDEPIM_HAVE_X11 #include #include #endif using namespace Akonadi; namespace { bool refreshAlarmsQueued = false; QDBusInterface* korgInterface = nullptr; struct UpdateStatusData { KAlarm::UpdateResult status; // status code and KOrganizer error message if any int warnErr; int warnKOrg; explicit UpdateStatusData(KAlarm::UpdateStatus s = KAlarm::UPDATE_OK) : status(s), warnErr(0), warnKOrg(0) {} // Set an error status and increment to number of errors to warn about void setError(KAlarm::UpdateStatus st, int errorCount = -1) { status.set(st); if (errorCount < 0) ++warnErr; else warnErr = errorCount; } // Update the error status with a KOrganizer related status void korgUpdate(const KAlarm::UpdateResult& result) { if (result.status != KAlarm::UPDATE_OK) { ++warnKOrg; if (result.status > status.status) status = result; } } }; const QLatin1String KMAIL_DBUS_SERVICE("org.kde.kmail"); //const QLatin1String KMAIL_DBUS_IFACE("org.kde.kmail.kmail"); //const QLatin1String KMAIL_DBUS_WINDOW_PATH("/kmail/kmail_mainwindow_1"); const QLatin1String KORG_DBUS_SERVICE("org.kde.korganizer"); const QLatin1String KORG_DBUS_IFACE("org.kde.korganizer.Korganizer"); // D-Bus object path of KOrganizer's notification interface #define KORG_DBUS_PATH "/Korganizer" #define KORG_DBUS_LOAD_PATH "/korganizer_PimApplication" //const QLatin1String KORG_DBUS_WINDOW_PATH("/korganizer/MainWindow_1"); const QLatin1String KORG_MIME_TYPE("application/x-vnd.akonadi.calendar.event"); const QLatin1String KORGANIZER_UID("-korg"); const QLatin1String ALARM_OPTS_FILE("alarmopts"); const char* DONT_SHOW_ERRORS_GROUP = "DontShowErrors"; void editNewTemplate(EditAlarmDlg::Type, const KAEvent* preset, QWidget* parent); void displayUpdateError(QWidget* parent, KAlarm::UpdateError, const UpdateStatusData&, bool showKOrgError = true); KAlarm::UpdateResult sendToKOrganizer(const KAEvent&); KAlarm::UpdateResult deleteFromKOrganizer(const QString& eventID); KAlarm::UpdateResult runKOrganizer(); QString uidKOrganizer(const QString& eventID); } namespace KAlarm { Private* Private::mInstance = nullptr; /****************************************************************************** * Display a main window with the specified event selected. */ MainWindow* displayMainWindowSelected(Akonadi::Item::Id eventId) { MainWindow* win = MainWindow::firstWindow(); if (!win) { if (theApp()->checkCalendar()) // ensure calendar is open { win = MainWindow::create(); win->show(); } } else { // There is already a main window, so make it the active window win->hide(); // in case it's on a different desktop win->setWindowState(win->windowState() & ~Qt::WindowMinimized); win->show(); win->raise(); win->activateWindow(); } if (win && eventId >= 0) win->selectEvent(eventId); return win; } /****************************************************************************** * Create an "Alarms Enabled/Enable Alarms" action. */ KToggleAction* createAlarmEnableAction(QObject* parent) { KToggleAction* action = new KToggleAction(i18nc("@action", "Enable &Alarms"), parent); action->setChecked(theApp()->alarmsEnabled()); QObject::connect(action, &QAction::toggled, theApp(), &KAlarmApp::setAlarmsEnabled); // The following line ensures that all instances are kept in the same state QObject::connect(theApp(), &KAlarmApp::alarmEnabledToggled, action, &QAction::setChecked); return action; } /****************************************************************************** * Create a "Stop Play" action. */ QAction* createStopPlayAction(QObject* parent) { QAction* action = new QAction(QIcon::fromTheme(QStringLiteral("media-playback-stop")), i18nc("@action", "Stop Play"), parent); action->setEnabled(MessageWin::isAudioPlaying()); QObject::connect(action, &QAction::triggered, theApp(), &KAlarmApp::stopAudio); // The following line ensures that all instances are kept in the same state QObject::connect(theApp(), &KAlarmApp::audioPlaying, action, &QAction::setEnabled); return action; } /****************************************************************************** * Create a "Spread Windows" action. */ KToggleAction* createSpreadWindowsAction(QObject* parent) { KToggleAction* action = new KToggleAction(i18nc("@action", "Spread Windows"), parent); QObject::connect(action, &QAction::triggered, theApp(), &KAlarmApp::spreadWindows); // The following line ensures that all instances are kept in the same state QObject::connect(theApp(), &KAlarmApp::spreadWindowsToggled, action, &QAction::setChecked); return action; } /****************************************************************************** * Add a new active (non-archived) alarm. * Save it in the calendar file and add it to every main window instance. * Parameters: msgParent = parent widget for any calendar selection prompt or * error message. * event - is updated with the actual event ID. */ UpdateResult addEvent(KAEvent& event, Collection* calendar, QWidget* msgParent, int options, bool showKOrgErr) { - qCDebug(KALARM_LOG) << event.id(); + 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) << events.count(); + 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) << "No calendar"; + qCDebug(KALARM_LOG) << "addEvents: No calendar"; status.status = UPDATE_FAILED; } } if (status.status == UPDATE_OK) { AlarmCalendar* cal = AlarmCalendar::resources(); for (int i = 0, end = events.count(); i < end; ++i) { // Save the event details in the calendar file, and get the new event ID if (!cal->addEvent(events[i], msgParent, false, &collection)) { status.setError(UPDATE_ERROR); continue; } if (allowKOrgUpdate && events[i].copyToKOrganizer()) { UpdateResult st = sendToKOrganizer(events[i]); // tell KOrganizer to show the event status.korgUpdate(st); } } if (status.warnErr == events.count()) status.status = UPDATE_FAILED; else if (!cal->save()) status.setError(SAVE_FAILED, events.count()); // everything failed } if (status.status != UPDATE_OK && msgParent) displayUpdateError(msgParent, ERR_ADD, status, showKOrgErr); return status.status; } /****************************************************************************** * Save the event in the archived calendar and adjust every main window instance. * The event's ID is changed to an archived ID if necessary. */ bool addArchivedEvent(KAEvent& event, Collection* collection) { - qCDebug(KALARM_LOG) << event.id(); + 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) << event.id(); + 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) << oldEvent.id(); + 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) << event.id(); + qCDebug(KALARM_LOG) << "updateEvent:" << event.id(); if (!event.isValid()) deleteEvent(event, archiveOnDelete); else { // Update the event in the calendar file. AlarmCalendar* cal = AlarmCalendar::resources(); cal->updateEvent(event); if (!cal->save()) { if (msgParent) displayUpdateError(msgParent, ERR_ADD, UpdateStatusData(SAVE_FAILED)); return UpdateResult(SAVE_FAILED); } } return UpdateResult(UPDATE_OK); } /****************************************************************************** * Update a template in the calendar file and in every template list view. * If 'selectionView' is non-null, the selection highlight is moved to the * updated event in that listView instance. */ UpdateResult updateTemplate(KAEvent& event, QWidget* msgParent) { AlarmCalendar* cal = AlarmCalendar::resources(); KAEvent* newEvent = cal->updateEvent(event); UpdateStatus status = UPDATE_OK; if (!newEvent) status = UPDATE_FAILED; else if (!cal->save()) status = SAVE_FAILED; if (status != UPDATE_OK) { if (msgParent) displayUpdateError(msgParent, ERR_TEMPLATE, UpdateStatusData(SAVE_FAILED)); return UpdateResult(status); } return UpdateResult(UPDATE_OK); } /****************************************************************************** * Delete alarms from the calendar file and from every main window instance. * If the events are archived, the events' IDs are changed to archived IDs if necessary. */ UpdateResult deleteEvent(KAEvent& event, bool archive, QWidget* msgParent, bool showKOrgErr) { QVector events(1, event); return deleteEvents(events, archive, msgParent, showKOrgErr); } UpdateResult deleteEvents(QVector& events, bool archive, QWidget* msgParent, bool showKOrgErr) { - qCDebug(KALARM_LOG) << events.count(); + qCDebug(KALARM_LOG) << "deleteEvents:" << events.count(); if (events.isEmpty()) return UpdateResult(UPDATE_OK); UpdateStatusData status; AlarmCalendar* cal = AlarmCalendar::resources(); bool deleteWakeFromSuspendAlarm = false; QString wakeFromSuspendId = checkRtcWakeConfig().value(0); for (int i = 0, end = events.count(); i < end; ++i) { // Save the event details in the calendar file, and get the new event ID KAEvent* event = &events[i]; QString id = event->id(); // Delete the event from the calendar file if (event->category() != CalEvent::ARCHIVED) { if (event->copyToKOrganizer()) { // The event was shown in KOrganizer, so tell KOrganizer to // delete it. But ignore errors, because the user could have // manually deleted it from KOrganizer since it was set up. UpdateResult st = deleteFromKOrganizer(id); status.korgUpdate(st); } if (archive && event->toBeArchived()) { KAEvent ev(*event); addArchivedEvent(ev); // this changes the event ID to an archived ID } } if (!cal->deleteEvent(*event, false)) // don't save calendar after deleting status.setError(UPDATE_ERROR); if (id == wakeFromSuspendId) deleteWakeFromSuspendAlarm = true; // Remove "Don't show error messages again" for this alarm setDontShowErrors(EventId(*event)); } if (status.warnErr == events.count()) status.status = UPDATE_FAILED; else if (!cal->save()) // save the calendars now status.setError(SAVE_FAILED, events.count()); if (status.status != UPDATE_OK && msgParent) displayUpdateError(msgParent, ERR_DELETE, status, showKOrgErr); // Remove any wake-from-suspend scheduled for a deleted alarm if (deleteWakeFromSuspendAlarm && !wakeFromSuspendId.isEmpty()) cancelRtcWake(msgParent, wakeFromSuspendId); return status.status; } /****************************************************************************** * Delete templates from the calendar file and from every template list view. */ UpdateResult deleteTemplates(const KAEvent::List& events, QWidget* msgParent) { int count = events.count(); - qCDebug(KALARM_LOG) << count; + qCDebug(KALARM_LOG) << "deleteTemplates:" << count; if (!count) return UpdateResult(UPDATE_OK); UpdateStatusData status; AlarmCalendar* cal = AlarmCalendar::resources(); for (int i = 0, end = count; i < end; ++i) { // Update the window lists // Delete the template from the calendar file AlarmCalendar* cal = AlarmCalendar::resources(); if (!cal->deleteEvent(*events[i], false)) // don't save calendar after deleting status.setError(UPDATE_ERROR); } if (status.warnErr == count) status.status = UPDATE_FAILED; else if (!cal->save()) // save the calendars now status.setError(SAVE_FAILED, count); if (status.status != UPDATE_OK && msgParent) displayUpdateError(msgParent, ERR_TEMPLATE, status); return status.status; } /****************************************************************************** * Delete an alarm from the display calendar. */ void deleteDisplayEvent(const QString& eventID) { - qCDebug(KALARM_LOG) << 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) << events.count(); + 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) << "No calendar"; + 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) << events.count(); + qCDebug(KALARM_LOG) << "enableEvents:" << events.count(); if (events.isEmpty()) return UpdateResult(UPDATE_OK); UpdateStatusData status; AlarmCalendar* cal = AlarmCalendar::resources(); bool deleteWakeFromSuspendAlarm = false; QString wakeFromSuspendId = checkRtcWakeConfig().value(0); for (int i = 0, end = events.count(); i < end; ++i) { KAEvent* event = &events[i]; if (event->category() == CalEvent::ACTIVE && enable != event->enabled()) { event->setEnabled(enable); if (!enable && event->id() == wakeFromSuspendId) deleteWakeFromSuspendAlarm = true; // Update the event in the calendar file KAEvent* newev = cal->updateEvent(event); if (!newev) - qCCritical(KALARM_LOG) << "Error updating event in calendar:" << event->id(); + 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) << purgeDays; + 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) << templateName << ": template not found"; + qCWarning(KALARM_LOG) << "editNewAlarm:" << templateName << ": template not found"; } return false; } /****************************************************************************** * Create a new template. */ void editNewTemplate(EditAlarmDlg::Type type, QWidget* parent) { ::editNewTemplate(type, nullptr, parent); } /****************************************************************************** * Create a new template, based on an existing event or template. */ void editNewTemplate(const KAEvent* preset, QWidget* parent) { ::editNewTemplate(EditAlarmDlg::Type(0), preset, parent); } /****************************************************************************** * Find the identity of the desktop we are running on. */ QString currentDesktopIdentityName() { return QProcessEnvironment::systemEnvironment().value(QStringLiteral("XDG_CURRENT_DESKTOP")); } /****************************************************************************** * Find the identity of the desktop we are running on. */ Desktop currentDesktopIdentity() { const QString desktop = currentDesktopIdentityName(); if (desktop == QStringLiteral("KDE")) return Desktop::Kde; if (desktop == QStringLiteral("Unity")) return Desktop::Unity; return Desktop::Other; } /****************************************************************************** * Check the config as to whether there is a wake-on-suspend alarm pending, and * if so, delete it from the config if it has expired. * If 'checkExists' is true, the config entry will only be returned if the * event exists. * Reply = config entry: [0] = event's collection ID (Akonadi only), * [1] = event ID, * [2] = trigger time (time_t). * = empty list if none or expired. */ QStringList checkRtcWakeConfig(bool checkEventExists) { KConfigGroup config(KSharedConfig::openConfig(), "General"); const QStringList params = config.readEntry("RtcWake", QStringList()); if (params.count() == 3 && params[2].toUInt() > KADateTime::currentUtcDateTime().toTime_t()) { if (checkEventExists && !AlarmCalendar::getEvent(EventId(params[0].toLongLong(), params[1]))) return QStringList(); return params; // config entry is valid } if (!params.isEmpty()) { config.deleteEntry("RtcWake"); // delete the expired config entry config.sync(); } return QStringList(); } /****************************************************************************** * Delete any wake-on-suspend alarm from the config. */ void deleteRtcWakeConfig() { KConfigGroup config(KSharedConfig::openConfig(), "General"); config.deleteEntry("RtcWake"); config.sync(); } /****************************************************************************** * Delete any wake-on-suspend alarm, optionally only for a specified event. */ void cancelRtcWake(QWidget* msgParent, const QString& eventId) { const QStringList wakeup = checkRtcWakeConfig(); if (!wakeup.isEmpty() && (eventId.isEmpty() || wakeup[0] == eventId)) { Private::instance()->mMsgParent = msgParent ? msgParent : MainWindow::mainMainWindow(); QTimer::singleShot(0, Private::instance(), &Private::cancelRtcWake); } } /****************************************************************************** * Delete any wake-on-suspend alarm. */ void Private::cancelRtcWake() { // setRtcWakeTime will only work with a parent window specified setRtcWakeTime(0, mMsgParent); deleteRtcWakeConfig(); KAMessageBox::information(mMsgParent, i18nc("info", "The scheduled Wake from Suspend has been cancelled.")); } /****************************************************************************** * Set the wakeup time for the system. * Set 'triggerTime' to zero to cancel the wakeup. * Reply = true if successful. */ bool setRtcWakeTime(unsigned triggerTime, QWidget* parent) { QVariantMap args; args[QStringLiteral("time")] = triggerTime; KAuth::Action action(QStringLiteral("org.kde.kalarm.rtcwake.settimer")); action.setHelperId(QStringLiteral("org.kde.kalarm.rtcwake")); action.setParentWidget(parent); action.setArguments(args); KAuth::ExecuteJob* job = action.execute(); if (!job->exec()) { QString errmsg = job->errorString(); - qCDebug(KALARM_LOG) << "Error code=" << job->error() << errmsg; + 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) << "Authorization error:" << errcode; + qCDebug(KALARM_LOG) << "setRtcWakeTime: Authorization error:" << errcode; return false; // the user should already know about this default: break; } errmsg = i18nc("@info", "Error obtaining authorization (%1)", errcode); } KAMessageBox::information(parent, errmsg); return false; } return true; } } // namespace KAlarm namespace { /****************************************************************************** * Create a new template. * 'preset' is non-null to base it on an existing event or template; otherwise, * the alarm type is set to 'type'. */ void editNewTemplate(EditAlarmDlg::Type type, const KAEvent* preset, QWidget* parent) { if (CollectionControlModel::enabledCollections(CalEvent::TEMPLATE, true).isEmpty()) { KAMessageBox::sorry(parent, i18nc("@info", "You must enable a template calendar to save the template in")); return; } // Use AutoQPointer to guard against crash on application exit while // the dialogue is still open. It prevents double deletion (both on // deletion of parent, and on return from this function). AutoQPointer editDlg; if (preset) editDlg = EditAlarmDlg::create(true, preset, true, parent); else editDlg = EditAlarmDlg::create(true, type, parent); if (editDlg->exec() == QDialog::Accepted) { KAEvent event; Akonadi::Collection calendar; editDlg->getEvent(event, calendar); // Add the template to the displayed lists and to the calendar file KAlarm::addTemplate(event, &calendar, editDlg); Undo::saveAdd(event, calendar); } } } // namespace namespace KAlarm { /****************************************************************************** * Open the Edit Alarm dialog to edit the specified alarm. * If the alarm is read-only or archived, the dialog is opened read-only. */ void editAlarm(KAEvent* event, QWidget* parent) { if (event->expired() || AlarmCalendar::resources()->eventReadOnly(event->itemId())) { viewAlarm(event, parent); return; } EventId id(*event); // Use AutoQPointer to guard against crash on application exit while // the dialogue is still open. It prevents double deletion (both on // deletion of parent, and on return from this function). AutoQPointer editDlg = EditAlarmDlg::create(false, event, false, parent, EditAlarmDlg::RES_USE_EVENT_ID); if (editDlg->exec() == QDialog::Accepted) { if (!AlarmCalendar::resources()->event(id)) { // Event has been deleted while the user was editing the alarm, // so treat it as a new alarm. PrivateNewAlarmDlg().accept(editDlg); return; } KAEvent newEvent; Collection calendar; bool changeDeferral = !editDlg->getEvent(newEvent, calendar); // Update the event in the displays and in the calendar file Undo::Event undo(*event, calendar); if (changeDeferral) { // The only change has been to an existing deferral if (updateEvent(newEvent, editDlg, true) != UPDATE_OK) // keep the same event ID return; // failed to save event } else { UpdateResult status = modifyEvent(*event, newEvent, editDlg); if (status.status != UPDATE_OK && status.status <= UPDATE_KORG_ERR) displayKOrgUpdateError(editDlg, ERR_MODIFY, status); } Undo::saveEdit(undo, newEvent); outputAlarmWarnings(editDlg, &newEvent); } } /****************************************************************************** * Display the alarm edit dialog to edit the alarm with the specified ID. * An error occurs if the alarm is not found, if there is more than one alarm * with the same ID, or if it is read-only or expired. */ bool editAlarmById(const EventId& id, QWidget* parent) { const QString eventID(id.eventId()); KAEvent* event = AlarmCalendar::resources()->event(id, true); if (!event) { if (id.collectionId() != -1) - qCWarning(KALARM_LOG) << "Event ID not found, or duplicated:" << eventID; + qCWarning(KALARM_LOG) << "editAlarmById: Event ID not found, or duplicated:" << eventID; else - qCWarning(KALARM_LOG) << "Event ID not found:" << eventID; + qCWarning(KALARM_LOG) << "editAlarmById: Event ID not found:" << eventID; return false; } if (AlarmCalendar::resources()->eventReadOnly(event->itemId())) { - qCCritical(KALARM_LOG) << eventID << ": read-only"; + qCCritical(KALARM_LOG) << "editAlarmById:" << eventID << ": read-only"; return false; } switch (event->category()) { case CalEvent::ACTIVE: case CalEvent::TEMPLATE: break; default: - qCCritical(KALARM_LOG) << eventID << ": event not active or template"; + qCCritical(KALARM_LOG) << "editAlarmById:" << eventID << ": event not active or template"; return false; } editAlarm(event, parent); return true; } /****************************************************************************** * Open the Edit Alarm dialog to edit the specified template. * If the template is read-only, the dialog is opened read-only. */ void editTemplate(KAEvent* event, QWidget* parent) { if (AlarmCalendar::resources()->eventReadOnly(event->itemId())) { // The template is read-only, so make the dialogue read-only. // Use AutoQPointer to guard against crash on application exit while // the dialogue is still open. It prevents double deletion (both on // deletion of parent, and on return from this function). AutoQPointer editDlg = EditAlarmDlg::create(true, event, false, parent, EditAlarmDlg::RES_PROMPT, true); editDlg->exec(); return; } // Use AutoQPointer to guard against crash on application exit while // the dialogue is still open. It prevents double deletion (both on // deletion of parent, and on return from this function). AutoQPointer editDlg = EditAlarmDlg::create(true, event, false, parent, EditAlarmDlg::RES_USE_EVENT_ID); if (editDlg->exec() == QDialog::Accepted) { KAEvent newEvent; Akonadi::Collection calendar; editDlg->getEvent(newEvent, calendar); QString id = event->id(); newEvent.setEventId(id); newEvent.setCollectionId(event->collectionId()); newEvent.setItemId(event->itemId()); // Update the event in the displays and in the calendar file Undo::Event undo(*event, calendar); updateTemplate(newEvent, editDlg); Undo::saveEdit(undo, newEvent); } } /****************************************************************************** * Open the Edit Alarm dialog to view the specified alarm (read-only). */ void viewAlarm(const KAEvent* event, QWidget* parent) { // Use AutoQPointer to guard against crash on application exit while // the dialogue is still open. It prevents double deletion (both on // deletion of parent, and on return from this function). AutoQPointer editDlg = EditAlarmDlg::create(false, event, false, parent, EditAlarmDlg::RES_PROMPT, true); editDlg->exec(); } /****************************************************************************** * Called when OK is clicked in the alarm edit dialog invoked by the Edit button * in an alarm message window. * Updates the alarm calendar and closes the dialog. */ void updateEditedAlarm(EditAlarmDlg* editDlg, KAEvent& event, Collection& calendar) { - qCDebug(KALARM_LOG); + qCDebug(KALARM_LOG) << "updateEditedAlarm"; KAEvent newEvent; Akonadi::Collection cal; editDlg->getEvent(newEvent, cal); // Update the displayed lists and the calendar file UpdateResult status; if (AlarmCalendar::resources()->event(EventId(event))) { // The old alarm hasn't expired yet, so replace it Undo::Event undo(event, calendar); status = modifyEvent(event, newEvent, editDlg); Undo::saveEdit(undo, newEvent); } else { // The old event has expired, so simply create a new one status = addEvent(newEvent, &calendar, editDlg); Undo::saveAdd(newEvent, calendar); } if (status.status != UPDATE_OK && status.status <= UPDATE_KORG_ERR) displayKOrgUpdateError(editDlg, ERR_MODIFY, status); outputAlarmWarnings(editDlg, &newEvent); editDlg->close(); } /****************************************************************************** * Returns a list of all alarm templates. * If shell commands are disabled, command alarm templates are omitted. */ KAEvent::List templateList() { KAEvent::List templates; bool includeCmdAlarms = ShellProcess::authorised(); KAEvent::List events = AlarmCalendar::resources()->events(CalEvent::TEMPLATE); for (int i = 0, end = events.count(); i < end; ++i) { KAEvent* event = events[i]; if (includeCmdAlarms || !(event->actionTypes() & KAEvent::ACT_COMMAND)) templates.append(event); } return templates; } /****************************************************************************** * To be called after an alarm has been edited. * Prompt the user to re-enable alarms if they are currently disabled, and if * it's an email alarm, warn if no 'From' email address is configured. */ void outputAlarmWarnings(QWidget* parent, const KAEvent* event) { if (event && event->actionTypes() == KAEvent::ACT_EMAIL && Preferences::emailAddress().isEmpty()) KAMessageBox::information(parent, xi18nc("@info Please set the 'From' email address...", "%1Please set it in the Configuration dialog.", KAMail::i18n_NeedFromEmailAddress())); if (!theApp()->alarmsEnabled()) { if (KAMessageBox::warningYesNo(parent, xi18nc("@info", "Alarms are currently disabled.Do you want to enable alarms now?"), QString(), KGuiItem(i18nc("@action:button", "Enable")), KGuiItem(i18nc("@action:button", "Keep Disabled")), QStringLiteral("EditEnableAlarms")) == KMessageBox::Yes) theApp()->setAlarmsEnabled(true); } } /****************************************************************************** * Reload the calendar. */ void refreshAlarms() { - qCDebug(KALARM_LOG); + 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); + qCDebug(KALARM_LOG) << "refreshAlarmsIfQueued"; AlarmCalendar::resources()->reload(); // Close any message windows for alarms which are now disabled KAEvent::List events = AlarmCalendar::resources()->events(CalEvent::ACTIVE); for (int i = 0, end = events.count(); i < end; ++i) { KAEvent* event = events[i]; if (!event->enabled() && (event->actionTypes() & KAEvent::ACT_DISPLAY)) { MessageWin* win = MessageWin::findEvent(EventId(*event)); delete win; } } MainWindow::refresh(); refreshAlarmsQueued = false; } } /****************************************************************************** * Start KMail if it isn't already running, optionally minimised. * Reply = reason for failure to run KMail (which may be the empty string) * = null string if success. */ QString runKMail(bool minimise) { QDBusReply reply = QDBusConnection::sessionBus().interface()->isServiceRegistered(KMAIL_DBUS_SERVICE); if (!reply.isValid() || !reply.value()) { // Program is not already running, so start it QString errmsg; if (minimise && Private::startKMailMinimised()) return QString(); if (KToolInvocation::startServiceByDesktopName(QStringLiteral("org.kde.kmail2"), QString(), &errmsg)) { qCCritical(KALARM_LOG) << "Couldn't start KMail (" << errmsg << ")"; return xi18nc("@info", "Unable to start KMail(%1)", errmsg); } } return QString(); } /****************************************************************************** * Start KMail, minimised. * This code is taken from kstart in kdebase. */ bool Private::startKMailMinimised() { #if KDEPIM_HAVE_X11 NETRootInfo i(QX11Info::connection(), NET::Supported, NET::Properties2()); if (i.isSupported(NET::WM2KDETemporaryRules)) { - qCDebug(KALARM_LOG) << "using rules"; + qCDebug(KALARM_LOG) << "startKMailMinimised: using rules"; KXMessages msg; QString message = QLatin1String("wmclass=kmail\nwmclassmatch=1\n" // 1 = exact match "wmclasscomplete=false\n" "minimize=true\nminimizerule=3\n" "type=") + QString().setNum(NET::Normal) + QLatin1String("\ntyperule=2"); msg.broadcastMessage("_KDE_NET_WM_TEMPORARY_RULES", message, -1); qApp->flush(); } else { // Connect to window add to get the NEW windows - qCDebug(KALARM_LOG) << "connecting to window add"; + qCDebug(KALARM_LOG) << "startKMailMinimised: connecting to window add"; connect(KWindowSystem::self(), &KWindowSystem::windowAdded, instance(), &Private::windowAdded); } // Propagate the app startup notification info to the started app. // We are not using KApplication, so the env remained set. KStartupInfoId id = KStartupInfo::currentStartupIdEnv(); KProcess* proc = new KProcess; (*proc) << QStringLiteral("kmail"); int pid = proc->startDetached(); if (!pid) { KStartupInfo::sendFinish(id); // failed to start return false; } KStartupInfoData data; data.addPid(pid); data.setName(QLatin1String("kmail")); data.setBin(QLatin1String("kmail")); KStartupInfo::sendChange(id, data); return true; #else return false; #endif } /****************************************************************************** * Called when a window is created, to minimise it. * This code is taken from kstart in kdebase. */ void Private::windowAdded(WId w) { #if KDEPIM_HAVE_X11 static const NET::WindowTypes SUPPORTED_TYPES = NET::NormalMask | NET::DesktopMask | NET::DockMask | NET::ToolbarMask | NET::MenuMask | NET::DialogMask | NET::OverrideMask | NET::TopMenuMask | NET::UtilityMask | NET::SplashMask; KWindowInfo kwinfo(w, NET::WMWindowType | NET::WMName); if (kwinfo.windowType(SUPPORTED_TYPES) == NET::TopMenu || kwinfo.windowType(SUPPORTED_TYPES) == NET::Toolbar || kwinfo.windowType(SUPPORTED_TYPES) == NET::Desktop) return; // always ignore these window types Display* display = QX11Info::display(); XWithdrawWindow(display, w, QX11Info::appScreen()); QApplication::flush(); NETWinInfo info(QX11Info::connection(), w, QX11Info::appRootWindow(), NET::WMState, NET::Properties2()); XWMHints* hints = XGetWMHints(display, w); if (hints) { hints->flags |= StateHint; hints->initial_state = IconicState; XSetWMHints(display, w, hints); XFree(hints); } info.setWindowType(NET::Normal); XSync(display, False); XMapWindow(display, w); XSync(display, False); QApplication::flush(); #endif } /****************************************************************************** * The "Don't show again" option for error messages is personal to the user on a * particular computer. For example, he may want to inhibit error messages only * on his laptop. So the status is not stored in the alarm calendar, but in the * user's local KAlarm data directory. ******************************************************************************/ /****************************************************************************** * Return the Don't-show-again error message tags set for a specified alarm ID. */ QStringList dontShowErrors(const EventId& eventId) { if (eventId.isEmpty()) return QStringList(); KConfig config(QStandardPaths::writableLocation(QStandardPaths::DataLocation) + QLatin1Char('/') + ALARM_OPTS_FILE); KConfigGroup group(&config, DONT_SHOW_ERRORS_GROUP); const QString id = QStringLiteral("%1:%2").arg(eventId.collectionId()).arg(eventId.eventId()); return group.readEntry(id, QStringList()); } /****************************************************************************** * Check whether the specified Don't-show-again error message tag is set for an * alarm ID. */ bool dontShowErrors(const EventId& eventId, const QString& tag) { if (tag.isEmpty()) return false; QStringList tags = dontShowErrors(eventId); return tags.indexOf(tag) >= 0; } /****************************************************************************** * Reset the Don't-show-again error message tags for an alarm ID. * If 'tags' is empty, the config entry is deleted. */ void setDontShowErrors(const EventId& eventId, const QStringList& tags) { if (eventId.isEmpty()) return; KConfig config(QStandardPaths::writableLocation(QStandardPaths::DataLocation) + QLatin1Char('/') + ALARM_OPTS_FILE); KConfigGroup group(&config, DONT_SHOW_ERRORS_GROUP); const QString id = QStringLiteral("%1:%2").arg(eventId.collectionId()).arg(eventId.eventId()); if (tags.isEmpty()) group.deleteEntry(id); else group.writeEntry(id, tags); group.sync(); } /****************************************************************************** * Set the specified Don't-show-again error message tag for an alarm ID. * Existing tags are unaffected. */ void setDontShowErrors(const EventId& eventId, const QString& tag) { if (eventId.isEmpty() || tag.isEmpty()) return; KConfig config(QStandardPaths::writableLocation(QStandardPaths::DataLocation) + QLatin1Char('/') + ALARM_OPTS_FILE); KConfigGroup group(&config, DONT_SHOW_ERRORS_GROUP); const QString id = QStringLiteral("%1:%2").arg(eventId.collectionId()).arg(eventId.eventId()); QStringList tags = group.readEntry(id, QStringList()); if (tags.indexOf(tag) < 0) { tags += tag; group.writeEntry(id, tags); group.sync(); } } /****************************************************************************** * Read the size for the specified window from the config file, for the * current screen resolution. * Reply = true if size set in the config file, in which case 'result' is set * = false if no size is set, in which case 'result' is unchanged. */ bool readConfigWindowSize(const char* window, QSize& result, int* splitterWidth) { KConfigGroup config(KSharedConfig::openConfig(), window); QWidget* desktop = qApp->desktop(); QSize s = QSize(config.readEntry(QStringLiteral("Width %1").arg(desktop->width()), (int)0), config.readEntry(QStringLiteral("Height %1").arg(desktop->height()), (int)0)); if (s.isEmpty()) return false; result = s; if (splitterWidth) *splitterWidth = config.readEntry(QStringLiteral("Splitter %1").arg(desktop->width()), -1); return true; } /****************************************************************************** * Write the size for the specified window to the config file, for the * current screen resolution. */ void writeConfigWindowSize(const char* window, const QSize& size, int splitterWidth) { KConfigGroup config(KSharedConfig::openConfig(), window); QWidget* desktop = qApp->desktop(); config.writeEntry(QStringLiteral("Width %1").arg(desktop->width()), size.width()); config.writeEntry(QStringLiteral("Height %1").arg(desktop->height()), size.height()); if (splitterWidth >= 0) config.writeEntry(QStringLiteral("Splitter %1").arg(desktop->width()), splitterWidth); config.sync(); } /****************************************************************************** * Check from its mime type whether a file appears to be a text or image file. * If a text file, its type is distinguished. * Reply = file type. */ FileType fileType(const QMimeType& mimetype) { if (mimetype.inherits(QStringLiteral("text/html"))) return TextFormatted; if (mimetype.inherits(QStringLiteral("application/x-executable"))) return TextApplication; if (mimetype.inherits(QStringLiteral("text/plain"))) return TextPlain; if (mimetype.name().startsWith(QLatin1String("image/"))) return Image; return Unknown; } /****************************************************************************** * Check that a file exists and is a plain readable file. * Updates 'filename' and 'url' even if an error occurs, since 'filename' may * be needed subsequently by showFileErrMessage(). * 'filename' is in user input format and may be a local file path or URL. */ FileErr checkFileExists(QString& filename, QUrl& url) { // Convert any relative file path to absolute // (using home directory as the default). // This also supports absolute paths and absolute urls. FileErr err = FileErr_None; url = QUrl::fromUserInput(filename, QDir::homePath(), QUrl::AssumeLocalFile); if (filename.isEmpty()) { url = QUrl(); err = FileErr_Blank; // blank file name } else if (!url.isValid()) err = FileErr_Nonexistent; else if (url.isLocalFile()) { // It's a local file filename = url.toLocalFile(); QFileInfo info(filename); if (info.isDir()) err = FileErr_Directory; else if (!info.exists()) err = FileErr_Nonexistent; else if (!info.isReadable()) err = FileErr_Unreadable; } else { filename = url.toDisplayString(); auto statJob = KIO::stat(url, KIO::StatJob::SourceSide, 2); KJobWidgets::setWindow(statJob, MainWindow::mainMainWindow()); if (!statJob->exec()) err = FileErr_Nonexistent; else { KFileItem fi(statJob->statResult(), url); if (fi.isDir()) err = FileErr_Directory; else if (!fi.isReadable()) err = FileErr_Unreadable; } } return err; } /****************************************************************************** * Display an error message appropriate to 'err'. * Display a Continue/Cancel error message if 'errmsgParent' non-null. * Reply = true to continue, false to cancel. */ bool showFileErrMessage(const QString& filename, FileErr err, FileErr blankError, QWidget* errmsgParent) { if (err != FileErr_None) { // If file is a local file, remove "file://" from name QString file = filename; QRegExp f(QStringLiteral("^file:/+")); if (f.indexIn(file) >= 0) file = file.mid(f.matchedLength() - 1); QString errmsg; switch (err) { case FileErr_Blank: if (blankError == FileErr_BlankDisplay) errmsg = i18nc("@info", "Please select a file to display"); else if (blankError == FileErr_BlankPlay) errmsg = i18nc("@info", "Please select a file to play"); else qFatal("Program error"); KAMessageBox::sorry(errmsgParent, errmsg); return false; case FileErr_Directory: KAMessageBox::sorry(errmsgParent, xi18nc("@info", "%1 is a folder", file)); return false; case FileErr_Nonexistent: errmsg = xi18nc("@info", "%1 not found", file); break; case FileErr_Unreadable: errmsg = xi18nc("@info", "%1 is not readable", file); break; case FileErr_NotTextImage: errmsg = xi18nc("@info", "%1 appears not to be a text or image file", file); break; default: break; } if (KAMessageBox::warningContinueCancel(errmsgParent, errmsg) == KMessageBox::Cancel) return false; } return true; } /****************************************************************************** * If a url string is a local file, strip off the 'file:/' prefix. */ QString pathOrUrl(const QString& url) { static const QRegExp localfile(QStringLiteral("^file:/+")); return (localfile.indexIn(url) >= 0) ? url.mid(localfile.matchedLength() - 1) : url; } /****************************************************************************** * Display a modal dialog to choose an existing file, initially highlighting * any specified file. * @param initialFile The file to initially highlight - must be a full path name or URL. * @param defaultDir The directory to start in if @p initialFile is empty. If empty, * the user's home directory will be used. Updated to the * directory containing the selected file, if a file is chosen. * @param existing true to return only existing files, false to allow new ones. * Reply = URL selected. * = empty, non-null string if no file was selected. * = null string if dialogue was deleted while visible (indicating that * the parent widget was probably also deleted). */ QString browseFile(const QString& caption, QString& defaultDir, const QString& initialFile, const QString& filter, bool existing, QWidget* parent) { QString initialDir = !initialFile.isEmpty() ? QString(initialFile).remove(QRegExp(QLatin1String("/[^/]*$"))) : !defaultDir.isEmpty() ? defaultDir : QDir::homePath(); // Use AutoQPointer to guard against crash on application exit while // the dialogue is still open. It prevents double deletion (both on // deletion of parent, and on return from this function). AutoQPointer fileDlg = new QFileDialog(parent, caption, initialDir, filter); fileDlg->setAcceptMode(existing ? QFileDialog::AcceptOpen : QFileDialog::AcceptSave); fileDlg->setFileMode(existing ? QFileDialog::ExistingFile : QFileDialog::AnyFile); if (!initialFile.isEmpty()) fileDlg->selectFile(initialFile); if (fileDlg->exec() != QDialog::Accepted) return fileDlg ? QStringLiteral("") : QString(); // return null only if dialog was deleted const QList urls = fileDlg->selectedUrls(); if (urls.isEmpty()) return QStringLiteral(""); // return empty, non-null string const QUrl& url = urls[0]; defaultDir = url.isLocalFile() ? KIO::upUrl(url).toLocalFile() : url.adjusted(QUrl::RemoveFilename).path(); bool localOnly = true; return localOnly ? url.toDisplayString(QUrl::PreferLocalFile) : url.toDisplayString(); } /****************************************************************************** * Return a prompt string to ask the user whether to convert the calendar to the * current format. * If 'whole' is true, the whole calendar needs to be converted; else only some * alarms may need to be converted. * * Note: This method is defined here to avoid duplicating the i18n string * definition between the Akonadi and KResources code. */ QString conversionPrompt(const QString& calendarName, const QString& calendarVersion, bool whole) { QString msg = whole ? xi18nc("@info", "Calendar %1 is in an old format (KAlarm version %2), " "and will be read-only unless you choose to update it to the current format.", calendarName, calendarVersion) : xi18nc("@info", "Some or all of the alarms in calendar %1 are in an old KAlarm format, " "and will be read-only unless you choose to update them to the current format.", calendarName); return xi18nc("@info", "%1" "Do not update the calendar if it is also used with an older version of KAlarm " "(e.g. on another computer). If you do so, the calendar may become unusable there." "Do you wish to update the calendar?", msg); } #ifndef NDEBUG /****************************************************************************** * Set up KAlarm test conditions based on environment variables. * KALARM_TIME: specifies current system time (format [[[yyyy-]mm-]dd-]hh:mm [TZ]). */ void setTestModeConditions() { const QByteArray newTime = qgetenv("KALARM_TIME"); if (!newTime.isEmpty()) { KADateTime dt; if (AlarmTime::convertTimeString(newTime, dt, KADateTime::realCurrentLocalDateTime(), true)) setSimulatedSystemTime(dt); } } /****************************************************************************** * Set the simulated system time. */ void setSimulatedSystemTime(const KADateTime& dt) { KADateTime::setSimulatedSystemTime(dt); qCDebug(KALARM_LOG) << "New time =" << qPrintable(KADateTime::currentLocalDateTime().toString(QStringLiteral("%Y-%m-%d %H:%M %:Z"))); } #endif } // namespace KAlarm namespace { /****************************************************************************** * Display an error message about an error when saving an event. */ void displayUpdateError(QWidget* parent, KAlarm::UpdateError code, const UpdateStatusData& status, bool showKOrgError) { QString errmsg; if (status.status.status > KAlarm::UPDATE_KORG_ERR) { switch (code) { case KAlarm::ERR_ADD: case KAlarm::ERR_MODIFY: errmsg = (status.warnErr > 1) ? i18nc("@info", "Error saving alarms") : i18nc("@info", "Error saving alarm"); break; case KAlarm::ERR_DELETE: errmsg = (status.warnErr > 1) ? i18nc("@info", "Error deleting alarms") : i18nc("@info", "Error deleting alarm"); break; case KAlarm::ERR_REACTIVATE: errmsg = (status.warnErr > 1) ? i18nc("@info", "Error saving reactivated alarms") : i18nc("@info", "Error saving reactivated alarm"); break; case KAlarm::ERR_TEMPLATE: errmsg = (status.warnErr > 1) ? i18nc("@info", "Error saving alarm templates") : i18nc("@info", "Error saving alarm template"); break; } KAMessageBox::error(parent, errmsg); } else if (showKOrgError) displayKOrgUpdateError(parent, code, status.status, status.warnKOrg); } /****************************************************************************** * Tell KOrganizer to put an alarm in its calendar. * It will be held by KOrganizer as a simple event, without alarms - KAlarm * is still responsible for alarming. */ KAlarm::UpdateResult sendToKOrganizer(const KAEvent& event) { Event::Ptr kcalEvent(new KCalCore::Event); event.updateKCalEvent(kcalEvent, KAEvent::UID_IGNORE); // Change the event ID to avoid duplicating the same unique ID as the original event QString uid = uidKOrganizer(event.id()); kcalEvent->setUid(uid); kcalEvent->clearAlarms(); QString userEmail; switch (event.actionTypes()) { case KAEvent::ACT_DISPLAY: case KAEvent::ACT_COMMAND: case KAEvent::ACT_DISPLAY_COMMAND: kcalEvent->setSummary(event.cleanText()); userEmail = Preferences::emailAddress(); break; case KAEvent::ACT_EMAIL: { QString from = event.emailFromId() ? Identities::identityManager()->identityForUoid(event.emailFromId()).fullEmailAddr() : Preferences::emailAddress(); AlarmText atext; atext.setEmail(event.emailAddresses(QStringLiteral(", ")), from, QString(), QString(), event.emailSubject(), QString()); kcalEvent->setSummary(atext.displayText()); userEmail = from; break; } case KAEvent::ACT_AUDIO: kcalEvent->setSummary(event.audioFile()); break; default: break; } Person::Ptr person(new Person(QString(), userEmail)); kcalEvent->setOrganizer(person); kcalEvent->setDuration(Duration(Preferences::kOrgEventDuration() * 60, Duration::Seconds)); // Translate the event into string format ICalFormat format; format.setTimeZone(Preferences::timeSpecAsZone()); QString iCal = format.toICalString(kcalEvent); // Send the event to KOrganizer KAlarm::UpdateResult status = runKOrganizer(); // start KOrganizer if it isn't already running, and create its D-Bus interface if (status != KAlarm::UPDATE_OK) return status; QList args; args << iCal; QDBusReply reply = korgInterface->callWithArgumentList(QDBus::Block, QStringLiteral("addIncidence"), args); if (!reply.isValid()) { if (reply.error().type() == QDBusError::UnknownObject) { status = KAlarm::UPDATE_KORG_ERRSTART; - qCCritical(KALARM_LOG) << "addIncidence() D-Bus error: still starting"; + qCCritical(KALARM_LOG) << "sendToKOrganizer: addIncidence() D-Bus error: still starting"; } else { status.set(KAlarm::UPDATE_KORG_ERR, reply.error().message()); - qCCritical(KALARM_LOG) << "addIncidence(" << uid << ") D-Bus call failed:" << status.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) << "addIncidence(" << uid << ") D-Bus call returned false"; + qCDebug(KALARM_LOG) << "sendToKOrganizer: addIncidence(" << uid << ") D-Bus call returned false"; } else - qCDebug(KALARM_LOG) << uid << ": success"; + qCDebug(KALARM_LOG) << "sendToKOrganizer:" << uid << ": success"; return status; } /****************************************************************************** * Tell KOrganizer to delete an event from its calendar. */ KAlarm::UpdateResult deleteFromKOrganizer(const QString& eventID) { const QString newID = uidKOrganizer(eventID); new CollectionSearch(KORG_MIME_TYPE, newID, true); // this auto-deletes when complete // Ignore errors return KAlarm::UpdateResult(KAlarm::UPDATE_OK); } /****************************************************************************** * Start KOrganizer if not already running, and create its D-Bus interface. */ KAlarm::UpdateResult runKOrganizer() { KAlarm::UpdateResult status; QString error, dbusService; int result = KDBusServiceStarter::self()->findServiceFor(QStringLiteral("DBUS/Organizer"), QString(), &error, &dbusService); if (result) { status.set(KAlarm::UPDATE_KORG_ERRINIT, error); - qCWarning(KALARM_LOG) << "Unable to start DBUS/Organizer:" << status.message; + qCWarning(KALARM_LOG) << "runKOrganizer: Unable to start DBUS/Organizer:" << status.message; return status; } // If Kontact is running, there is a load() method which needs to be called to // load KOrganizer into Kontact. But if KOrganizer is running independently, // the load() method doesn't exist. QDBusInterface iface(KORG_DBUS_SERVICE, QStringLiteral(KORG_DBUS_LOAD_PATH), QStringLiteral("org.kde.PIMUniqueApplication")); if (!iface.isValid()) { status.set(KAlarm::UPDATE_KORG_ERR, iface.lastError().message()); - qCWarning(KALARM_LOG) << "Unable to access " KORG_DBUS_LOAD_PATH " D-Bus interface:" << status.message; + qCWarning(KALARM_LOG) << "runKOrganizer: Unable to access " KORG_DBUS_LOAD_PATH " D-Bus interface:" << status.message; return status; } QDBusReply reply = iface.call(QStringLiteral("load")); if ((!reply.isValid() || !reply.value()) && iface.lastError().type() != QDBusError::UnknownMethod) { status.set(KAlarm::UPDATE_KORG_ERR, iface.lastError().message()); qCWarning(KALARM_LOG) << "Loading KOrganizer failed:" << status.message; return status; } // KOrganizer has been started, but it may not have the necessary // D-Bus interface available yet. if (!korgInterface || !korgInterface->isValid()) { delete korgInterface; korgInterface = new QDBusInterface(KORG_DBUS_SERVICE, QStringLiteral(KORG_DBUS_PATH), KORG_DBUS_IFACE); if (!korgInterface->isValid()) { status.set(KAlarm::UPDATE_KORG_ERRSTART, korgInterface->lastError().message()); - qCWarning(KALARM_LOG) << "Unable to access " KORG_DBUS_PATH " D-Bus interface:" << status.message; + qCWarning(KALARM_LOG) << "runKOrganizer: Unable to access " KORG_DBUS_PATH " D-Bus interface:" << status.message; delete korgInterface; korgInterface = nullptr; } } return status; } /****************************************************************************** * Insert a KOrganizer string after the hyphen in the supplied event ID. */ QString uidKOrganizer(const QString& id) { QString result = id; int i = result.lastIndexOf(QLatin1Char('-')); if (i < 0) i = result.length(); return result.insert(i, KORGANIZER_UID); } } // namespace /****************************************************************************** * Case insensitive comparison for use by qSort(). */ bool caseInsensitiveLessThan(const QString& s1, const QString& s2) { return s1.toLower() < s2.toLower(); } // vim: et sw=4: diff --git a/src/kalarmapp.cpp b/src/kalarmapp.cpp index c241990f..bf0766dd 100644 --- a/src/kalarmapp.cpp +++ b/src/kalarmapp.cpp @@ -1,2493 +1,2493 @@ /* * kalarmapp.cpp - the KAlarm application object * Program: kalarm * Copyright © 2001-2018 by 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 #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) { - qCDebug(KALARM_LOG); + 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) << "KOrganizer options disabled (KOrganizer not found)"; } + 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) << "Window keyboard focus broken"; } + if (mWindowFocusBroken) { qCDebug(KALARM_LOG) << "KAlarmApp: Window keyboard focus broken"; } } /****************************************************************************** */ KAlarmApp::~KAlarmApp() { while (!mCommandProcesses.isEmpty()) { ProcData* pd = mCommandProcesses[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) << "initialising calendars"; + 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) << "Restoring"; + 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) << "no main window to be restored!?"; + 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); + 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; } 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 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) << exitCode << ": quitting"; + 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); + 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' 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) << "now:" << qPrintable(now.toString(QStringLiteral("%Y-%m-%d %H:%M %:Z"))) << ", next:" << qPrintable(nextDt.toString(QStringLiteral("%Y-%m-%d %H:%M %:Z"))) << ", due:" << interval; + 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) << nextEvent->id() << ": due now"; + 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. #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) << nextEvent->id() << "wait" << interval/1000 << "seconds"; + 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) { if (mActionQueue[i].function == EVENT_HANDLE && mActionQueue[i].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); + 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); + 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) { KAEvent event = *events[i]; if (!cancelReminderAndDeferral(event)) { if (mAlarmsEnabled) 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); + 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) << "changed ->" << 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); + qCDebug(KALARM_LOG) << "KAlarmApp::checkWritableCalendar"; // Find whether there are any writable active alarm calendars bool active = !CollectionControlModel::enabledCollections(CalEvent::ACTIVE, true).isEmpty(); if (!active) { - qCWarning(KALARM_LOG) << "No writable active calendar"; + 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) << collection.id() << ": standard archived..."; + 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 KAEvent* event = &events[i]; 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(' ') + dateTime.toString(QStringLiteral("%Y%m%dT%H%M ")) + AlarmText::summary(*event, 1); alarms << text; } return alarms; } /****************************************************************************** * Enable or disable alarm monitoring. */ void KAlarmApp::setAlarmsEnabled(bool enabled) { if (enabled != mAlarmsEnabled) { mAlarmsEnabled = enabled; Q_EMIT alarmEnabledToggled(enabled); if (!enabled) KAlarm::cancelRtcWake(nullptr); else if (!mProcessingQueue) checkNextDueAlarm(); } } /****************************************************************************** * Spread or collect alarm message and error message windows. */ void KAlarmApp::spreadWindows(bool spread) { spread = MessageWin::spread(spread); Q_EMIT spreadWindowsToggled(spread); } /****************************************************************************** * Called when the spread status of message windows changes. * Set the 'spread windows' action state. */ void KAlarmApp::setSpreadWindowsState(bool spread) { Q_EMIT spreadWindowsToggled(spread); } /****************************************************************************** * Check whether the window manager's handling of keyboard focus transfer * between application windows is broken. This is true for Ubuntu's Unity * desktop, where MessageWin windows steal keyboard focus from EditAlarmDlg * windows. */ bool KAlarmApp::windowFocusBroken() const { return mWindowFocusBroken; } /****************************************************************************** * Check whether window/keyboard focus currently needs to be fixed manually due * to the window manager not handling it correctly. This will occur if there are * both EditAlarmDlg and MessageWin windows currently active. */ bool KAlarmApp::needWindowFocusFix() const { return mWindowFocusBroken && MessageWin::instanceCount(true) && EditAlarmDlg::instanceCount(); } /****************************************************************************** * Called to schedule a new alarm, either in response to a DCOP notification or * to command line options. * Reply = true unless there was a parameter error or an error opening calendar file. */ bool KAlarmApp::scheduleEvent(KAEvent::SubAction action, const QString& text, const KADateTime& dateTime, int lateCancel, KAEvent::Flags flags, const QColor& bg, const QColor& fg, const QFont& font, const QString& audioFile, float audioVolume, int reminderMinutes, const KARecurrence& recurrence, const KCalCore::Duration& repeatInterval, int repeatCount, uint mailFromID, const KCalCore::Person::List& mailAddresses, const QString& mailSubject, const QStringList& mailAttachments) { - qCDebug(KALARM_LOG) << text; + 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) << eventID; + 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); + 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) << "Event ID not found, or duplicated:" << eventID; + qCWarning(KALARM_LOG) << "KAlarmApp::handleEvent: Event ID not found, or duplicated:" << eventID; else - qCWarning(KALARM_LOG) << "Event ID not found:" << eventID; + qCWarning(KALARM_LOG) << "KAlarmApp::handleEvent: Event ID not found:" << eventID; return false; } switch (function) { case EVENT_CANCEL: - qCDebug(KALARM_LOG) << eventID << ", 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) << eventID << "," << (function==EVENT_TRIGGER?"TRIGGER:":"HANDLE:") << qPrintable(now.qDateTime().toString(QStringLiteral("yyyy-MM-dd hh:mm"))) << "UTC"; + 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) << "Alarm" << alarm.type() << "at" << nextDT.qDateTime() << ": not due"; + 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) << "Alarm" << alarm.type() << "at" << nextDT.qDateTime() << ": not during working hours"; + 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) << "REPEAT_AT_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) << "LATE_CANCEL"; + 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); 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) << "Alarm" << alarm.type() << ": execute"; + 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) << "Alarm" << alarm.type() << ": skip"; + 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(); 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) << "No action"; } + 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(); - qCDebug(KALARM_LOG) << event.id() << ":" << command; + 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) << "Alarm type:" << alarm.type(); + 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); switch (type) { case KAEvent::NO_OCCURRENCE: // All repetitions are finished, so cancel the event - qCDebug(KALARM_LOG) << "No occurrence"; + 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 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); + 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) << event.id() << ": 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 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) { ProcData* pd = mCommandProcesses[i]; if (pd->event->id() == event.id() && (pd->flags & ProcData::PRE_ACTION)) { - qCDebug(KALARM_LOG) << "Already executing pre-DISPLAY command"; + 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(); - qCDebug(KALARM_LOG) << "Pre-DISPLAY command:" << command; + qCDebug(KALARM_LOG) << "KAlarmApp::execAlarm: Pre-DISPLAY command:" << command; 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) << event.id() << ": pre-action failed: cancelled"; + 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); (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) << "EMAIL to:" << event.emailAddresses(QStringLiteral(",")); + 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; 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) << (copyerr ? "Copy error:" : "Failed:") << errmsgs[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(); if (event.commandScript()) { // Store the command script in a temporary file for execution - qCDebug(KALARM_LOG) << "Script"; + qCDebug(KALARM_LOG) << "KAlarmApp::execCommandAlarm: Script"; 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) << command; + 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) << command << "," << event.id(); + 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) << "Command failed (no terminal selected)"; + 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)"); 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(); 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) << "Command failed to start"; + 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) << command << "," << event.id(); + 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")); 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); 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")); 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); + 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]; if (pd->process == proc) { // Found the command. Check its exit status. bool executeAlarm = pd->preAction(); ShellProcess::Status status = proc->status(); if (status == ShellProcess::SUCCESS && !proc->exitCode()) { - qCDebug(KALARM_LOG) << pd->event->id() << ": SUCCESS"; + 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) << pd->event->id() << ":" << errmsg << "exit status =" << status << ", code =" << proc->exitCode(); + qCWarning(KALARM_LOG) << "KAlarmApp::slotCommandExited:" << pd->event->id() << ":" << errmsg << "exit status =" << status << ", code =" << proc->exitCode(); else - qCWarning(KALARM_LOG) << pd->event->id() << ":" << errmsg << "exit status =" << status; + 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(); 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) << pd->event->id() << ": pre-action failed: cancelled"; + 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) { 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) << "first time"; + 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) { 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 2f6b02a6..053c220f 100644 --- a/src/kamail.cpp +++ b/src/kamail.cpp @@ -1,980 +1,980 @@ /* * kamail.cpp - email functions * Program: kalarm * Copyright © 2002-2018 by 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 #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) << "Identity" << jobdata.event.emailFromId() << "not found"; + 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) << "Identity" << identity.identityName() << "uoid" << identity.uoid() << ": no email address"; + 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) << "To:" << jobdata.event.emailAddresses(QStringLiteral(",")) + qCDebug(KALARM_LOG) << "KAMail::send: To:" << jobdata.event.emailAddresses(QStringLiteral(",")) << 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) << "Sending via sendmail"; + qCDebug(KALARM_LOG) << "KAMail::send: Sending via sendmail"; QStringList paths; 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) << "sendmail not found"; + 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) << "Error compiling message:" << err; + 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) << "Unable to open a pipe to " << command; + 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) << "Sending via KDE"; + 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) << "No mail transport found for identity" << identity.identityName() << "uoid" << identity.uoid(); + 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) << "Using transport" << transport->name() << ", id=" << transport->id(); + 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) << "Error compiling message:" << err; + 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) << "Failed:" << job->errorString(); + 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) << "Wrong job at head of queue: wiping queue"; + 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; KCalCore::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()); 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; 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(); 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.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->assemble(); message.addContent(content); } // Append each attachment in turn for (QStringList::Iterator at = attachments.begin(); at != attachments.end(); ++at) { QString attachment = QString::fromLatin1((*at).toLocal8Bit()); QUrl url = QUrl::fromUserInput(attachment, QString(), QUrl::AssumeLocalFile); 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) << "Not found:" << attachment; + 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) << "Not file/not readable:" << attachment; + 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) << "Load failure:" << attachment; + qCCritical(KALARM_LOG) << "KAMail::appendBodyAttachments: Load failure:" << attachment; return attachError; } contents = downloadJob->data(); if (static_cast(contents.size()) < fi.size()) { - qCDebug(KALARM_LOG) << "Read error:" << attachment; + qCDebug(KALARM_LOG) << "KAMail::appendBodyAttachments: Read error:" << attachment; atterror = true; } } else { QFile f(url.toLocalFile()); if (!f.open(QIODevice::ReadOnly)) { - qCCritical(KALARM_LOG) << "Load failure:" << attachment; + 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(); KCalCore::Person::List addresses = event.emailAddressees(); for (int i = 0, end = addresses.count(); i < end; ++i) { QByteArray email = addresses[i]->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; 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, KCalCore::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) { KCalCore::Person::Ptr person(new KCalCore::Person(mailboxes[i].name(), mailboxes[i].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(); if (!n) return 0; int start = 0; int end = n - 1; if (address[end] == QLatin1Char('>')) { // The email address is in <...> if ((start = address.indexOf(QLatin1Char('<'))) < 0) return -1; ++start; --end; } 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(); 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; } /****************************************************************************** * 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; QDBusInterface iface(KMAIL_DBUS_SERVICE, QString(), QStringLiteral("KMailIface")); QDBusReply reply = iface.callWithArgumentList(QDBus::Block, QStringLiteral("getDecodedBodyPart"), args); if (!reply.isValid()) { - qCCritical(KALARM_LOG) << "D-Bus call failed:" << reply.error().message(); + 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; Q_FOREACH(const QString& email, splitEmails) { normalizedEmail << KEmailAddress::extractEmailAddress(KEmailAddress::normalizeAddressesAndEncodeIdn(email)); } return normalizedEmail; } //----------------------------------------------------------------------------- // Based on KMail KMMsgBase::autoDetectCharset(). QByteArray autoDetectCharset(const QString& text) { static QList charsets; if (charsets.isEmpty()) charsets << "us-ascii" << "iso-8859-1" << "locale" << "utf-8"; for (int i = 0, count = charsets.count(); i < count; ++i) { QByteArray encoding = charsets[i]; if (encoding == "locale") { encoding = QTextCodec::codecForName(KLocale::global()->encoding())->name(); encoding = encoding.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) <<"Auto-Charset: Something is wrong and I cannot get a codec. [" << encoding <<"]"; + 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(); 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); 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('"')) 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/shellprocess.cpp b/src/lib/shellprocess.cpp index f3a1fd78..96f77934 100644 --- a/src/lib/shellprocess.cpp +++ b/src/lib/shellprocess.cpp @@ -1,226 +1,226 @@ /* * shellprocess.cpp - execute a shell process * Program: kalarm * Copyright © 2004,2005,2007,2008 by 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 "shellprocess.h" #include #include "kalarm_debug.h" #include #include #include #include QByteArray ShellProcess::mShellName; QByteArray ShellProcess::mShellPath; bool ShellProcess::mInitialised = false; bool ShellProcess::mAuthorised = false; ShellProcess::ShellProcess(const QString& command) : mCommand(command), mStdinBytes(0), mStatus(INACTIVE), mStdinExit(false) { } /****************************************************************************** * Execute a command. */ bool ShellProcess::start(OpenMode openMode) { if (!authorised()) { mStatus = UNAUTHORISED; return false; } connect(this, &QIODevice::bytesWritten, this, &ShellProcess::writtenStdin); connect(this, SIGNAL(finished(int,QProcess::ExitStatus)), SLOT(slotExited(int,QProcess::ExitStatus))); connect(this, &QProcess::readyReadStandardOutput, this, &ShellProcess::stdoutReady); connect(this, &QProcess::readyReadStandardError, this, &ShellProcess::stderrReady); QStringList args; args << QStringLiteral("-c") << mCommand; QProcess::start(QLatin1String(shellName()), args, openMode); if (!waitForStarted()) { mStatus = START_FAIL; return false; } mStatus = RUNNING; return true; } /****************************************************************************** * Called when a shell process execution completes. * Interprets the exit status according to which shell was called, and emits * a shellExited() signal. */ void ShellProcess::slotExited(int exitCode, QProcess::ExitStatus exitStatus) { - qCDebug(KALARM_LOG) << exitCode << "," << exitStatus; + qCDebug(KALARM_LOG) << "ShellProcess::slotExited:" << exitCode << "," << exitStatus; mStdinQueue.clear(); mStatus = SUCCESS; mExitCode = exitCode; if (exitStatus != NormalExit) { - qCWarning(KALARM_LOG) << mCommand << ":" << mShellName << ": crashed/killed"; + qCWarning(KALARM_LOG) << "ShellProcess::slotExited:" << mCommand << ":" << mShellName << ": crashed/killed"; mStatus = DIED; } else { // Some shells report if the command couldn't be found, or is not executable if ((mShellName == "bash" && (exitCode == 126 || exitCode == 127)) || (mShellName == "ksh" && exitCode == 127)) { - qCWarning(KALARM_LOG) << mCommand << ":" << mShellName << ": not found or not executable"; + qCWarning(KALARM_LOG) << "ShellProcess::slotExited:" << mCommand << ":" << mShellName << ": not found or not executable"; mStatus = NOT_FOUND; } } Q_EMIT shellExited(this); } /****************************************************************************** * Write a string to STDIN. */ void ShellProcess::writeStdin(const char* buffer, int bufflen) { QByteArray scopy(buffer, bufflen); // construct a deep copy bool doWrite = mStdinQueue.isEmpty(); mStdinQueue.enqueue(scopy); if (doWrite) { mStdinBytes = mStdinQueue.head().length(); write(mStdinQueue.head()); } } /****************************************************************************** * Called when output to STDIN completes. * Send the next queued output, if any. * Note that buffers written to STDIN must not be freed until the bytesWritten() * signal has been processed. */ void ShellProcess::writtenStdin(qint64 bytes) { mStdinBytes -= bytes; if (mStdinBytes > 0) return; // buffer has only been partially written so far if (!mStdinQueue.isEmpty()) mStdinQueue.dequeue(); // free the buffer which has now been written if (!mStdinQueue.isEmpty()) { mStdinBytes = mStdinQueue.head().length(); write(mStdinQueue.head()); } else if (mStdinExit) kill(); } /****************************************************************************** * Tell the process to exit once all STDIN strings have been written. */ void ShellProcess::stdinExit() { if (mStdinQueue.isEmpty()) kill(); else mStdinExit = true; } /****************************************************************************** * Return the error message corresponding to the command exit status. * Reply = null string if not yet exited, or if command successful. */ QString ShellProcess::errorMessage() const { switch (mStatus) { case UNAUTHORISED: return i18nc("@info", "Failed to execute command (shell access not authorized)"); case START_FAIL: case NOT_FOUND: return i18nc("@info", "Failed to execute command"); case DIED: return i18nc("@info", "Command execution error"); case SUCCESS: if (mExitCode) return i18nc("@info", "Command exit code: %1", mExitCode); // Fall through to INACTIVE Q_FALLTHROUGH(); case INACTIVE: case RUNNING: default: return QString(); } } /****************************************************************************** * Determine which shell to use. * Don't use the KProcess default shell, since we need to know which shell is * used in order to decide what its exit code means. */ const QByteArray& ShellProcess::shellPath() { if (mShellPath.isEmpty()) { // Get the path to the shell mShellPath = "/bin/sh"; QByteArray envshell = qgetenv("SHELL").trimmed(); if (!envshell.isEmpty()) { QT_STATBUF fileinfo; if (QT_STAT(envshell.data(), &fileinfo) != -1 // ensure file exists && !S_ISDIR(fileinfo.st_mode) // and it's not a directory && !S_ISCHR(fileinfo.st_mode) // and it's not a character device && !S_ISBLK(fileinfo.st_mode) // and it's not a block device #ifdef S_ISSOCK && !S_ISSOCK(fileinfo.st_mode) // and it's not a socket #endif && !S_ISFIFO(fileinfo.st_mode) // and it's not a fifo && !access(envshell.data(), X_OK)) // and it's executable mShellPath = envshell; } // Get the shell filename with the path stripped off int i = mShellPath.lastIndexOf('/'); if (i >= 0) mShellName = mShellPath.mid(i + 1); else mShellName = mShellPath; } return mShellPath; } /****************************************************************************** * Check whether shell commands are allowed at all. */ bool ShellProcess::authorised() { if (!mInitialised) { mAuthorised = KAuthorized::authorize(QStringLiteral("shell_access")); mInitialised = true; } return mAuthorised; } // vim: et sw=4: diff --git a/src/lib/stackedwidgets.cpp b/src/lib/stackedwidgets.cpp index 22af9511..85402d14 100644 --- a/src/lib/stackedwidgets.cpp +++ b/src/lib/stackedwidgets.cpp @@ -1,161 +1,161 @@ /* * stackedwidgets.cpp - group of stacked widgets * Program: kalarm * Copyright © 2008 by 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 "stackedwidgets.h" #include "desktop.h" #include #include #include "kalarm_debug.h" StackedScrollWidget::StackedScrollWidget(StackedScrollGroup* group, QWidget* parent) : StackedWidgetT(group, parent) { setFrameStyle(QFrame::NoFrame); setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); setWidgetResizable(true); setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding); } StackedScrollGroup::StackedScrollGroup(QDialog* dlg, QObject* tabParent) : StackedGroupT(tabParent), mDialog(dlg), mMinHeight(-1), mHeightReduction(0), mSized(false) { } /****************************************************************************** * Return the minimum size for the tab, adjusted if necessary to a height that * fits the screen. * In order to make the QStackedWidget containing the tabs take the correct * size, the value returned is actually the minimum size of the largest tab. * Otherwise, only the currently visible tab would be taken into account with * the result that the dialog would initially be displayed too small. */ QSize StackedScrollGroup::minimumSizeHint() const { QSize s = maxMinimumSizeHint(); if (!s.isEmpty() && mMinHeight > 0 && mMinHeight < s.height()) return QSize(s.width() + mWidgets[0]->style()->pixelMetric(QStyle::PM_ScrollBarExtent), mMinHeight); return s; } /****************************************************************************** * Return the maximum minimum size for any instance. */ QSize StackedScrollGroup::maxMinimumSizeHint() const { QSize sz; for (int i = 0, count = mWidgets.count(); i < count; ++i) { QWidget* w = static_cast(mWidgets[i])->widget(); if (!w) return QSize(); QSize s = w->minimumSizeHint(); if (!s.isValid()) return QSize(); sz = sz.expandedTo(s); } return sz; } /****************************************************************************** * Return the minimum size for the dialog. * If the minimum size would be too high to fit the desktop, the tab contents * are made scrollable. */ QSize StackedScrollGroup::adjustSize(bool force) { if (force) mSized = false; if (mSized) return QSize(); // Cancel any previous minimum height and set the height of the // scroll widget contents widgets. mMinHeight = -1; mHeightReduction = 0; QSize s = maxMinimumSizeHint(); if (s.isEmpty()) return QSize(); int maxTabHeight = s.height(); for (int i = 0, count = mWidgets.count(); i < count; ++i) { mWidgets[i]->setMinimumHeight(maxTabHeight); QWidget* w = static_cast(mWidgets[i])->widget(); if (w) w->resize(s); } for (QWidget* w = mWidgets[0]->parentWidget(); w && w != mDialog; w = w->parentWidget()) { w->setMinimumHeight(0); w->adjustSize(); } mDialog->setMinimumHeight(0); int decoration = mDialog->frameGeometry().height() - mDialog->geometry().height(); if (!decoration) { // On X11 at least, the window decoration height may not be // available, so use a guess of 25 pixels. decoration = 25; } int desk = KAlarm::desktopWorkArea().height(); // There is no stored size, or the deferral group is visible. // Allow the tab contents to be scrolled vertically if that is necessary // to avoid the dialog exceeding the screen height. QSize dlgsize = mDialog->QDialog::minimumSizeHint(); int y = dlgsize.height() + decoration - desk; if (y > 0) { mHeightReduction = y; mMinHeight = maxTabHeight - y; - qCDebug(KALARM_LOG) << "Scrolling: max tab height=" << maxTabHeight << ", reduction=" << mHeightReduction << "-> min tab height=" << mMinHeight; + qCDebug(KALARM_LOG) << "StackedScrollGroup::adjustSize: Scrolling: max tab height=" << maxTabHeight << ", reduction=" << mHeightReduction << "-> min tab height=" << mMinHeight; if (mMinHeight > 0) { for (int i = 0, count = mWidgets.count(); i < count; ++i) { mWidgets[i]->setMinimumHeight(mMinHeight); mWidgets[i]->resize(QSize(mWidgets[i]->width(), mMinHeight)); } } mSized = true; QSize s = mWidgets[0]->parentWidget()->sizeHint(); if (s.height() < mMinHeight) s.setHeight(mMinHeight); mWidgets[0]->parentWidget()->resize(s); for (QWidget* w = mWidgets[0]->parentWidget(); w && w != mDialog; w = w->parentWidget()) w->setMinimumHeight(qMin(w->minimumSizeHint().height(), w->sizeHint().height())); dlgsize.setHeight(dlgsize.height() - mHeightReduction); s = mDialog->QDialog::minimumSizeHint(); if (s.height() > dlgsize.height()) dlgsize.setHeight(s.height()); mDialog->setMinimumHeight(dlgsize.height()); } mSized = true; mDialog->resize(dlgsize); return s; } // vim: et sw=4: diff --git a/src/lib/synchtimer.cpp b/src/lib/synchtimer.cpp index f1245deb..9d720109 100644 --- a/src/lib/synchtimer.cpp +++ b/src/lib/synchtimer.cpp @@ -1,242 +1,242 @@ /* * synchtimer.cpp - timers which synchronize to time boundaries * Program: kalarm * Copyright © 2004,2005,2007-2009 by 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 "synchtimer.h" #include #include #include #include "kalarm_debug.h" /*============================================================================= = Class: SynchTimer = Virtual base class for application-wide timer synchronized to a time boundary. =============================================================================*/ SynchTimer::SynchTimer() { mTimer = new QTimer(this); mTimer->setSingleShot(true); } SynchTimer::~SynchTimer() { delete mTimer; mTimer = nullptr; } /****************************************************************************** * Connect to the timer. The timer is started if necessary. */ void SynchTimer::connecT(QObject* receiver, const char* member) { Connection connection(receiver, member); if (mConnections.contains(connection)) return; // the slot is already connected, so ignore request connect(mTimer, SIGNAL(timeout()), receiver, member); mConnections.append(connection); if (!mTimer->isActive()) { connect(mTimer, &QTimer::timeout, this, &SynchTimer::slotTimer); start(); } } /****************************************************************************** * Disconnect from the timer. The timer is stopped if no longer needed. */ void SynchTimer::disconnecT(QObject* receiver, const char* member) { if (mTimer) { mTimer->disconnect(receiver, member); if (member) { int i = mConnections.indexOf(Connection(receiver, member)); if (i >= 0) mConnections.removeAt(i); } else { for (int i = 0; i < mConnections.count(); ) { if (mConnections[i].receiver == receiver) mConnections.removeAt(i); else ++i; } } if (mConnections.isEmpty()) { mTimer->disconnect(); mTimer->stop(); } } } /*============================================================================= = Class: MinuteTimer = Application-wide timer synchronized to the minute boundary. =============================================================================*/ MinuteTimer* MinuteTimer::mInstance = nullptr; MinuteTimer* MinuteTimer::instance() { if (!mInstance) mInstance = new MinuteTimer; return mInstance; } /****************************************************************************** * Called when the timer triggers, or to start the timer. * Timers can under some circumstances wander off from the correct trigger time, * so rather than setting a 1 minute interval, calculate the correct next * interval each time it triggers. */ void MinuteTimer::slotTimer() { - qCDebug(KALARM_LOG); + qCDebug(KALARM_LOG) << "MinuteTimer::slotTimer"; int interval = 62 - QTime::currentTime().second(); mTimer->start(interval * 1000); // execute a single shot } /*============================================================================= = Class: DailyTimer = Application-wide timer synchronized to midnight. =============================================================================*/ QList DailyTimer::mFixedTimers; DailyTimer::DailyTimer(const QTime& timeOfDay, bool fixed) : mTime(timeOfDay), mFixed(fixed) { if (fixed) mFixedTimers.append(this); } DailyTimer::~DailyTimer() { if (mFixed) mFixedTimers.removeAt(mFixedTimers.indexOf(this)); } DailyTimer* DailyTimer::fixedInstance(const QTime& timeOfDay, bool create) { for (int i = 0, end = mFixedTimers.count(); i < end; ++i) if (mFixedTimers[i]->mTime == timeOfDay) return mFixedTimers[i]; return create ? new DailyTimer(timeOfDay, true) : nullptr; } /****************************************************************************** * Disconnect from the timer signal which triggers at the given fixed time of day. * If there are no remaining connections to that timer, it is destroyed. */ void DailyTimer::disconnect(const QTime& timeOfDay, QObject* receiver, const char* member) { DailyTimer* timer = fixedInstance(timeOfDay, false); if (!timer) return; timer->disconnecT(receiver, member); if (!timer->hasConnections()) delete timer; } /****************************************************************************** * Change the time at which the variable timer triggers. */ void DailyTimer::changeTime(const QTime& newTimeOfDay, bool triggerMissed) { if (mFixed) return; if (mTimer->isActive()) { mTimer->stop(); bool triggerNow = false; if (triggerMissed) { QTime now = QTime::currentTime(); if (now >= newTimeOfDay && now < mTime) { // The trigger time is now earlier and it has already arrived today. // Trigger a timer event immediately. triggerNow = true; } } mTime = newTimeOfDay; if (triggerNow) mTimer->start(0); // trigger immediately else start(); } else mTime = newTimeOfDay; } /****************************************************************************** * Initialise the timer to trigger at the specified time. * This will either be today or tomorrow, depending on whether the trigger time * has already passed. */ void DailyTimer::start() { // TIMEZONE = local time QDateTime now = QDateTime::currentDateTime(); // Find out whether to trigger today or tomorrow. // In preference, use the last trigger date to determine this, since // that will avoid possible errors due to daylight savings time changes. bool today; if (mLastDate.isValid()) today = (mLastDate < now.date()); else today = (now.time() < mTime); QDateTime next; if (today) next = QDateTime(now.date(), mTime); else next = QDateTime(now.date().addDays(1), mTime); uint interval = next.toTime_t() - now.toTime_t(); mTimer->start(interval * 1000); // execute a single shot - qCDebug(KALARM_LOG) << "at" << mTime.hour() << ":" << mTime.minute() << ": interval =" << interval/3600 << ":" << (interval/60)%60 << ":" << interval%60; + qCDebug(KALARM_LOG) << "DailyTimer::start: at" << mTime.hour() << ":" << mTime.minute() << ": interval =" << interval/3600 << ":" << (interval/60)%60 << ":" << interval%60; } /****************************************************************************** * Called when the timer triggers. * Set the timer to trigger again tomorrow at the specified time. * Note that if daylight savings time changes occur, this will not be 24 hours * from now. */ void DailyTimer::slotTimer() { // TIMEZONE = local time QDateTime now = QDateTime::currentDateTime(); mLastDate = now.date(); QDateTime next = QDateTime(mLastDate.addDays(1), mTime); uint interval = next.toTime_t() - now.toTime_t(); mTimer->start(interval * 1000); // execute a single shot - qCDebug(KALARM_LOG) << "at" << mTime.hour() << ":" << mTime.minute() << ": interval =" << interval/3600 << ":" << (interval/60)%60 << ":" << interval%60; + qCDebug(KALARM_LOG) << "DailyTimer::slotTimer: at" << mTime.hour() << ":" << mTime.minute() << ": interval =" << interval/3600 << ":" << (interval/60)%60 << ":" << interval%60; } // vim: et sw=4: diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index d36cacbc..dfa5569c 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -1,1590 +1,1590 @@ /* * mainwindow.cpp - main application window * Program: kalarm * Copyright © 2001-2018 by David Jarvie * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kalarm.h" #include "mainwindow.h" #include "alarmcalendar.h" #include "alarmlistdelegate.h" #include "autoqpointer.h" #include "alarmlistview.h" #include "birthdaydlg.h" #include "functions.h" #include "kalarmapp.h" #include "kamail.h" #include "messagebox.h" #include "newalarmaction.h" #include "prefdlg.h" #include "preferences.h" #include "resourceselector.h" #include "synchtimer.h" #include "templatedlg.h" #include "templatemenuaction.h" #include "templatepickdlg.h" #include "traywindow.h" #include "wakedlg.h" #include "kalarm_debug.h" #include #include #include #include #include #include #include using namespace KCalCore; using namespace KCalUtils; #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace KAlarmCal; namespace { const QString UI_FILE(QStringLiteral("kalarmui.rc")); const char* WINDOW_NAME = "MainWindow"; const char* VIEW_GROUP = "View"; const char* SHOW_TIME_KEY = "ShowAlarmTime"; const char* SHOW_TIME_TO_KEY = "ShowTimeToAlarm"; const char* SHOW_ARCHIVED_KEY = "ShowArchivedAlarms"; const char* SHOW_RESOURCES_KEY = "ShowResources"; QString undoText; QString undoTextStripped; QList undoShortcut; QString redoText; QString redoTextStripped; QList redoShortcut; } /*============================================================================= = Class: MainWindow =============================================================================*/ MainWindow::WindowList MainWindow::mWindowList; TemplateDlg* MainWindow::mTemplateDlg = nullptr; // Collect these widget labels together to ensure consistent wording and // translations across different modules. QString MainWindow::i18n_a_ShowAlarmTimes() { return i18nc("@action", "Show &Alarm Times"); } QString MainWindow::i18n_chk_ShowAlarmTime() { return i18nc("@option:check", "Show alarm time"); } QString MainWindow::i18n_o_ShowTimeToAlarms() { return i18nc("@action", "Show Time t&o Alarms"); } QString MainWindow::i18n_chk_ShowTimeToAlarm() { return i18nc("@option:check", "Show time until alarm"); } /****************************************************************************** * Construct an instance. * To avoid resize() events occurring while still opening the calendar (and * resultant crashes), the calendar is opened before constructing the instance. */ MainWindow* MainWindow::create(bool restored) { theApp()->checkCalendar(); // ensure calendar is open return new MainWindow(restored); } MainWindow::MainWindow(bool restored) : MainWindowBase(nullptr, Qt::WindowContextHelpButtonHint), mResourcesWidth(-1), mHiddenTrayParent(false), mShown(false), mResizing(false) { - qCDebug(KALARM_LOG); + qCDebug(KALARM_LOG) << "MainWindow:"; setAttribute(Qt::WA_DeleteOnClose); setWindowModality(Qt::WindowModal); setObjectName(QStringLiteral("MainWin")); // used by LikeBack setPlainCaption(KAboutData::applicationData().displayName()); KConfigGroup config(KSharedConfig::openConfig(), VIEW_GROUP); mShowResources = config.readEntry(SHOW_RESOURCES_KEY, false); mShowArchived = config.readEntry(SHOW_ARCHIVED_KEY, false); mShowTime = config.readEntry(SHOW_TIME_KEY, true); mShowTimeTo = config.readEntry(SHOW_TIME_TO_KEY, false); if (!restored) { KConfigGroup wconfig(KSharedConfig::openConfig(), WINDOW_NAME); mResourcesWidth = wconfig.readEntry(QStringLiteral("Splitter %1").arg(qApp->desktop()->width()), (int)0); } setAcceptDrops(true); // allow drag-and-drop onto this window if (!mShowTimeTo) mShowTime = true; // ensure at least one time column is visible mSplitter = new QSplitter(Qt::Horizontal, this); mSplitter->setChildrenCollapsible(false); mSplitter->installEventFilter(this); setCentralWidget(mSplitter); // Create the calendar resource selector widget Akonadi::ControlGui::widgetNeedsAkonadi(this); mResourceSelector = new ResourceSelector(mSplitter); mSplitter->setStretchFactor(0, 0); // don't resize resource selector when window is resized mSplitter->setStretchFactor(1, 1); // Create the alarm list widget mListFilterModel = new AlarmListModel(this); mListFilterModel->setEventTypeFilter(mShowArchived ? CalEvent::ACTIVE | CalEvent::ARCHIVED : CalEvent::ACTIVE); mListView = new AlarmListView(WINDOW_NAME, mSplitter); mListView->setModel(mListFilterModel); mListView->selectTimeColumns(mShowTime, mShowTimeTo); mListView->sortByColumn(mShowTime ? AlarmListModel::TimeColumn : AlarmListModel::TimeToColumn, Qt::AscendingOrder); mListView->setItemDelegate(new AlarmListDelegate(mListView)); connect(mListView->selectionModel(), &QItemSelectionModel::selectionChanged, this, &MainWindow::slotSelection); connect(mListView, &AlarmListView::contextMenuRequested, this, &MainWindow::slotContextMenuRequested); connect(AkonadiModel::instance(), &AkonadiModel::collectionStatusChanged, this, &MainWindow::slotCalendarStatusChanged); connect(mResourceSelector, &ResourceSelector::resized, this, &MainWindow::resourcesResized); mListView->installEventFilter(this); initActions(); setAutoSaveSettings(QLatin1String(WINDOW_NAME), true); // save toolbars, window sizes etc. mWindowList.append(this); if (mWindowList.count() == 1) { // It's the first main window if (theApp()->wantShowInSystemTray()) theApp()->displayTrayIcon(true, this); // create system tray icon for run-in-system-tray mode else if (theApp()->trayWindow()) theApp()->trayWindow()->setAssocMainWindow(this); // associate this window with the system tray icon } slotCalendarStatusChanged(); // initialise action states now that window is registered } MainWindow::~MainWindow() { - qCDebug(KALARM_LOG); + qCDebug(KALARM_LOG) << "~MainWindow"; bool trayParent = isTrayParent(); // must call before removing from window list mWindowList.removeAt(mWindowList.indexOf(this)); // Prevent view updates during window destruction delete mResourceSelector; mResourceSelector = nullptr; delete mListView; mListView = nullptr; if (theApp()->trayWindow()) { if (trayParent) delete theApp()->trayWindow(); else theApp()->trayWindow()->removeWindow(this); } KSharedConfig::openConfig()->sync(); // save any new window size to disc theApp()->quitIf(); } /****************************************************************************** * Called when the QApplication::saveStateRequest() signal has been emitted. * Save settings to the session managed config file, for restoration * when the program is restored. */ void MainWindow::saveProperties(KConfigGroup& config) { config.writeEntry("HiddenTrayParent", isTrayParent() && isHidden()); config.writeEntry("ShowArchived", mShowArchived); config.writeEntry("ShowTime", mShowTime); config.writeEntry("ShowTimeTo", mShowTimeTo); config.writeEntry("ResourcesWidth", mResourceSelector->isHidden() ? 0 : mResourceSelector->width()); } /****************************************************************************** * Read settings from the session managed config file. * This function is automatically called whenever the app is being * restored. Read in whatever was saved in saveProperties(). */ void MainWindow::readProperties(const KConfigGroup& config) { mHiddenTrayParent = config.readEntry("HiddenTrayParent", true); mShowArchived = config.readEntry("ShowArchived", false); mShowTime = config.readEntry("ShowTime", true); mShowTimeTo = config.readEntry("ShowTimeTo", false); mResourcesWidth = config.readEntry("ResourcesWidth", (int)0); mShowResources = (mResourcesWidth > 0); } /****************************************************************************** * Get the main main window, i.e. the parent of the system tray icon, or if * none, the first main window to be created. Visible windows take precedence * over hidden ones. */ MainWindow* MainWindow::mainMainWindow() { MainWindow* tray = theApp()->trayWindow() ? theApp()->trayWindow()->assocMainWindow() : nullptr; if (tray && tray->isVisible()) return tray; for (int i = 0, end = mWindowList.count(); i < end; ++i) if (mWindowList[i]->isVisible()) return mWindowList[i]; if (tray) return tray; if (mWindowList.isEmpty()) return nullptr; return mWindowList[0]; } /****************************************************************************** * Check whether this main window is effectively the parent of the system tray icon. */ bool MainWindow::isTrayParent() const { TrayWindow* tray = theApp()->trayWindow(); if (!tray || !QSystemTrayIcon::isSystemTrayAvailable()) return false; if (tray->assocMainWindow() == this) return true; return mWindowList.count() == 1; } /****************************************************************************** * Close all main windows. */ void MainWindow::closeAll() { while (!mWindowList.isEmpty()) delete mWindowList[0]; // N.B. the destructor removes the window from the list } /****************************************************************************** * Intercept events for the splitter widget. */ bool MainWindow::eventFilter(QObject* obj, QEvent* e) { if (obj == mSplitter) { switch (e->type()) { case QEvent::Resize: // Don't change resources size while WINDOW is being resized. // Resize event always occurs before Paint. mResizing = true; break; case QEvent::Paint: // Allow resources to be resized again mResizing = false; break; default: break; } } else if (obj == mListView) { switch (e->type()) { case QEvent::KeyPress: { QKeyEvent* ke = static_cast(e); if (ke->key() == Qt::Key_Delete && ke->modifiers() == Qt::ShiftModifier) { // Prevent Shift-Delete being processed by EventListDelegate mActionDeleteForce->trigger(); return true; } break; } default: break; } } return false; } /****************************************************************************** * Called when the window's size has changed (before it is painted). * Sets the last column in the list view to extend at least to the right hand * edge of the list view. * Records the new size in the config file. */ void MainWindow::resizeEvent(QResizeEvent* re) { // Save the window's new size only if it's the first main window MainWindowBase::resizeEvent(re); if (mResourcesWidth > 0) { QList widths; widths.append(mResourcesWidth); widths.append(width() - mResourcesWidth - mSplitter->handleWidth()); mSplitter->setSizes(widths); } } void MainWindow::resourcesResized() { if (!mShown || mResizing) return; QList widths = mSplitter->sizes(); if (widths.count() > 1) { mResourcesWidth = widths[0]; // Width is reported as non-zero when resource selector is // actually invisible, so note a zero width in these circumstances. if (mResourcesWidth <= 5) mResourcesWidth = 0; else if (mainMainWindow() == this) { KConfigGroup config(KSharedConfig::openConfig(), WINDOW_NAME); config.writeEntry(QStringLiteral("Splitter %1").arg(qApp->desktop()->width()), mResourcesWidth); } } } /****************************************************************************** * Called when the window is first displayed. * Sets the last column in the list view to extend at least to the right hand * edge of the list view. */ void MainWindow::showEvent(QShowEvent* se) { if (mResourcesWidth > 0) { QList widths; widths.append(mResourcesWidth); widths.append(width() - mResourcesWidth - mSplitter->handleWidth()); mSplitter->setSizes(widths); } MainWindowBase::showEvent(se); mShown = true; } /****************************************************************************** * Display the window. */ void MainWindow::show() { MainWindowBase::show(); if (mMenuError) { // Show error message now that the main window has been displayed. // Waiting until now lets the user easily associate the message with // the main window which is faulty. KAMessageBox::error(this, xi18nc("@info", "Failure to create menus (perhaps %1 missing or corrupted)", UI_FILE)); mMenuError = false; } } /****************************************************************************** * Called after the window is hidden. */ void MainWindow::hideEvent(QHideEvent* he) { MainWindowBase::hideEvent(he); } /****************************************************************************** * Initialise the menu, toolbar and main window actions. */ void MainWindow::initActions() { KActionCollection* actions = actionCollection(); mActionTemplates = new QAction(i18nc("@action", "&Templates..."), this); actions->addAction(QStringLiteral("templates"), mActionTemplates); connect(mActionTemplates, &QAction::triggered, this, &MainWindow::slotTemplates); mActionNew = new NewAlarmAction(false, i18nc("@action", "&New"), this, actions); actions->addAction(QStringLiteral("new"), mActionNew); QAction* action = mActionNew->displayAlarmAction(QStringLiteral("newDisplay")); connect(action, &QAction::triggered, this, &MainWindow::slotNewDisplay); action = mActionNew->commandAlarmAction(QStringLiteral("newCommand")); connect(action, &QAction::triggered, this, &MainWindow::slotNewCommand); action = mActionNew->emailAlarmAction(QStringLiteral("newEmail")); connect(action, &QAction::triggered, this, &MainWindow::slotNewEmail); action = mActionNew->audioAlarmAction(QStringLiteral("newAudio")); connect(action, &QAction::triggered, this, &MainWindow::slotNewAudio); TemplateMenuAction* templateMenuAction = mActionNew->fromTemplateAlarmAction(QStringLiteral("newFromTemplate")); connect(templateMenuAction, &TemplateMenuAction::selected, this, &MainWindow::slotNewFromTemplate); mActionCreateTemplate = new QAction(i18nc("@action", "Create Tem&plate..."), this); actions->addAction(QStringLiteral("createTemplate"), mActionCreateTemplate); connect(mActionCreateTemplate, &QAction::triggered, this, &MainWindow::slotNewTemplate); mActionCopy = new QAction(QIcon::fromTheme(QStringLiteral("edit-copy")), i18nc("@action", "&Copy..."), this); actions->addAction(QStringLiteral("copy"), mActionCopy); actions->setDefaultShortcut(mActionCopy, QKeySequence(Qt::SHIFT + Qt::Key_Insert)); connect(mActionCopy, &QAction::triggered, this, &MainWindow::slotCopy); mActionModify = new QAction(QIcon::fromTheme(QStringLiteral("document-properties")), i18nc("@action", "&Edit..."), this); actions->addAction(QStringLiteral("modify"), mActionModify); actions->setDefaultShortcut(mActionModify, QKeySequence(Qt::CTRL + Qt::Key_E)); connect(mActionModify, &QAction::triggered, this, &MainWindow::slotModify); mActionDelete = new QAction(QIcon::fromTheme(QStringLiteral("edit-delete")), i18nc("@action", "&Delete"), this); actions->addAction(QStringLiteral("delete"), mActionDelete); actions->setDefaultShortcut(mActionDelete, QKeySequence::Delete); connect(mActionDelete, &QAction::triggered, this, &MainWindow::slotDeleteIf); // Set up Shift-Delete as a shortcut to delete without confirmation mActionDeleteForce = new QAction(i18nc("@action", "Delete Without Confirmation"), this); actions->addAction(QStringLiteral("delete-force"), mActionDeleteForce); actions->setDefaultShortcut(mActionDeleteForce, QKeySequence::Delete + Qt::SHIFT); connect(mActionDeleteForce, &QAction::triggered, this, &MainWindow::slotDeleteForce); mActionReactivate = new QAction(i18nc("@action", "Reac&tivate"), this); actions->addAction(QStringLiteral("undelete"), mActionReactivate); actions->setDefaultShortcut(mActionReactivate, QKeySequence(Qt::CTRL + Qt::Key_R)); connect(mActionReactivate, &QAction::triggered, this, &MainWindow::slotReactivate); mActionEnable = new QAction(this); actions->addAction(QStringLiteral("disable"), mActionEnable); actions->setDefaultShortcut(mActionEnable, QKeySequence(Qt::CTRL + Qt::Key_B)); connect(mActionEnable, &QAction::triggered, this, &MainWindow::slotEnable); action = new QAction(i18nc("@action", "Wake From Suspend..."), this); actions->addAction(QStringLiteral("wakeSuspend"), action); connect(action, &QAction::triggered, this, &MainWindow::slotWakeFromSuspend); action = KAlarm::createStopPlayAction(this); actions->addAction(QStringLiteral("stopAudio"), action); KGlobalAccel::setGlobalShortcut(action, QList()); // allow user to set a global shortcut mActionShowTime = new KToggleAction(i18n_a_ShowAlarmTimes(), this); actions->addAction(QStringLiteral("showAlarmTimes"), mActionShowTime); connect(mActionShowTime, &KToggleAction::triggered, this, &MainWindow::slotShowTime); mActionShowTimeTo = new KToggleAction(i18n_o_ShowTimeToAlarms(), this); actions->addAction(QStringLiteral("showTimeToAlarms"), mActionShowTimeTo); actions->setDefaultShortcut(mActionShowTimeTo, QKeySequence(Qt::CTRL + Qt::Key_I)); connect(mActionShowTimeTo, &KToggleAction::triggered, this, &MainWindow::slotShowTimeTo); mActionShowArchived = new KToggleAction(i18nc("@action", "Show Archi&ved Alarms"), this); actions->addAction(QStringLiteral("showArchivedAlarms"), mActionShowArchived); actions->setDefaultShortcut(mActionShowArchived, QKeySequence(Qt::CTRL + Qt::Key_P)); connect(mActionShowArchived, &KToggleAction::triggered, this, &MainWindow::slotShowArchived); mActionToggleTrayIcon = new KToggleAction(i18nc("@action", "Show in System &Tray"), this); actions->addAction(QStringLiteral("showInSystemTray"), mActionToggleTrayIcon); connect(mActionToggleTrayIcon, &KToggleAction::triggered, this, &MainWindow::slotToggleTrayIcon); mActionToggleResourceSel = new KToggleAction(QIcon::fromTheme(QStringLiteral("view-choose")), i18nc("@action", "Show &Calendars"), this); actions->addAction(QStringLiteral("showResources"), mActionToggleResourceSel); connect(mActionToggleResourceSel, &KToggleAction::triggered, this, &MainWindow::slotToggleResourceSelector); mActionSpreadWindows = KAlarm::createSpreadWindowsAction(this); actions->addAction(QStringLiteral("spread"), mActionSpreadWindows); KGlobalAccel::setGlobalShortcut(mActionSpreadWindows, QList()); // allow user to set a global shortcut mActionImportAlarms = new QAction(i18nc("@action", "Import &Alarms..."), this); actions->addAction(QStringLiteral("importAlarms"), mActionImportAlarms); connect(mActionImportAlarms, &QAction::triggered, this, &MainWindow::slotImportAlarms); mActionImportBirthdays = new QAction(i18nc("@action", "Import &Birthdays..."), this); actions->addAction(QStringLiteral("importBirthdays"), mActionImportBirthdays); connect(mActionImportBirthdays, &QAction::triggered, this, &MainWindow::slotBirthdays); mActionExportAlarms = new QAction(i18nc("@action", "E&xport Selected Alarms..."), this); actions->addAction(QStringLiteral("exportAlarms"), mActionExportAlarms); connect(mActionExportAlarms, &QAction::triggered, this, &MainWindow::slotExportAlarms); mActionExport = new QAction(i18nc("@action", "E&xport..."), this); actions->addAction(QStringLiteral("export"), mActionExport); connect(mActionExport, &QAction::triggered, this, &MainWindow::slotExportAlarms); action = new QAction(QIcon::fromTheme(QStringLiteral("view-refresh")), i18nc("@action", "&Refresh Alarms"), this); actions->addAction(QStringLiteral("refreshAlarms"), action); connect(action, &QAction::triggered, this, &MainWindow::slotRefreshAlarms); KToggleAction* toggleAction = KAlarm::createAlarmEnableAction(this); actions->addAction(QStringLiteral("alarmsEnable"), toggleAction); if (undoText.isNull()) { // Get standard texts, etc., for Undo and Redo actions QAction* act = KStandardAction::undo(this, nullptr, actions); undoShortcut = act->shortcuts(); undoText = act->text(); undoTextStripped = KLocalizedString::removeAcceleratorMarker(undoText); delete act; act = KStandardAction::redo(this, nullptr, actions); redoShortcut = act->shortcuts(); redoText = act->text(); redoTextStripped = KLocalizedString::removeAcceleratorMarker(redoText); delete act; } mActionUndo = new KToolBarPopupAction(QIcon::fromTheme(QStringLiteral("edit-undo")), undoText, this); actions->addAction(QStringLiteral("edit_undo"), mActionUndo); actions->setDefaultShortcuts(mActionUndo, undoShortcut); connect(mActionUndo, &KToolBarPopupAction::triggered, this, &MainWindow::slotUndo); mActionRedo = new KToolBarPopupAction(QIcon::fromTheme(QStringLiteral("edit-redo")), redoText, this); actions->addAction(QStringLiteral("edit_redo"), mActionRedo); actions->setDefaultShortcuts(mActionRedo, redoShortcut); connect(mActionRedo, &KToolBarPopupAction::triggered, this, &MainWindow::slotRedo); KStandardAction::find(mListView, SLOT(slotFind()), actions); mActionFindNext = KStandardAction::findNext(mListView, SLOT(slotFindNext()), actions); mActionFindPrev = KStandardAction::findPrev(mListView, SLOT(slotFindPrev()), actions); KStandardAction::selectAll(mListView, SLOT(selectAll()), actions); KStandardAction::deselect(mListView, SLOT(clearSelection()), actions); // Quit only once the event loop is called; otherwise, the parent window will // be deleted while still processing the action, resulting in a crash. QAction* act = KStandardAction::quit(nullptr, nullptr, actions); connect(act, &QAction::triggered, this, &MainWindow::slotQuit, Qt::QueuedConnection); KStandardAction::keyBindings(this, SLOT(slotConfigureKeys()), actions); KStandardAction::configureToolbars(this, SLOT(slotConfigureToolbar()), actions); KStandardAction::preferences(this, SLOT(slotPreferences()), actions); mResourceSelector->initActions(actions); setStandardToolBarMenuEnabled(true); createGUI(UI_FILE); // Load menu and toolbar settings applyMainWindowSettings(KSharedConfig::openConfig()->group(WINDOW_NAME)); mContextMenu = static_cast(factory()->container(QStringLiteral("listContext"), this)); mActionsMenu = static_cast(factory()->container(QStringLiteral("actions"), this)); QMenu* resourceMenu = static_cast(factory()->container(QStringLiteral("resourceContext"), this)); mResourceSelector->setContextMenu(resourceMenu); mMenuError = (!mContextMenu || !mActionsMenu || !resourceMenu); connect(mActionUndo->menu(), &QMenu::aboutToShow, this, &MainWindow::slotInitUndoMenu); connect(mActionUndo->menu(), &QMenu::triggered, this, &MainWindow::slotUndoItem); connect(mActionRedo->menu(), &QMenu::aboutToShow, this, &MainWindow::slotInitRedoMenu); connect(mActionRedo->menu(), &QMenu::triggered, this, &MainWindow::slotRedoItem); connect(Undo::instance(), &Undo::changed, this, &MainWindow::slotUndoStatus); connect(mListView, &AlarmListView::findActive, this, &MainWindow::slotFindActive); Preferences::connect(SIGNAL(archivedKeepDaysChanged(int)), this, SLOT(updateKeepArchived(int))); Preferences::connect(SIGNAL(showInSystemTrayChanged(bool)), this, SLOT(updateTrayIconAction())); connect(theApp(), &KAlarmApp::trayIconToggled, this, &MainWindow::updateTrayIconAction); // Set menu item states setEnableText(true); mActionShowTime->setChecked(mShowTime); mActionShowTimeTo->setChecked(mShowTimeTo); mActionShowArchived->setChecked(mShowArchived); if (!Preferences::archivedKeepDays()) mActionShowArchived->setEnabled(false); mActionToggleResourceSel->setChecked(mShowResources); slotToggleResourceSelector(); updateTrayIconAction(); // set the correct text for this action mActionUndo->setEnabled(Undo::haveUndo()); mActionRedo->setEnabled(Undo::haveRedo()); mActionFindNext->setEnabled(false); mActionFindPrev->setEnabled(false); mActionCopy->setEnabled(false); mActionModify->setEnabled(false); mActionDelete->setEnabled(false); mActionReactivate->setEnabled(false); mActionEnable->setEnabled(false); mActionCreateTemplate->setEnabled(false); mActionExport->setEnabled(false); Undo::emitChanged(); // set the Undo/Redo menu texts // Daemon::monitoringAlarms(); } /****************************************************************************** * Enable or disable the Templates menu item in every main window instance. */ void MainWindow::enableTemplateMenuItem(bool enable) { for (int i = 0, end = mWindowList.count(); i < end; ++i) mWindowList[i]->mActionTemplates->setEnabled(enable); } /****************************************************************************** * Refresh the alarm list in every main window instance. */ void MainWindow::refresh() { - qCDebug(KALARM_LOG); + qCDebug(KALARM_LOG) << "MainWindow::refresh"; AkonadiModel::instance()->reload(); } /****************************************************************************** * Called when the keep archived alarm setting changes in the user preferences. * Enable/disable Show archived alarms option. */ void MainWindow::updateKeepArchived(int days) { - qCDebug(KALARM_LOG) << (bool)days; + qCDebug(KALARM_LOG) << "MainWindow::updateKeepArchived:" << (bool)days; if (mShowArchived && !days) slotShowArchived(); // toggle Show Archived option setting mActionShowArchived->setEnabled(days); } /****************************************************************************** * Select an alarm in the displayed list. */ void MainWindow::selectEvent(Akonadi::Item::Id eventId) { mListView->clearSelection(); QModelIndex index = mListFilterModel->eventIndex(eventId); if (index.isValid()) { mListView->select(index); mListView->scrollTo(index); } } /****************************************************************************** * Return the single selected alarm in the displayed list. */ KAEvent MainWindow::selectedEvent() const { return mListView->selectedEvent(); } /****************************************************************************** * Deselect all alarms in the displayed list. */ void MainWindow::clearSelection() { mListView->clearSelection(); } /****************************************************************************** * Called when the New button is clicked to edit a new alarm to add to the list. */ void MainWindow::slotNew(EditAlarmDlg::Type type) { KAlarm::editNewAlarm(type, mListView); } /****************************************************************************** * Called when a template is selected from the New From Template popup menu. * Executes a New Alarm dialog, preset from the selected template. */ void MainWindow::slotNewFromTemplate(const KAEvent* tmplate) { KAlarm::editNewAlarm(tmplate, mListView); } /****************************************************************************** * Called when the New Template button is clicked to create a new template * based on the currently selected alarm. */ void MainWindow::slotNewTemplate() { KAEvent event = mListView->selectedEvent(); if (event.isValid()) KAlarm::editNewTemplate(&event, this); } /****************************************************************************** * Called when the Copy button is clicked to edit a copy of an existing alarm, * to add to the list. */ void MainWindow::slotCopy() { KAEvent event = mListView->selectedEvent(); if (event.isValid()) KAlarm::editNewAlarm(&event, this); } /****************************************************************************** * Called when the Modify button is clicked to edit the currently highlighted * alarm in the list. */ void MainWindow::slotModify() { KAEvent event = mListView->selectedEvent(); if (event.isValid()) KAlarm::editAlarm(&event, this); // edit alarm (view-only mode if archived or read-only) } /****************************************************************************** * Called when the Delete button is clicked to delete the currently highlighted * alarms in the list. */ void MainWindow::slotDelete(bool force) { QVector events = mListView->selectedEvents(); if (!force && Preferences::confirmAlarmDeletion()) { int n = events.count(); if (KAMessageBox::warningContinueCancel(this, i18ncp("@info", "Do you really want to delete the selected alarm?", "Do you really want to delete the %1 selected alarms?", n), i18ncp("@title:window", "Delete Alarm", "Delete Alarms", n), KGuiItem(i18nc("@action:button", "&Delete"), QStringLiteral("edit-delete")), KStandardGuiItem::cancel(), Preferences::CONFIRM_ALARM_DELETION) != KMessageBox::Continue) return; } // Remove any events which have just triggered, from the list to delete. Undo::EventList undos; AlarmCalendar* resources = AlarmCalendar::resources(); for (int i = 0; i < events.count(); ) { Akonadi::Collection c = resources->collectionForEvent(events[i].itemId()); if (!c.isValid()) events.remove(i); else undos.append(events[i++], c); } if (events.isEmpty()) - qCDebug(KALARM_LOG) << "No alarms left to delete"; + qCDebug(KALARM_LOG) << "MainWindow::slotDelete: No alarms left to delete"; else { // Delete the events from the calendar and displays KAlarm::deleteEvents(events, true, this); Undo::saveDeletes(undos); } } /****************************************************************************** * Called when the Reactivate button is clicked to reinstate the currently * highlighted archived alarms in the list. */ void MainWindow::slotReactivate() { QVector events = mListView->selectedEvents(); mListView->clearSelection(); // Add the alarms to the displayed lists and to the calendar file Undo::EventList undos; QVector ineligibleIDs; KAlarm::reactivateEvents(events, ineligibleIDs, nullptr, this); // Create the undo list, excluding ineligible events AlarmCalendar* resources = AlarmCalendar::resources(); for (int i = 0, end = events.count(); i < end; ++i) { if (!ineligibleIDs.contains(EventId(events[i]))) { undos.append(events[i], resources->collectionForEvent(events[i].itemId())); } } Undo::saveReactivates(undos); } /****************************************************************************** * Called when the Enable/Disable button is clicked to enable or disable the * currently highlighted alarms in the list. */ void MainWindow::slotEnable() { bool enable = mActionEnableEnable; // save since changed in response to KAlarm::enableEvent() QVector events = mListView->selectedEvents(); QVector eventCopies; for (int i = 0, end = events.count(); i < end; ++i) eventCopies += events[i]; KAlarm::enableEvents(eventCopies, enable, this); slotSelection(); // update Enable/Disable action text } /****************************************************************************** * Called when the Show Alarm Times menu item is selected or deselected. */ void MainWindow::slotShowTime() { mShowTime = !mShowTime; mActionShowTime->setChecked(mShowTime); if (!mShowTime && !mShowTimeTo) slotShowTimeTo(); // at least one time column must be displayed else { mListView->selectTimeColumns(mShowTime, mShowTimeTo); KConfigGroup config(KSharedConfig::openConfig(), VIEW_GROUP); config.writeEntry(SHOW_TIME_KEY, mShowTime); config.writeEntry(SHOW_TIME_TO_KEY, mShowTimeTo); } } /****************************************************************************** * Called when the Show Time To Alarms menu item is selected or deselected. */ void MainWindow::slotShowTimeTo() { mShowTimeTo = !mShowTimeTo; mActionShowTimeTo->setChecked(mShowTimeTo); if (!mShowTimeTo && !mShowTime) slotShowTime(); // at least one time column must be displayed else { mListView->selectTimeColumns(mShowTime, mShowTimeTo); KConfigGroup config(KSharedConfig::openConfig(), VIEW_GROUP); config.writeEntry(SHOW_TIME_KEY, mShowTime); config.writeEntry(SHOW_TIME_TO_KEY, mShowTimeTo); } } /****************************************************************************** * Called when the Show Archived Alarms menu item is selected or deselected. */ void MainWindow::slotShowArchived() { mShowArchived = !mShowArchived; mActionShowArchived->setChecked(mShowArchived); mActionShowArchived->setToolTip(mShowArchived ? i18nc("@info:tooltip", "Hide Archived Alarms") : i18nc("@info:tooltip", "Show Archived Alarms")); mListFilterModel->setEventTypeFilter(mShowArchived ? CalEvent::ACTIVE | CalEvent::ARCHIVED : CalEvent::ACTIVE); mListView->reset(); KConfigGroup config(KSharedConfig::openConfig(), VIEW_GROUP); config.writeEntry(SHOW_ARCHIVED_KEY, mShowArchived); } /****************************************************************************** * Called when the Spread Windows global shortcut is selected, to spread alarm * windows so that they are all visible. */ void MainWindow::slotSpreadWindowsShortcut() { mActionSpreadWindows->trigger(); } /****************************************************************************** * Called when the Wake From Suspend menu option is selected. */ void MainWindow::slotWakeFromSuspend() { (WakeFromSuspendDlg::create(this))->show(); } /****************************************************************************** * Called when the Import Alarms menu item is selected, to merge alarms from an * external calendar into the current calendars. */ void MainWindow::slotImportAlarms() { AlarmCalendar::importAlarms(this); } /****************************************************************************** * Called when the Export Alarms menu item is selected, to export the selected * alarms to an external calendar. */ void MainWindow::slotExportAlarms() { QVector events = mListView->selectedEvents(); if (!events.isEmpty()) { KAEvent::List evts = KAEvent::ptrList(events); AlarmCalendar::exportAlarms(evts, this); } } /****************************************************************************** * Called when the Import Birthdays menu item is selected, to display birthdays * from the address book for selection as alarms. */ void MainWindow::slotBirthdays() { // Use AutoQPointer to guard against crash on application exit while // the dialogue is still open. It prevents double deletion (both on // deletion of MainWindow, and on return from this function). AutoQPointer dlg = new BirthdayDlg(this); if (dlg->exec() == QDialog::Accepted) { QVector events = dlg->events(); if (!events.isEmpty()) { mListView->clearSelection(); // Add alarm to the displayed lists and to the calendar file KAlarm::UpdateResult status = KAlarm::addEvents(events, dlg, true, true); Undo::EventList undos; AlarmCalendar* resources = AlarmCalendar::resources(); for (int i = 0, end = events.count(); i < end; ++i) { Akonadi::Collection c = resources->collectionForEvent(events[i].itemId()); undos.append(events[i], c); } Undo::saveAdds(undos, i18nc("@info", "Import birthdays")); if (status != KAlarm::UPDATE_FAILED) KAlarm::outputAlarmWarnings(dlg); } } } /****************************************************************************** * Called when the Templates menu item is selected, to display the alarm * template editing dialog. */ void MainWindow::slotTemplates() { if (!mTemplateDlg) { mTemplateDlg = TemplateDlg::create(this); enableTemplateMenuItem(false); // disable menu item in all windows connect(mTemplateDlg, &QDialog::finished, this, &MainWindow::slotTemplatesEnd); mTemplateDlg->show(); } } /****************************************************************************** * Called when the alarm template editing dialog has exited. */ void MainWindow::slotTemplatesEnd() { if (mTemplateDlg) { mTemplateDlg->deleteLater(); // this deletes the dialog once it is safe to do so mTemplateDlg = nullptr; enableTemplateMenuItem(true); // re-enable menu item in all windows } } /****************************************************************************** * Called when the Display System Tray Icon menu item is selected. */ void MainWindow::slotToggleTrayIcon() { theApp()->displayTrayIcon(!theApp()->trayIconDisplayed(), this); } /****************************************************************************** * Called when the Show Resource Selector menu item is selected. */ void MainWindow::slotToggleResourceSelector() { mShowResources = mActionToggleResourceSel->isChecked(); if (mShowResources) { if (mResourcesWidth <= 0) { mResourcesWidth = mResourceSelector->sizeHint().width(); mResourceSelector->resize(mResourcesWidth, mResourceSelector->height()); QList widths = mSplitter->sizes(); if (widths.count() == 1) { int listwidth = widths[0] - mSplitter->handleWidth() - mResourcesWidth; mListView->resize(listwidth, mListView->height()); widths.append(listwidth); widths[0] = mResourcesWidth; } mSplitter->setSizes(widths); } mResourceSelector->show(); } else mResourceSelector->hide(); KConfigGroup config(KSharedConfig::openConfig(), VIEW_GROUP); config.writeEntry(SHOW_RESOURCES_KEY, mShowResources); } /****************************************************************************** * Called when an error occurs in the resource calendar, to display a message. */ void MainWindow::showErrorMessage(const QString& msg) { KAMessageBox::error(this, msg); } /****************************************************************************** * Called when the system tray icon is created or destroyed. * Set the system tray icon menu text according to whether or not the system * tray icon is currently visible. */ void MainWindow::updateTrayIconAction() { mActionToggleTrayIcon->setEnabled(QSystemTrayIcon::isSystemTrayAvailable()); mActionToggleTrayIcon->setChecked(theApp()->trayIconDisplayed()); } /****************************************************************************** * Called when the active status of Find changes. */ void MainWindow::slotFindActive(bool active) { mActionFindNext->setEnabled(active); mActionFindPrev->setEnabled(active); } /****************************************************************************** * Called when the Undo action is selected. */ void MainWindow::slotUndo() { Undo::undo(this, KLocalizedString::removeAcceleratorMarker(mActionUndo->text())); } /****************************************************************************** * Called when the Redo action is selected. */ void MainWindow::slotRedo() { Undo::redo(this, KLocalizedString::removeAcceleratorMarker(mActionRedo->text())); } /****************************************************************************** * Called when an Undo item is selected. */ void MainWindow::slotUndoItem(QAction* action) { int id = mUndoMenuIds[action]; Undo::undo(id, this, Undo::actionText(Undo::UNDO, id)); } /****************************************************************************** * Called when a Redo item is selected. */ void MainWindow::slotRedoItem(QAction* action) { int id = mUndoMenuIds[action]; Undo::redo(id, this, Undo::actionText(Undo::REDO, id)); } /****************************************************************************** * Called when the Undo menu is about to show. * Populates the menu. */ void MainWindow::slotInitUndoMenu() { initUndoMenu(mActionUndo->menu(), Undo::UNDO); } /****************************************************************************** * Called when the Redo menu is about to show. * Populates the menu. */ void MainWindow::slotInitRedoMenu() { initUndoMenu(mActionRedo->menu(), Undo::REDO); } /****************************************************************************** * Populate the undo or redo menu. */ void MainWindow::initUndoMenu(QMenu* menu, Undo::Type type) { menu->clear(); mUndoMenuIds.clear(); const QString& action = (type == Undo::UNDO) ? undoTextStripped : redoTextStripped; QList ids = Undo::ids(type); for (int i = 0, end = ids.count(); i < end; ++i) { int id = ids[i]; QString actText = Undo::actionText(type, id); QString descrip = Undo::description(type, id); QString text = descrip.isEmpty() ? i18nc("@action Undo/Redo [action]", "%1 %2", action, actText) : i18nc("@action Undo [action]: message", "%1 %2: %3", action, actText, descrip); QAction* act = menu->addAction(text); mUndoMenuIds[act] = id; } } /****************************************************************************** * Called when the status of the Undo or Redo list changes. * Change the Undo or Redo text to include the action which would be undone/redone. */ void MainWindow::slotUndoStatus(const QString& undo, const QString& redo) { if (undo.isNull()) { mActionUndo->setEnabled(false); mActionUndo->setText(undoText); } else { mActionUndo->setEnabled(true); mActionUndo->setText(QStringLiteral("%1 %2").arg(undoText, undo)); } if (redo.isNull()) { mActionRedo->setEnabled(false); mActionRedo->setText(redoText); } else { mActionRedo->setEnabled(true); mActionRedo->setText(QStringLiteral("%1 %2").arg(redoText, redo)); } } /****************************************************************************** * Called when the Refresh Alarms menu item is selected. */ void MainWindow::slotRefreshAlarms() { KAlarm::refreshAlarms(); } /****************************************************************************** * Called when the "Configure KAlarm" menu item is selected. */ void MainWindow::slotPreferences() { KAlarmPrefDlg::display(); } /****************************************************************************** * Called when the Configure Keys menu item is selected. */ void MainWindow::slotConfigureKeys() { KShortcutsDialog::configure(actionCollection(), KShortcutsEditor::LetterShortcutsAllowed, this); } /****************************************************************************** * Called when the Configure Toolbars menu item is selected. */ void MainWindow::slotConfigureToolbar() { KConfigGroup grp(KSharedConfig::openConfig()->group(WINDOW_NAME)); saveMainWindowSettings(grp); KEditToolBar dlg(factory()); connect(&dlg, &KEditToolBar::newToolBarConfig, this, &MainWindow::slotNewToolbarConfig); dlg.exec(); } /****************************************************************************** * Called when OK or Apply is clicked in the Configure Toolbars dialog, to save * the new configuration. */ void MainWindow::slotNewToolbarConfig() { createGUI(UI_FILE); applyMainWindowSettings(KSharedConfig::openConfig()->group(WINDOW_NAME)); } /****************************************************************************** * Called when the Quit menu item is selected. * Note that this must be called by the event loop, not directly from the menu * item, since otherwise the window will be deleted while still processing the * menu, resulting in a crash. */ void MainWindow::slotQuit() { theApp()->doQuit(this); } /****************************************************************************** * Called when the user or the session manager attempts to close the window. */ void MainWindow::closeEvent(QCloseEvent* ce) { if (!qApp->isSavingSession()) { // The user (not the session manager) wants to close the window. if (isTrayParent()) { // It's the parent window of the system tray icon, so just hide // it to prevent the system tray icon closing. hide(); theApp()->quitIf(); ce->ignore(); return; } } ce->accept(); } /****************************************************************************** * Called when the drag cursor enters a main or system tray window, to accept * or reject the dragged object. */ void MainWindow::executeDragEnterEvent(QDragEnterEvent* e) { const QMimeData* data = e->mimeData(); bool accept = ICalDrag::canDecode(data) ? !e->source() // don't accept "text/calendar" objects from this application : data->hasText() || data->hasUrls() || KPIM::MailList::canDecode(data); if (accept) e->acceptProposedAction(); } /****************************************************************************** * Called when an object is dropped on the window. * If the object is recognised, the edit alarm dialog is opened appropriately. */ void MainWindow::dropEvent(QDropEvent* e) { executeDropEvent(this, e); } static QString getMailHeader(const char* header, KMime::Content& content) { KMime::Headers::Base* hd = content.headerByType(header); return hd ? hd->asUnicodeString() : QString(); } /****************************************************************************** * Called when an object is dropped on a main or system tray window, to * evaluate the action required and extract the text. */ void MainWindow::executeDropEvent(MainWindow* win, QDropEvent* e) { - qCDebug(KALARM_LOG) << "Formats:" << e->mimeData()->formats(); + qCDebug(KALARM_LOG) << "MainWindow::executeDropEvent: Formats:" << e->mimeData()->formats(); const QMimeData* data = e->mimeData(); KAEvent::SubAction action = KAEvent::MESSAGE; QByteArray bytes; AlarmText alarmText; KPIM::MailList mailList; QList files; MemoryCalendar::Ptr calendar(new MemoryCalendar(Preferences::timeSpecAsZone())); #ifndef NDEBUG QString fmts = data->formats().join(QStringLiteral(", ")); - qCDebug(KALARM_LOG) << fmts; + qCDebug(KALARM_LOG) << "MainWindow::executeDropEvent:" << fmts; #endif /* The order of the tests below matters, since some dropped objects * provide more than one mime type. * Don't change them without careful thought !! */ if (!(bytes = data->data(QStringLiteral("message/rfc822"))).isEmpty()) { // Email message(s). Ignore all but the first. - qCDebug(KALARM_LOG) << "email"; + qCDebug(KALARM_LOG) << "MainWindow::executeDropEvent: email"; KMime::Content content; content.setContent(bytes); content.parse(); QString body; if (content.textContent()) body = content.textContent()->decodedText(true, true); // strip trailing newlines & spaces unsigned long sernum = 0; if (KPIM::MailList::canDecode(data)) { // Get its KMail serial number to allow the KMail message // to be called up from the alarm message window. mailList = KPIM::MailList::fromMimeData(data); if (!mailList.isEmpty()) sernum = mailList[0].serialNumber(); } alarmText.setEmail(getMailHeader("To", content), getMailHeader("From", content), getMailHeader("Cc", content), getMailHeader("Date", content), getMailHeader("Subject", content), body, sernum); } else if (KPIM::MailList::canDecode(data)) { mailList = KPIM::MailList::fromMimeData(data); // KMail message(s). Ignore all but the first. - qCDebug(KALARM_LOG) << "KMail_list"; + qCDebug(KALARM_LOG) << "MainWindow::executeDropEvent: KMail_list"; if (mailList.isEmpty()) return; KPIM::MailSummary& summary = mailList[0]; QDateTime dt; dt.setTime_t(summary.date()); QString body = KAMail::getMailBody(summary.serialNumber()); alarmText.setEmail(summary.to(), summary.from(), QString(), QLocale().toString(dt), summary.subject(), body, summary.serialNumber()); } else if (ICalDrag::fromMimeData(data, calendar)) { // iCalendar - If events are included, use the first event - qCDebug(KALARM_LOG) << "iCalendar"; + qCDebug(KALARM_LOG) << "MainWindow::executeDropEvent: iCalendar"; Event::List events = calendar->rawEvents(); if (!events.isEmpty()) { KAEvent ev(events[0]); KAlarm::editNewAlarm(&ev, win); return; } // If todos are included, use the first todo Todo::List todos = calendar->rawTodos(); if (todos.isEmpty()) return; Todo::Ptr todo = todos[0]; alarmText.setTodo(todo); KADateTime start(todo->dtStart(true)); if (!start.isValid() && todo->hasDueDate()) start = KADateTime(todo->dtDue(true)); if (todo->allDay()) start.setDateOnly(true); KAEvent::Flags flags = KAEvent::DEFAULT_FONT; if (start.isDateOnly()) flags |= KAEvent::ANY_TIME; KAEvent ev(start, alarmText.displayText(), Preferences::defaultBgColour(), Preferences::defaultFgColour(), QFont(), KAEvent::MESSAGE, 0, flags, true); if (todo->recurs()) { ev.setRecurrence(*todo->recurrence()); ev.setNextOccurrence(KADateTime::currentUtcDateTime()); } ev.endChanges(); KAlarm::editNewAlarm(&ev, win); return; } else if (!(files = data->urls()).isEmpty()) { - qCDebug(KALARM_LOG) << "URL"; + qCDebug(KALARM_LOG) << "MainWindow::executeDropEvent: URL"; // Try to find the mime type of the file, without downloading a remote file QMimeDatabase mimeDb; const QString mimeTypeName = mimeDb.mimeTypeForUrl(files[0]).name(); action = mimeTypeName.startsWith(QStringLiteral("audio/")) ? KAEvent::AUDIO : KAEvent::FILE; alarmText.setText(files[0].toDisplayString()); } else if (data->hasText()) { QString text = data->text(); - qCDebug(KALARM_LOG) << "text"; + qCDebug(KALARM_LOG) << "MainWindow::executeDropEvent: text"; alarmText.setText(text); } else return; if (!alarmText.isEmpty()) { if (action == KAEvent::MESSAGE && (alarmText.isEmail() || alarmText.isScript())) { // If the alarm text could be interpreted as an email or command script, // prompt for which type of alarm to create. QStringList types; types += i18nc("@item:inlistbox", "Display Alarm"); if (alarmText.isEmail()) types += i18nc("@item:inlistbox", "Email Alarm"); else if (alarmText.isScript()) types += i18nc("@item:inlistbox", "Command Alarm"); bool ok = false; QString type = QInputDialog::getItem(mainMainWindow(), i18nc("@title:window", "Alarm Type"), i18nc("@info", "Choose alarm type to create:"), types, 0, false, &ok); if (!ok) return; // user didn't press OK int i = types.indexOf(type); if (i == 1) action = alarmText.isEmail() ? KAEvent::EMAIL : KAEvent::COMMAND; } KAlarm::editNewAlarm(action, win, &alarmText); } } /****************************************************************************** * Called when the status of a calendar has changed. * Enable or disable actions appropriately. */ void MainWindow::slotCalendarStatusChanged() { // Find whether there are any writable calendars bool active = !CollectionControlModel::enabledCollections(CalEvent::ACTIVE, true).isEmpty(); bool templat = !CollectionControlModel::enabledCollections(CalEvent::TEMPLATE, true).isEmpty(); for (int i = 0, end = mWindowList.count(); i < end; ++i) { MainWindow* w = mWindowList[i]; w->mActionImportAlarms->setEnabled(active || templat); w->mActionImportBirthdays->setEnabled(active); w->mActionCreateTemplate->setEnabled(templat); // Note: w->mActionNew enabled status is set in the NewAlarmAction class. w->slotSelection(); } } /****************************************************************************** * Called when the selected items in the ListView change. * Enables the actions appropriately. */ void MainWindow::slotSelection() { // Find which events have been selected QVector events = mListView->selectedEvents(); int count = events.count(); if (!count) { selectionCleared(); // disable actions Q_EMIT selectionChanged(); return; } // Find whether there are any writable resources bool active = mActionNew->isEnabled(); bool readOnly = false; bool allArchived = true; bool enableReactivate = true; bool enableEnableDisable = true; bool enableEnable = false; bool enableDisable = false; AlarmCalendar* resources = AlarmCalendar::resources(); const KADateTime now = KADateTime::currentUtcDateTime(); for (int i = 0; i < count; ++i) { KAEvent* ev = resources->event(EventId(events.at(i))); // get up-to-date status KAEvent* event = ev ? ev : &events[i]; bool expired = event->expired(); if (!expired) allArchived = false; if (resources->eventReadOnly(event->itemId())) readOnly = true; if (enableReactivate && (!expired || !event->occursAfter(now, true))) enableReactivate = false; if (enableEnableDisable) { if (expired) enableEnableDisable = enableEnable = enableDisable = false; else { if (!enableEnable && !event->enabled()) enableEnable = true; if (!enableDisable && event->enabled()) enableDisable = true; } } } - qCDebug(KALARM_LOG) << "true"; + qCDebug(KALARM_LOG) << "MainWindow::slotSelection: true"; mActionCreateTemplate->setEnabled((count == 1) && !CollectionControlModel::enabledCollections(CalEvent::TEMPLATE, true).isEmpty()); mActionExportAlarms->setEnabled(true); mActionExport->setEnabled(true); mActionCopy->setEnabled(active && count == 1); mActionModify->setEnabled(count == 1); mActionDelete->setEnabled(!readOnly && (active || allArchived)); mActionReactivate->setEnabled(active && enableReactivate); mActionEnable->setEnabled(active && !readOnly && (enableEnable || enableDisable)); if (enableEnable || enableDisable) setEnableText(enableEnable); Q_EMIT selectionChanged(); } /****************************************************************************** * Called when a context menu is requested in the ListView. * Displays a context menu to modify or delete the selected item. */ void MainWindow::slotContextMenuRequested(const QPoint& globalPos) { - qCDebug(KALARM_LOG); + qCDebug(KALARM_LOG) << "MainWindow::slotContextMenuRequested"; if (mContextMenu) mContextMenu->popup(globalPos); } /****************************************************************************** * Disables actions when no item is selected. */ void MainWindow::selectionCleared() { mActionCreateTemplate->setEnabled(false); mActionExportAlarms->setEnabled(false); mActionExport->setEnabled(false); mActionCopy->setEnabled(false); mActionModify->setEnabled(false); mActionDelete->setEnabled(false); mActionReactivate->setEnabled(false); mActionEnable->setEnabled(false); } /****************************************************************************** * Set the text of the Enable/Disable menu action. */ void MainWindow::setEnableText(bool enable) { mActionEnableEnable = enable; mActionEnable->setText(enable ? i18nc("@action", "Ena&ble") : i18nc("@action", "Disa&ble")); } /****************************************************************************** * Display or hide the specified main window. * This should only be called when the application doesn't run in the system tray. */ MainWindow* MainWindow::toggleWindow(MainWindow* win) { if (win && mWindowList.indexOf(win) != -1) { // A window is specified (and it exists) if (win->isVisible()) { // The window is visible, so close it win->close(); return nullptr; } else { // The window is hidden, so display it win->hide(); // in case it's on a different desktop win->setWindowState(win->windowState() & ~Qt::WindowMinimized); win->raise(); win->activateWindow(); return win; } } // No window is specified, or the window doesn't exist. Open a new one. win = create(); win->show(); return win; } /****************************************************************************** * Called when the Edit button is clicked in an alarm message window. * This controls the alarm edit dialog created by the alarm window, and allows * it to remain unaffected by the alarm window closing. * See MessageWin::slotEdit() for more information. */ void MainWindow::editAlarm(EditAlarmDlg* dlg, const KAEvent& event) { mEditAlarmMap[dlg] = event; connect(dlg, &KEditToolBar::accepted, this, &MainWindow::editAlarmOk); connect(dlg, &KEditToolBar::destroyed, this, &MainWindow::editAlarmDeleted); dlg->setAttribute(Qt::WA_DeleteOnClose, true); // ensure no memory leaks dlg->show(); } /****************************************************************************** * Called when OK is clicked in the alarm edit dialog shown by editAlarm(). * Updates the event which has been edited. */ void MainWindow::editAlarmOk() { EditAlarmDlg* dlg = qobject_cast(sender()); if (!dlg) return; QMap::Iterator it = mEditAlarmMap.find(dlg); if (it == mEditAlarmMap.end()) return; KAEvent event = it.value(); mEditAlarmMap.erase(it); if (!event.isValid()) return; if (dlg->result() != QDialog::Accepted) return; Akonadi::Collection c = AkonadiModel::instance()->collection(event); KAlarm::updateEditedAlarm(dlg, event, c); } /****************************************************************************** * Called when the alarm edit dialog shown by editAlarm() is deleted. * Removes the dialog from the pending list. */ void MainWindow::editAlarmDeleted(QObject* obj) { mEditAlarmMap.remove(static_cast(obj)); } // vim: et sw=4: diff --git a/src/messagewin.cpp b/src/messagewin.cpp index e02290c8..a31b8488 100644 --- a/src/messagewin.cpp +++ b/src/messagewin.cpp @@ -1,2435 +1,2435 @@ /* * messagewin.cpp - displays an alarm message * Program: kalarm * Copyright © 2001-2019 by David Jarvie * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "config-kalarm.h" #include "kalarm.h" #include "messagewin_p.h" #include "messagewin.h" #include "alarmcalendar.h" #include "autoqpointer.h" #include "collectionmodel.h" #include "deferdlg.h" #include "desktop.h" #include "editdlg.h" #include "functions.h" #include "kalarmapp.h" #include "mainwindow.h" #include "messagebox.h" #include "preferences.h" #include "pushbutton.h" #include "shellprocess.h" #include "synchtimer.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if KDEPIM_HAVE_X11 #include #include #include #endif #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 using namespace KCalCore; using namespace KAlarmCal; #if KDEPIM_HAVE_X11 enum FullScreenType { NoFullScreen = 0, FullScreen = 1, FullScreenActive = 2 }; static FullScreenType haveFullScreenWindow(int screen); static FullScreenType findFullScreenWindows(const QVector& screenRects, QVector& screenTypes); #endif #include "kmailinterface.h" static const QLatin1String KMAIL_DBUS_SERVICE("org.kde.kmail"); static const QLatin1String KMAIL_DBUS_PATH("/KMail"); // The delay for enabling message window buttons if a zero delay is // configured, i.e. the windows are placed far from the cursor. static const int proximityButtonDelay = 1000; // (milliseconds) static const int proximityMultiple = 10; // multiple of button height distance from cursor for proximity // A text label widget which can be scrolled and copied with the mouse class MessageText : public KTextEdit { public: MessageText(QWidget* parent = nullptr) : KTextEdit(parent), mNewLine(false) { setReadOnly(true); setFrameStyle(NoFrame); setLineWrapMode(NoWrap); } int scrollBarHeight() const { return horizontalScrollBar()->height(); } int scrollBarWidth() const { return verticalScrollBar()->width(); } void setBackgroundColour(const QColor& c) { QPalette pal = viewport()->palette(); pal.setColor(viewport()->backgroundRole(), c); viewport()->setPalette(pal); } QSize sizeHint() const override { const QSizeF docsize = document()->size(); return QSize(static_cast(docsize.width() + 0.99) + verticalScrollBar()->width(), static_cast(docsize.height() + 0.99) + horizontalScrollBar()->height()); } bool newLine() const { return mNewLine; } void setNewLine(bool nl) { mNewLine = nl; } private: bool mNewLine; }; // Basic flags for the window static const Qt::WindowFlags WFLAGS = Qt::WindowStaysOnTopHint; static const Qt::WindowFlags WFLAGS2 = Qt::WindowContextHelpButtonHint; static const Qt::WidgetAttribute WidgetFlags = Qt::WA_DeleteOnClose; // Error message bit masks enum { ErrMsg_Speak = 0x01, ErrMsg_AudioFile = 0x02 }; QList MessageWin::mWindowList; QMap MessageWin::mErrorMessages; bool MessageWin::mRedisplayed = false; // There can only be one audio thread at a time: trying to play multiple // sound files simultaneously would result in a cacophony, and besides // that, Phonon currently crashes... QPointer MessageWin::mAudioThread; MessageWin* AudioThread::mAudioOwner = nullptr; /****************************************************************************** * Construct the message window for the specified alarm. * Other alarms in the supplied event may have been updated by the caller, so * the whole event needs to be stored for updating the calendar file when it is * displayed. */ MessageWin::MessageWin(const KAEvent* event, const KAAlarm& alarm, int flags) : MainWindowBase(nullptr, static_cast(WFLAGS | WFLAGS2 | ((flags & ALWAYS_HIDE) || getWorkAreaAndModal() ? Qt::WindowType(0) : Qt::X11BypassWindowManagerHint))), mMessage(event->cleanText()), mFont(event->font()), mBgColour(event->bgColour()), mFgColour(event->fgColour()), mEventItemId(event->itemId()), mEventId(*event), mAudioFile(event->audioFile()), mVolume(event->soundVolume()), mFadeVolume(event->fadeVolume()), mFadeSeconds(qMin(event->fadeSeconds(), 86400)), mDefaultDeferMinutes(event->deferDefaultMinutes()), mAlarmType(alarm.type()), mAction(event->actionSubType()), mKMailSerialNumber(event->kmailSerialNumber()), mCommandError(event->commandError()), mRestoreHeight(0), mAudioRepeatPause(event->repeatSoundPause()), mConfirmAck(event->confirmAck()), mNoDefer(true), mInvalid(false), mEvent(*event), mOriginalEvent(*event), mCollection(AlarmCalendar::resources()->collectionForEvent(mEventItemId)), mTimeLabel(nullptr), mRemainingText(nullptr), mEditButton(nullptr), mDeferButton(nullptr), mSilenceButton(nullptr), mKMailButton(nullptr), mCommandText(nullptr), mDontShowAgainCheck(nullptr), mEditDlg(nullptr), mDeferDlg(nullptr), mAlwaysHide(flags & ALWAYS_HIDE), mErrorWindow(false), mInitialised(false), mNoPostAction(alarm.type() & KAAlarm::REMINDER_ALARM), mRecreating(false), mBeep(event->beep()), mSpeak(event->speak()), mRescheduleEvent(!(flags & NO_RESCHEDULE)), mShown(false), mPositioning(false), mNoCloseConfirm(false), mDisableDeferral(false) { - qCDebug(KALARM_LOG) << (void*)this << "event" << mEventId; + qCDebug(KALARM_LOG) << "MessageWin:" << (void*)this << "event" << mEventId; setAttribute(static_cast(WidgetFlags)); setWindowModality(Qt::WindowModal); setObjectName(QStringLiteral("MessageWin")); // used by LikeBack if (alarm.type() & KAAlarm::REMINDER_ALARM) { if (event->reminderMinutes() < 0) { event->previousOccurrence(alarm.dateTime(false).effectiveKDateTime(), mDateTime, false); if (!mDateTime.isValid() && event->repeatAtLogin()) mDateTime = alarm.dateTime().addSecs(event->reminderMinutes() * 60); } else mDateTime = event->mainDateTime(true); } else mDateTime = alarm.dateTime(true); if (!(flags & (NO_INIT_VIEW | ALWAYS_HIDE))) { const bool readonly = AlarmCalendar::resources()->eventReadOnly(mEventItemId); mShowEdit = !mEventId.isEmpty() && !readonly; mNoDefer = readonly || (flags & NO_DEFER) || alarm.repeatAtLogin(); initView(); } // Set to save settings automatically, but don't save window size. // File alarm window size is saved elsewhere. setAutoSaveSettings(QStringLiteral("MessageWin"), false); mWindowList.append(this); if (event->autoClose()) mCloseTime = alarm.dateTime().effectiveKDateTime().toUtc().qDateTime().addSecs(event->lateCancel() * 60); if (mAlwaysHide) { hide(); displayComplete(); // play audio, etc. } } /****************************************************************************** * Display an error message window. * If 'dontShowAgain' is non-null, a "Don't show again" option is displayed. Note * that the option is specific to 'event'. */ void MessageWin::showError(const KAEvent& event, const DateTime& alarmDateTime, const QStringList& errmsgs, const QString& dontShowAgain) { if (!dontShowAgain.isEmpty() && KAlarm::dontShowErrors(EventId(event), dontShowAgain)) return; // Don't pile up duplicate error messages for the same alarm for (int i = 0, end = mWindowList.count(); i < end; ++i) { const MessageWin* w = mWindowList[i]; if (w->mErrorWindow && w->mEventId == EventId(event) && w->mErrorMsgs == errmsgs && w->mDontShowAgain == dontShowAgain) return; } (new MessageWin(&event, alarmDateTime, errmsgs, dontShowAgain))->show(); } /****************************************************************************** * Construct the message window for a specified error message. * If 'dontShowAgain' is non-null, a "Don't show again" option is displayed. Note * that the option is specific to 'event'. */ MessageWin::MessageWin(const KAEvent* event, const DateTime& alarmDateTime, const QStringList& errmsgs, const QString& dontShowAgain) : MainWindowBase(nullptr, WFLAGS | WFLAGS2), mMessage(event->cleanText()), mDateTime(alarmDateTime), mEventItemId(event->itemId()), mEventId(*event), mAlarmType(KAAlarm::MAIN_ALARM), mAction(event->actionSubType()), mKMailSerialNumber(0), mCommandError(KAEvent::CMD_NO_ERROR), mErrorMsgs(errmsgs), mDontShowAgain(dontShowAgain), mRestoreHeight(0), mConfirmAck(false), mShowEdit(false), mNoDefer(true), mInvalid(false), mEvent(*event), mOriginalEvent(*event), mTimeLabel(nullptr), mRemainingText(nullptr), mEditButton(nullptr), mDeferButton(nullptr), mSilenceButton(nullptr), mKMailButton(nullptr), mCommandText(nullptr), mDontShowAgainCheck(nullptr), mEditDlg(nullptr), mDeferDlg(nullptr), mAlwaysHide(false), mErrorWindow(true), mInitialised(false), mNoPostAction(true), mRecreating(false), mRescheduleEvent(false), mShown(false), mPositioning(false), mNoCloseConfirm(false), mDisableDeferral(false) { - qCDebug(KALARM_LOG) << "errmsg"; + qCDebug(KALARM_LOG) << "MessageWin: errmsg"; setAttribute(static_cast(WidgetFlags)); setWindowModality(Qt::WindowModal); setObjectName(QStringLiteral("ErrorWin")); // used by LikeBack getWorkAreaAndModal(); initView(); mWindowList.append(this); } /****************************************************************************** * Construct the message window for restoration by session management. * The window is initialised by readProperties(). */ MessageWin::MessageWin() : MainWindowBase(nullptr, WFLAGS), mTimeLabel(nullptr), mRemainingText(nullptr), mEditButton(nullptr), mDeferButton(nullptr), mSilenceButton(nullptr), mKMailButton(nullptr), mCommandText(nullptr), mDontShowAgainCheck(nullptr), mEditDlg(nullptr), mDeferDlg(nullptr), mAlwaysHide(false), mErrorWindow(false), mInitialised(false), mRecreating(false), mRescheduleEvent(false), mShown(false), mPositioning(false), mNoCloseConfirm(false), mDisableDeferral(false) { - qCDebug(KALARM_LOG) << (void*)this << "restore"; + qCDebug(KALARM_LOG) << "MessageWin:" << (void*)this << "restore"; setAttribute(WidgetFlags); setWindowModality(Qt::WindowModal); setObjectName(QStringLiteral("RestoredMsgWin")); // used by LikeBack getWorkAreaAndModal(); mWindowList.append(this); } /****************************************************************************** * Destructor. Perform any post-alarm actions before tidying up. */ MessageWin::~MessageWin() { - qCDebug(KALARM_LOG) << (void*)this << mEventId; + qCDebug(KALARM_LOG) << "~MessageWin" << (void*)this << mEventId; if (AudioThread::mAudioOwner == this && !mAudioThread.isNull()) mAudioThread->quit(); mErrorMessages.remove(mEventId); mWindowList.removeAll(this); if (!mRecreating) { if (!mNoPostAction && !mEvent.postAction().isEmpty()) theApp()->alarmCompleted(mEvent); if (!instanceCount(true)) theApp()->quitIf(); // no visible windows remain - check whether to quit } } /****************************************************************************** * Construct the message window. */ void MessageWin::initView() { const bool reminder = (!mErrorWindow && (mAlarmType & KAAlarm::REMINDER_ALARM)); const int leading = fontMetrics().leading(); setCaption((mAlarmType & KAAlarm::REMINDER_ALARM) ? i18nc("@title:window", "Reminder") : i18nc("@title:window", "Message")); QWidget* topWidget = new QWidget(this); setCentralWidget(topWidget); QVBoxLayout* topLayout = new QVBoxLayout(topWidget); topLayout->setMargin(style()->pixelMetric(QStyle::PM_DefaultChildMargin)); topLayout->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); QPalette labelPalette = palette(); labelPalette.setColor(backgroundRole(), labelPalette.color(QPalette::Window)); // Show the alarm date/time, together with a reminder text where appropriate. // Alarm date/time: display time zone if not local time zone. mTimeLabel = new QLabel(topWidget); mTimeLabel->setText(dateTimeToDisplay()); mTimeLabel->setFrameStyle(QFrame::StyledPanel); mTimeLabel->setPalette(labelPalette); mTimeLabel->setAutoFillBackground(true); topLayout->addWidget(mTimeLabel, 0, Qt::AlignHCenter); mTimeLabel->setWhatsThis(i18nc("@info:whatsthis", "The scheduled date/time for the message (as opposed to the actual time of display).")); if (mDateTime.isValid()) { // Reminder if (reminder) { // Create a label "time\nReminder" by inserting the time at the // start of the translated string, allowing for possible HTML tags // enclosing "Reminder". QString s = i18nc("@info", "Reminder"); QRegExp re(QStringLiteral("^(<[^>]+>)*")); re.indexIn(s); s.insert(re.matchedLength(), mTimeLabel->text() + QLatin1String("
")); mTimeLabel->setText(s); mTimeLabel->setAlignment(Qt::AlignHCenter); } } else mTimeLabel->hide(); if (!mErrorWindow) { // It's a normal alarm message window switch (mAction) { case KAEvent::FILE: { // Display the file name KSqueezedTextLabel* label = new KSqueezedTextLabel(mMessage, topWidget); label->setFrameStyle(QFrame::StyledPanel); label->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard); label->setPalette(labelPalette); label->setAutoFillBackground(true); label->setWhatsThis(i18nc("@info:whatsthis", "The file whose contents are displayed below")); topLayout->addWidget(label, 0, Qt::AlignHCenter); // Display contents of file const QUrl url = QUrl::fromUserInput(mMessage, QString(), QUrl::AssumeLocalFile); auto statJob = KIO::stat(url, KIO::StatJob::SourceSide, 0, KIO::HideProgressInfo); const bool exists = statJob->exec(); const bool isDir = statJob->statResult().isDir(); bool opened = false; if (exists && !isDir) { auto job = KIO::storedGet(url); KJobWidgets::setWindow(job, MainWindow::mainMainWindow()); if (job->exec()) { opened = true; const QByteArray data = job->data(); QTemporaryFile tmpFile; tmpFile.write(data); tmpFile.seek(0); QTextBrowser* view = new QTextBrowser(topWidget); view->setFrameStyle(QFrame::NoFrame); view->setWordWrapMode(QTextOption::NoWrap); QPalette pal = view->viewport()->palette(); pal.setColor(view->viewport()->backgroundRole(), mBgColour); view->viewport()->setPalette(pal); view->setTextColor(mFgColour); view->setCurrentFont(mFont); QMimeDatabase db; QMimeType mime = db.mimeTypeForUrl(url); if (mime.name() == QLatin1String("application/octet-stream")) mime = db.mimeTypeForData(&tmpFile); switch (KAlarm::fileType(mime)) { case KAlarm::Image: view->setHtml(QLatin1String("")); break; case KAlarm::TextFormatted: view->QTextBrowser::setSource(QUrl::fromLocalFile(tmpFile.fileName())); //krazy:exclude=qclasses break; default: { view->setPlainText(QString::fromUtf8(data)); break; } } view->setMinimumSize(view->sizeHint()); topLayout->addWidget(view); // Set the default size to 20 lines square. // Note that after the first file has been displayed, this size // is overridden by the user-set default stored in the config file. // So there is no need to calculate an accurate size. int h = 20*view->fontMetrics().lineSpacing() + 2*view->frameWidth(); view->resize(QSize(h, h).expandedTo(view->sizeHint())); view->setWhatsThis(i18nc("@info:whatsthis", "The contents of the file to be displayed")); } } if (!exists || isDir || !opened) { mErrorMsgs += isDir ? i18nc("@info", "File is a folder") : exists ? i18nc("@info", "Failed to open file") : i18nc("@info", "File not found"); } break; } case KAEvent::MESSAGE: { // Message label // Using MessageText instead of QLabel allows scrolling and mouse copying MessageText* text = new MessageText(topWidget); text->setAutoFillBackground(true); text->setBackgroundColour(mBgColour); text->setTextColor(mFgColour); text->setCurrentFont(mFont); text->insertPlainText(mMessage); const int lineSpacing = text->fontMetrics().lineSpacing(); const QSize s = text->sizeHint(); const int h = s.height(); text->setMaximumHeight(h + text->scrollBarHeight()); text->setMinimumHeight(qMin(h, lineSpacing*4)); text->setMaximumWidth(s.width() + text->scrollBarWidth()); text->setWhatsThis(i18nc("@info:whatsthis", "The alarm message")); const int vspace = lineSpacing/2; const int hspace = lineSpacing - style()->pixelMetric(QStyle::PM_DefaultChildMargin); topLayout->addSpacing(vspace); topLayout->addStretch(); // Don't include any horizontal margins if message is 2/3 screen width if (text->sizeHint().width() >= KAlarm::desktopWorkArea(mScreenNumber).width()*2/3) topLayout->addWidget(text, 1, Qt::AlignHCenter); else { QHBoxLayout* layout = new QHBoxLayout(); layout->addSpacing(hspace); layout->addWidget(text, 1, Qt::AlignHCenter); layout->addSpacing(hspace); topLayout->addLayout(layout); } if (!reminder) topLayout->addStretch(); break; } case KAEvent::COMMAND: { mCommandText = new MessageText(topWidget); mCommandText->setBackgroundColour(mBgColour); mCommandText->setTextColor(mFgColour); mCommandText->setCurrentFont(mFont); topLayout->addWidget(mCommandText); mCommandText->setWhatsThis(i18nc("@info:whatsthis", "The output of the alarm's command")); theApp()->execCommandAlarm(mEvent, mEvent.alarm(mAlarmType), this, SLOT(readProcessOutput(ShellProcess*))); break; } case KAEvent::EMAIL: default: break; } if (reminder && mEvent.reminderMinutes() > 0) { // Advance reminder: show remaining time until the actual alarm mRemainingText = new QLabel(topWidget); mRemainingText->setFrameStyle(QFrame::Box | QFrame::Raised); mRemainingText->setMargin(leading); mRemainingText->setPalette(labelPalette); mRemainingText->setAutoFillBackground(true); if (mDateTime.isDateOnly() || KADateTime::currentLocalDate().daysTo(mDateTime.date()) > 0) { setRemainingTextDay(); MidnightTimer::connect(this, SLOT(setRemainingTextDay())); // update every day } else { setRemainingTextMinute(); MinuteTimer::connect(this, SLOT(setRemainingTextMinute())); // update every minute } topLayout->addWidget(mRemainingText, 0, Qt::AlignHCenter); topLayout->addSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); topLayout->addStretch(); } } else { // It's an error message switch (mAction) { case KAEvent::EMAIL: { // Display the email addresses and subject. QFrame* frame = new QFrame(topWidget); frame->setFrameStyle(QFrame::Box | QFrame::Raised); frame->setWhatsThis(i18nc("@info:whatsthis", "The email to send")); topLayout->addWidget(frame, 0, Qt::AlignHCenter); QGridLayout* grid = new QGridLayout(frame); grid->setMargin(style()->pixelMetric(QStyle::PM_DefaultChildMargin)); grid->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); QLabel* label = new QLabel(i18nc("@info Email addressee", "To:"), frame); label->setFixedSize(label->sizeHint()); grid->addWidget(label, 0, 0, Qt::AlignLeft); label = new QLabel(mEvent.emailAddresses(QStringLiteral("\n")), frame); label->setFixedSize(label->sizeHint()); grid->addWidget(label, 0, 1, Qt::AlignLeft); label = new QLabel(i18nc("@info Email subject", "Subject:"), frame); label->setFixedSize(label->sizeHint()); grid->addWidget(label, 1, 0, Qt::AlignLeft); label = new QLabel(mEvent.emailSubject(), frame); label->setFixedSize(label->sizeHint()); grid->addWidget(label, 1, 1, Qt::AlignLeft); break; } case KAEvent::COMMAND: case KAEvent::FILE: case KAEvent::MESSAGE: default: // Just display the error message strings break; } } if (!mErrorMsgs.count()) { topWidget->setAutoFillBackground(true); QPalette palette = topWidget->palette(); palette.setColor(topWidget->backgroundRole(), mBgColour); topWidget->setPalette(palette); } else { setCaption(i18nc("@title:window", "Error")); QHBoxLayout* layout = new QHBoxLayout(); layout->setMargin(2 * style()->pixelMetric(QStyle::PM_DefaultChildMargin)); layout->addStretch(); topLayout->addLayout(layout); QLabel* label = new QLabel(topWidget); label->setPixmap(QIcon::fromTheme(QStringLiteral("dialog-error")).pixmap(IconSize(KIconLoader::Desktop), IconSize(KIconLoader::Desktop))); label->setFixedSize(label->sizeHint()); layout->addWidget(label, 0, Qt::AlignRight); QVBoxLayout* vlayout = new QVBoxLayout(); layout->addLayout(vlayout); for (QStringList::ConstIterator it = mErrorMsgs.constBegin(); it != mErrorMsgs.constEnd(); ++it) { label = new QLabel(*it, topWidget); label->setFixedSize(label->sizeHint()); vlayout->addWidget(label, 0, Qt::AlignLeft); } layout->addStretch(); if (!mDontShowAgain.isEmpty()) { mDontShowAgainCheck = new QCheckBox(i18nc("@option:check", "Do not display this error message again for this alarm"), topWidget); mDontShowAgainCheck->setFixedSize(mDontShowAgainCheck->sizeHint()); topLayout->addWidget(mDontShowAgainCheck, 0, Qt::AlignLeft); } } QGridLayout* grid = new QGridLayout(); grid->setColumnStretch(0, 1); // keep the buttons right-adjusted in the window topLayout->addLayout(grid); int gridIndex = 1; // Close button mOkButton = new PushButton(KStandardGuiItem::close(), topWidget); // Prevent accidental acknowledgement of the message if the user is typing when the window appears mOkButton->clearFocus(); mOkButton->setFocusPolicy(Qt::ClickFocus); // don't allow keyboard selection mOkButton->setFixedSize(mOkButton->sizeHint()); connect(mOkButton, &QAbstractButton::clicked, this, &MessageWin::slotOk); grid->addWidget(mOkButton, 0, gridIndex++, Qt::AlignHCenter); mOkButton->setWhatsThis(i18nc("@info:whatsthis", "Acknowledge the alarm")); if (mShowEdit) { // Edit button mEditButton = new PushButton(i18nc("@action:button", "&Edit..."), topWidget); mEditButton->setFocusPolicy(Qt::ClickFocus); // don't allow keyboard selection mEditButton->setFixedSize(mEditButton->sizeHint()); connect(mEditButton, &QAbstractButton::clicked, this, &MessageWin::slotEdit); grid->addWidget(mEditButton, 0, gridIndex++, Qt::AlignHCenter); mEditButton->setWhatsThis(i18nc("@info:whatsthis", "Edit the alarm.")); } // Defer button mDeferButton = new PushButton(i18nc("@action:button", "&Defer..."), topWidget); mDeferButton->setFocusPolicy(Qt::ClickFocus); // don't allow keyboard selection mDeferButton->setFixedSize(mDeferButton->sizeHint()); connect(mDeferButton, &QAbstractButton::clicked, this, &MessageWin::slotDefer); grid->addWidget(mDeferButton, 0, gridIndex++, Qt::AlignHCenter); mDeferButton->setWhatsThis(xi18nc("@info:whatsthis", "Defer the alarm until later." "You will be prompted to specify when the alarm should be redisplayed.")); if (mNoDefer) mDeferButton->hide(); else setDeferralLimit(mEvent); // ensure that button is disabled when alarm can't be deferred any more if (!mAudioFile.isEmpty() && (mVolume || mFadeVolume > 0)) { // Silence button to stop sound repetition const QPixmap pixmap = MainBarIcon(QStringLiteral("media-playback-stop")); mSilenceButton = new PushButton(topWidget); mSilenceButton->setIcon(pixmap); grid->addWidget(mSilenceButton, 0, gridIndex++, Qt::AlignHCenter); mSilenceButton->setToolTip(i18nc("@info:tooltip", "Stop sound")); mSilenceButton->setWhatsThis(i18nc("@info:whatsthis", "Stop playing the sound")); // To avoid getting in a mess, disable the button until sound playing has been set up mSilenceButton->setEnabled(false); } KIconLoader iconLoader; if (mKMailSerialNumber) { // KMail button const QPixmap pixmap = iconLoader.loadIcon(QStringLiteral("internet-mail"), KIconLoader::MainToolbar); mKMailButton = new PushButton(topWidget); mKMailButton->setIcon(pixmap); connect(mKMailButton, &QAbstractButton::clicked, this, &MessageWin::slotShowKMailMessage); grid->addWidget(mKMailButton, 0, gridIndex++, Qt::AlignHCenter); mKMailButton->setToolTip(xi18nc("@info:tooltip Locate this email in KMail", "Locate in KMail")); mKMailButton->setWhatsThis(xi18nc("@info:whatsthis", "Locate and highlight this email in KMail")); } // KAlarm button const QPixmap pixmap = iconLoader.loadIcon(KAboutData::applicationData().componentName(), KIconLoader::MainToolbar); mKAlarmButton = new PushButton(topWidget); mKAlarmButton->setIcon(pixmap); connect(mKAlarmButton, &QAbstractButton::clicked, this, &MessageWin::displayMainWindow); grid->addWidget(mKAlarmButton, 0, gridIndex++, Qt::AlignHCenter); mKAlarmButton->setToolTip(xi18nc("@info:tooltip", "Activate KAlarm")); mKAlarmButton->setWhatsThis(xi18nc("@info:whatsthis", "Activate KAlarm")); int butsize = mKAlarmButton->sizeHint().height(); if (mSilenceButton) butsize = qMax(butsize, mSilenceButton->sizeHint().height()); if (mKMailButton) butsize = qMax(butsize, mKMailButton->sizeHint().height()); mKAlarmButton->setFixedSize(butsize, butsize); if (mSilenceButton) mSilenceButton->setFixedSize(butsize, butsize); if (mKMailButton) mKMailButton->setFixedSize(butsize, butsize); // Disable all buttons initially, to prevent accidental clicking on if they happen to be // under the mouse just as the window appears. mOkButton->setEnabled(false); if (mDeferButton->isVisible()) mDeferButton->setEnabled(false); if (mEditButton) mEditButton->setEnabled(false); if (mKMailButton) mKMailButton->setEnabled(false); mKAlarmButton->setEnabled(false); topLayout->activate(); setMinimumSize(QSize(grid->sizeHint().width() + 2 * style()->pixelMetric(QStyle::PM_DefaultChildMargin), sizeHint().height())); const bool modal = !(windowFlags() & Qt::X11BypassWindowManagerHint); NET::States wstate = NET::Sticky | NET::StaysOnTop; if (modal) wstate |= NET::Modal; WId winid = winId(); KWindowSystem::setState(winid, wstate); KWindowSystem::setOnAllDesktops(winid, true); mInitialised = true; // the window's widgets have been created } /****************************************************************************** * Return the number of message windows, optionally excluding always-hidden ones. */ int MessageWin::instanceCount(bool excludeAlwaysHidden) { int count = mWindowList.count(); if (excludeAlwaysHidden) { foreach (MessageWin* win, mWindowList) { if (win->mAlwaysHide) --count; } } return count; } bool MessageWin::hasDefer() const { return mDeferButton && mDeferButton->isVisible(); } /****************************************************************************** * Show the Defer button when it was previously hidden. */ void MessageWin::showDefer() { if (mDeferButton) { mNoDefer = false; mDeferButton->show(); setDeferralLimit(mEvent); // ensure that button is disabled when alarm can't be deferred any more resize(sizeHint()); } } /****************************************************************************** * Convert a reminder window into a normal alarm window. */ void MessageWin::cancelReminder(const KAEvent& event, const KAAlarm& alarm) { if (!mInitialised) return; mDateTime = alarm.dateTime(true); mNoPostAction = false; mAlarmType = alarm.type(); if (event.autoClose()) mCloseTime = alarm.dateTime().effectiveKDateTime().toUtc().qDateTime().addSecs(event.lateCancel() * 60); setCaption(i18nc("@title:window", "Message")); mTimeLabel->setText(dateTimeToDisplay()); if (mRemainingText) mRemainingText->hide(); MidnightTimer::disconnect(this, SLOT(setRemainingTextDay())); MinuteTimer::disconnect(this, SLOT(setRemainingTextMinute())); setMinimumHeight(0); centralWidget()->layout()->activate(); setMinimumHeight(sizeHint().height()); resize(sizeHint()); } /****************************************************************************** * Show the alarm's trigger time. * This is assumed to have previously been hidden. */ void MessageWin::showDateTime(const KAEvent& event, const KAAlarm& alarm) { if (!mTimeLabel) return; mDateTime = (alarm.type() & KAAlarm::REMINDER_ALARM) ? event.mainDateTime(true) : alarm.dateTime(true); if (mDateTime.isValid()) { mTimeLabel->setText(dateTimeToDisplay()); mTimeLabel->show(); } } /****************************************************************************** * Get the trigger time to display. */ QString MessageWin::dateTimeToDisplay() { QString tm; if (mDateTime.isValid()) { if (mDateTime.isDateOnly()) tm = QLocale().toString(mDateTime.date(), QLocale::ShortFormat); else { bool showZone = false; if (mDateTime.timeType() == KADateTime::UTC || (mDateTime.timeType() == KADateTime::TimeZone && !mDateTime.isLocalZone())) { // Display time zone abbreviation if it's different from the local // zone. Note that the iCalendar time zone might represent the local // time zone in a slightly different way from the system time zone, // so the zone comparison above might not produce the desired result. const QString tz = mDateTime.kDateTime().toString(QStringLiteral("%Z")); KADateTime local = mDateTime.kDateTime(); local.setTimeSpec(KADateTime::Spec::LocalZone()); showZone = (local.toString(QStringLiteral("%Z")) != tz); } #pragma message("port QT5") tm = QLocale().toString(mDateTime.qDateTime(), QLocale::ShortFormat); // KLocale::DateTimeFormatOptions(showZone ? KLocale::TimeZone : 0)); } } return tm; } /****************************************************************************** * Set the remaining time text in a reminder window. * Called at the start of every day (at the user-defined start-of-day time). */ void MessageWin::setRemainingTextDay() { QString text; const int days = KADateTime::currentLocalDate().daysTo(mDateTime.date()); if (days <= 0 && !mDateTime.isDateOnly()) { // The alarm is due today, so start refreshing every minute MidnightTimer::disconnect(this, SLOT(setRemainingTextDay())); setRemainingTextMinute(); MinuteTimer::connect(this, SLOT(setRemainingTextMinute())); // update every minute } else { if (days <= 0) text = i18nc("@info", "Today"); else if (days % 7) text = i18ncp("@info", "Tomorrow", "in %1 days' time", days); else text = i18ncp("@info", "in 1 week's time", "in %1 weeks' time", days/7); } mRemainingText->setText(text); } /****************************************************************************** * Set the remaining time text in a reminder window. * Called on every minute boundary. */ void MessageWin::setRemainingTextMinute() { QString text; const int mins = (KADateTime::currentUtcDateTime().secsTo(mDateTime.effectiveKDateTime()) + 59) / 60; if (mins < 60) text = i18ncp("@info", "in 1 minute's time", "in %1 minutes' time", (mins > 0 ? mins : 0)); else if (mins % 60 == 0) text = i18ncp("@info", "in 1 hour's time", "in %1 hours' time", mins/60); else { QString hourText = i18ncp("@item:intext inserted into 'in ... %1 minute's time' below", "1 hour", "%1 hours", mins/60); text = i18ncp("@info '%2' is the previous message '1 hour'/'%1 hours'", "in %2 1 minute's time", "in %2 %1 minutes' time", mins%60, hourText); } mRemainingText->setText(text); } /****************************************************************************** * Called when output is available from the command which is providing the text * for this window. Add the output and resize the window to show it. */ void MessageWin::readProcessOutput(ShellProcess* proc) { const QByteArray data = proc->readAll(); if (!data.isEmpty()) { // Strip any trailing newline, to avoid showing trailing blank line // in message window. if (mCommandText->newLine()) mCommandText->append(QStringLiteral("\n")); const int nl = data.endsWith('\n') ? 1 : 0; mCommandText->setNewLine(nl); mCommandText->insertPlainText(QString::fromLocal8Bit(data.data(), data.length() - nl)); resize(sizeHint()); } } /****************************************************************************** * Save settings to the session managed config file, for restoration * when the program is restored. */ void MessageWin::saveProperties(KConfigGroup& config) { if (mShown && !mErrorWindow && !mAlwaysHide) { config.writeEntry("EventID", mEventId.eventId()); config.writeEntry("EventItemID", mEventItemId); config.writeEntry("AlarmType", static_cast(mAlarmType)); if (mAlarmType == KAAlarm::INVALID_ALARM) - qCCritical(KALARM_LOG) << "Invalid alarm: id=" << mEventId << ", alarm count=" << mEvent.alarmCount(); + qCCritical(KALARM_LOG) << "MessageWin::saveProperties: Invalid alarm: id=" << mEventId << ", alarm count=" << mEvent.alarmCount(); config.writeEntry("Message", mMessage); config.writeEntry("Type", static_cast(mAction)); config.writeEntry("Font", mFont); config.writeEntry("BgColour", mBgColour); config.writeEntry("FgColour", mFgColour); config.writeEntry("ConfirmAck", mConfirmAck); if (mDateTime.isValid()) { //TODO: Write KADateTime when it becomes possible config.writeEntry("Time", mDateTime.effectiveDateTime()); config.writeEntry("DateOnly", mDateTime.isDateOnly()); QByteArray zone; if (mDateTime.isUtc()) zone = "UTC"; else if (mDateTime.timeType() == KADateTime::TimeZone) { const QTimeZone tz = mDateTime.timeZone(); if (tz.isValid()) zone = tz.id(); } config.writeEntry("TimeZone", zone); } if (mCloseTime.isValid()) config.writeEntry("Expiry", mCloseTime); if (mAudioRepeatPause >= 0 && mSilenceButton && mSilenceButton->isEnabled()) { // Only need to restart sound file playing if it's being repeated config.writePathEntry("AudioFile", mAudioFile); config.writeEntry("Volume", static_cast(mVolume * 100)); config.writeEntry("AudioPause", mAudioRepeatPause); } config.writeEntry("Speak", mSpeak); config.writeEntry("Height", height()); config.writeEntry("DeferMins", mDefaultDeferMinutes); config.writeEntry("NoDefer", mNoDefer); config.writeEntry("NoPostAction", mNoPostAction); config.writeEntry("KMailSerial", static_cast(mKMailSerialNumber)); config.writeEntry("CmdErr", static_cast(mCommandError)); config.writeEntry("DontShowAgain", mDontShowAgain); } else config.writeEntry("Invalid", true); } /****************************************************************************** * Read settings from the session managed config file. * This function is automatically called whenever the app is being restored. * Read in whatever was saved in saveProperties(). */ void MessageWin::readProperties(const KConfigGroup& config) { mInvalid = config.readEntry("Invalid", false); QString eventId = config.readEntry("EventID"); mEventItemId = config.readEntry("EventItemID", Akonadi::Item::Id(-1)); mAlarmType = static_cast(config.readEntry("AlarmType", 0)); if (mAlarmType == KAAlarm::INVALID_ALARM) { mInvalid = true; - qCCritical(KALARM_LOG) << "Invalid alarm: id=" << eventId; + qCCritical(KALARM_LOG) << "MessageWin::readProperties: Invalid alarm: id=" << eventId; } mMessage = config.readEntry("Message"); mAction = static_cast(config.readEntry("Type", 0)); mFont = config.readEntry("Font", QFont()); mBgColour = config.readEntry("BgColour", QColor(Qt::white)); mFgColour = config.readEntry("FgColour", QColor(Qt::black)); mConfirmAck = config.readEntry("ConfirmAck", false); QDateTime invalidDateTime; QDateTime dt = config.readEntry("Time", invalidDateTime); const QByteArray zoneId = config.readEntry("TimeZone").toLatin1(); KADateTime::Spec timeSpec; if (zoneId.isEmpty()) timeSpec = KADateTime::LocalZone; else if (zoneId == "UTC") timeSpec = KADateTime::UTC; else timeSpec = QTimeZone(zoneId); mDateTime = KADateTime(dt.date(), dt.time(), timeSpec); const bool dateOnly = config.readEntry("DateOnly", false); if (dateOnly) mDateTime.setDateOnly(true); mCloseTime = config.readEntry("Expiry", invalidDateTime); mCloseTime.setTimeSpec(Qt::UTC); mAudioFile = config.readPathEntry("AudioFile", QString()); mVolume = static_cast(config.readEntry("Volume", 0)) / 100; mFadeVolume = -1; mFadeSeconds = 0; if (!mAudioFile.isEmpty()) // audio file URL was only saved if it repeats mAudioRepeatPause = config.readEntry("AudioPause", 0); mBeep = false; // don't beep after restart (similar to not playing non-repeated sound file) mSpeak = config.readEntry("Speak", false); mRestoreHeight = config.readEntry("Height", 0); mDefaultDeferMinutes = config.readEntry("DeferMins", 0); mNoDefer = config.readEntry("NoDefer", false); mNoPostAction = config.readEntry("NoPostAction", true); mKMailSerialNumber = static_cast(config.readEntry("KMailSerial", QVariant(QVariant::ULongLong)).toULongLong()); mCommandError = KAEvent::CmdErrType(config.readEntry("CmdErr", static_cast(KAEvent::CMD_NO_ERROR))); mDontShowAgain = config.readEntry("DontShowAgain", QString()); mShowEdit = false; // Temporarily initialise mCollection and mEventId - they will be set by redisplayAlarm() mCollection = Akonadi::Collection(); mEventId = EventId(mCollection.id(), eventId); - qCDebug(KALARM_LOG) << eventId; + qCDebug(KALARM_LOG) << "MessageWin::readProperties:" << eventId; if (mAlarmType != KAAlarm::INVALID_ALARM) { // Recreate the event from the calendar file (if possible) if (eventId.isEmpty()) initView(); else { // Close any other window for this alarm which has already been restored by redisplayAlarms() if (!AkonadiModel::instance()->isCollectionTreeFetched()) { connect(AkonadiModel::instance(), &Akonadi::EntityTreeModel::collectionTreeFetched, this, &MessageWin::showRestoredAlarm); return; } redisplayAlarm(); } } } /****************************************************************************** * Fetch the restored alarm from the calendar and redisplay it in this window. */ void MessageWin::showRestoredAlarm() { - qCDebug(KALARM_LOG) << mEventId; + qCDebug(KALARM_LOG) << "MessageWin::showRestoredAlarm:" << mEventId; redisplayAlarm(); show(); } /****************************************************************************** * Fetch the restored alarm from the calendar and redisplay it in this window. */ void MessageWin::redisplayAlarm() { mCollection = AkonadiModel::instance()->collectionForItem(mEventItemId); mEventId.setCollectionId(mCollection.id()); - qCDebug(KALARM_LOG) << mEventId; + qCDebug(KALARM_LOG) << "MessageWin::redisplayAlarm:" << mEventId; // Delete any already existing window for the same event MessageWin* duplicate = findEvent(mEventId, this); if (duplicate) - qCDebug(KALARM_LOG) << "Deleting duplicate window:" << mEventId; + qCDebug(KALARM_LOG) << "MessageWin::redisplayAlarm: Deleting duplicate window:" << mEventId; delete duplicate; KAEvent* event = AlarmCalendar::resources()->event(mEventId); if (event) { mEvent = *event; mShowEdit = true; } else { // It's not in the active calendar, so try the displaying or archive calendars retrieveEvent(mEvent, mCollection, mShowEdit, mNoDefer); mNoDefer = !mNoDefer; } initView(); } /****************************************************************************** * Redisplay alarms which were being shown when the program last exited. * Normally, these alarms will have been displayed by session restoration, but * if the program crashed or was killed, we can redisplay them here so that * they won't be lost. */ void MessageWin::redisplayAlarms() { if (mRedisplayed) return; - qCDebug(KALARM_LOG); + qCDebug(KALARM_LOG) << "MessageWin::redisplayAlarms"; mRedisplayed = true; AlarmCalendar* cal = AlarmCalendar::displayCalendar(); if (cal && cal->isOpen()) { KAEvent event; Akonadi::Collection collection; const Event::List events = cal->kcalEvents(); for (int i = 0, end = events.count(); i < end; ++i) { bool showDefer, showEdit; reinstateFromDisplaying(events[i], event, collection, showEdit, showDefer); Akonadi::Item::Id id = AkonadiModel::instance()->findItemId(event); if (id >= 0) event.setItemId(id); const EventId eventId(event); if (findEvent(eventId)) - qCDebug(KALARM_LOG) << "Message window already exists:" << eventId; + qCDebug(KALARM_LOG) << "MessageWin::redisplayAlarms: Message window already exists:" << eventId; else { // This event should be displayed, but currently isn't being const KAAlarm alarm = event.convertDisplayingAlarm(); if (alarm.type() == KAAlarm::INVALID_ALARM) { - qCCritical(KALARM_LOG) << "Invalid alarm: id=" << eventId; + qCCritical(KALARM_LOG) << "MessageWin::redisplayAlarms: Invalid alarm: id=" << eventId; continue; } - qCDebug(KALARM_LOG) << eventId; + qCDebug(KALARM_LOG) << "MessageWin::redisplayAlarms:" << eventId; const bool login = alarm.repeatAtLogin(); const int flags = NO_RESCHEDULE | (login ? NO_DEFER : 0) | NO_INIT_VIEW; MessageWin* win = new MessageWin(&event, alarm, flags); win->mCollection = collection; const bool rw = CollectionControlModel::isWritableEnabled(collection, event.category()) > 0; win->mShowEdit = rw ? showEdit : false; win->mNoDefer = (rw && !login) ? !showDefer : true; win->initView(); win->show(); } } } } /****************************************************************************** * Retrieves the event with the current ID from the displaying calendar file, * or if not found there, from the archive calendar. */ bool MessageWin::retrieveEvent(KAEvent& event, Akonadi::Collection& resource, bool& showEdit, bool& showDefer) { const Event::Ptr kcalEvent = AlarmCalendar::displayCalendar()->kcalEvent(CalEvent::uid(mEventId.eventId(), CalEvent::DISPLAYING)); if (!reinstateFromDisplaying(kcalEvent, event, resource, showEdit, showDefer)) { // The event isn't in the displaying calendar. // Try to retrieve it from the archive calendar. KAEvent* ev = nullptr; Akonadi::Collection archiveCol = CollectionControlModel::getStandard(CalEvent::ARCHIVED); if (archiveCol.isValid()) ev = AlarmCalendar::resources()->event(EventId(archiveCol.id(), CalEvent::uid(mEventId.eventId(), CalEvent::ARCHIVED))); if (!ev) return false; event = *ev; event.setArchive(); // ensure that it gets re-archived if it's saved event.setCategory(CalEvent::ACTIVE); if (mEventId.eventId() != event.id()) - qCCritical(KALARM_LOG) << "Wrong event ID"; + qCCritical(KALARM_LOG) << "MessageWin::retrieveEvent: Wrong event ID"; event.setEventId(mEventId.eventId()); resource = Akonadi::Collection(); showEdit = true; showDefer = true; - qCDebug(KALARM_LOG) << event.id() << ": success"; + qCDebug(KALARM_LOG) << "MessageWin::retrieveEvent:" << event.id() << ": success"; } return true; } /****************************************************************************** * Retrieves the displayed event from the calendar file, or if not found there, * from the displaying calendar. */ bool MessageWin::reinstateFromDisplaying(const Event::Ptr& kcalEvent, KAEvent& event, Akonadi::Collection& collection, bool& showEdit, bool& showDefer) { if (!kcalEvent) return false; Akonadi::Collection::Id collectionId; event.reinstateFromDisplaying(kcalEvent, collectionId, showEdit, showDefer); event.setCollectionId(collectionId); collection = AkonadiModel::instance()->collectionById(collectionId); - qCDebug(KALARM_LOG) << EventId(event) << ": success"; + qCDebug(KALARM_LOG) << "MessageWin::reinstateFromDisplaying:" << EventId(event) << ": success"; return true; } /****************************************************************************** * Called when an alarm is currently being displayed, to store a copy of the * alarm in the displaying calendar, and to reschedule it for its next repetition. * If no repetitions remain, cancel it. */ void MessageWin::alarmShowing(KAEvent& event) { - qCDebug(KALARM_LOG) << event.id() << "," << KAAlarm::debugType(mAlarmType); + qCDebug(KALARM_LOG) << "MessageWin::alarmShowing:" << event.id() << "," << KAAlarm::debugType(mAlarmType); const KAAlarm alarm = event.alarm(mAlarmType); if (!alarm.isValid()) { - qCCritical(KALARM_LOG) << "Alarm type not found:" << event.id() << ":" << mAlarmType; + qCCritical(KALARM_LOG) << "MessageWin::alarmShowing: Alarm type not found:" << event.id() << ":" << mAlarmType; return; } if (!mAlwaysHide) { // Copy the alarm to the displaying calendar in case of a crash, etc. KAEvent dispEvent; const Akonadi::Collection collection = AkonadiModel::instance()->collectionForItem(event.itemId()); dispEvent.setDisplaying(event, mAlarmType, collection.id(), mDateTime.effectiveKDateTime(), mShowEdit, !mNoDefer); AlarmCalendar* cal = AlarmCalendar::displayCalendarOpen(); if (cal) { cal->deleteDisplayEvent(dispEvent.id()); // in case it already exists cal->addEvent(dispEvent); cal->save(); } } theApp()->rescheduleAlarm(event, alarm); } /****************************************************************************** * Spread alarm windows over the screen so that they are all visible, or pile * them on top of each other again. * Reply = true if windows are now scattered, false if piled up. */ bool MessageWin::spread(bool scatter) { if (instanceCount(true) <= 1) // ignore always-hidden windows return false; const QRect desk = KAlarm::desktopWorkArea(); // get the usable area of the desktop if (scatter == isSpread(desk.topLeft())) return scatter; if (scatter) { // Usually there won't be many windows, so a crude // scattering algorithm should suffice. int x = desk.left(); int y = desk.top(); int ynext = y; for (int errmsgs = 0; errmsgs < 2; ++errmsgs) { // Display alarm messages first, then error messages, since most // error messages tend to be the same height. for (int i = 0, end = mWindowList.count(); i < end; ++i) { MessageWin* w = mWindowList[i]; if ((!errmsgs && w->mErrorWindow) || (errmsgs && !w->mErrorWindow)) continue; const QSize sz = w->frameGeometry().size(); if (x + sz.width() > desk.right()) { x = desk.left(); y = ynext; } int ytmp = y; if (y + sz.height() > desk.bottom()) { ytmp = desk.bottom() - sz.height(); if (ytmp < desk.top()) ytmp = desk.top(); } w->move(x, ytmp); x += sz.width(); if (ytmp + sz.height() > ynext) ynext = ytmp + sz.height(); } } } else { // Move all windows to the top left corner for (int i = 0, end = mWindowList.count(); i < end; ++i) mWindowList[i]->move(desk.topLeft()); } return scatter; } /****************************************************************************** * Check whether message windows are all piled up, or are spread out. * Reply = true if windows are currently spread, false if piled up. */ bool MessageWin::isSpread(const QPoint& topLeft) { for (int i = 0, end = mWindowList.count(); i < end; ++i) { if (mWindowList[i]->pos() != topLeft) return true; } return false; } /****************************************************************************** * Returns the existing message window (if any) which is displaying the event * with the specified ID. */ MessageWin* MessageWin::findEvent(const EventId& eventId, MessageWin* exclude) { if (!eventId.isEmpty()) { for (int i = 0, end = mWindowList.count(); i < end; ++i) { MessageWin* w = mWindowList[i]; if (w != exclude && w->mEventId == eventId && !w->mErrorWindow) return w; } } return nullptr; } /****************************************************************************** * Beep and play the audio file, as appropriate. */ void MessageWin::playAudio() { if (mBeep) { // Beep using two methods, in case the sound card/speakers are switched off or not working QApplication::beep(); // beep through the internal speaker KNotification::beep(); // beep through the sound card & speakers } if (!mAudioFile.isEmpty()) { if (!mVolume && mFadeVolume <= 0) return; // ensure zero volume doesn't play anything startAudio(); // play the audio file } else if (mSpeak) { // The message is to be spoken. In case of error messges, // call it on a timer to allow the window to display first. QTimer::singleShot(0, this, &MessageWin::slotSpeak); } } /****************************************************************************** * Speak the message. * Called asynchronously to avoid delaying the display of the message. */ void MessageWin::slotSpeak() { KPIMTextEdit::TextToSpeech *tts = KPIMTextEdit::TextToSpeech::self(); if (!tts->isReady()) { KAMessageBox::detailedError(MainWindow::mainMainWindow(), i18nc("@info", "Unable to speak message"), i18nc("@info", "Text-to-speech subsystem is not available")); clearErrorMessage(ErrMsg_Speak); return; } tts->say(mMessage); } /****************************************************************************** * Called when another window's audio thread has been destructed. * Start playing this window's audio file. Because initialising the sound system * and loading the file may take some time, it is called in a separate thread to * allow the window to display first. */ void MessageWin::startAudio() { if (mAudioThread) { // An audio file is already playing for another message // window, so wait until it has finished. connect(mAudioThread.data(), &QObject::destroyed, this, &MessageWin::audioTerminating); } else { - qCDebug(KALARM_LOG) << QThread::currentThread(); + qCDebug(KALARM_LOG) << "MessageWin::startAudio:" << QThread::currentThread(); mAudioThread = new AudioThread(this, mAudioFile, mVolume, mFadeVolume, mFadeSeconds, mAudioRepeatPause); connect(mAudioThread.data(), &AudioThread::readyToPlay, this, &MessageWin::playReady); connect(mAudioThread.data(), &QThread::finished, this, &MessageWin::playFinished); if (mSilenceButton) connect(mSilenceButton, &QAbstractButton::clicked, mAudioThread.data(), &QThread::quit); // Notify after creating mAudioThread, so that isAudioPlaying() will // return the correct value. theApp()->notifyAudioPlaying(true); mAudioThread->start(); } } /****************************************************************************** * Return whether audio playback is currently active. */ bool MessageWin::isAudioPlaying() { return mAudioThread; } /****************************************************************************** * Stop audio playback. */ void MessageWin::stopAudio(bool wait) { - qCDebug(KALARM_LOG); + qCDebug(KALARM_LOG) << "MessageWin::stopAudio"; if (mAudioThread) mAudioThread->stop(wait); } /****************************************************************************** * Called when another window's audio thread is being destructed. * Wait until the destructor has finished. */ void MessageWin::audioTerminating() { QTimer::singleShot(0, this, &MessageWin::startAudio); } /****************************************************************************** * Called when the audio file is ready to start playing. */ void MessageWin::playReady() { if (mSilenceButton) mSilenceButton->setEnabled(true); } /****************************************************************************** * Called when the audio file thread finishes. */ void MessageWin::playFinished() { if (mSilenceButton) mSilenceButton->setEnabled(false); if (mAudioThread) // mAudioThread can actually be null here! { const QString errmsg = mAudioThread->error(); if (!errmsg.isEmpty() && !haveErrorMessage(ErrMsg_AudioFile)) { KAMessageBox::error(this, errmsg); clearErrorMessage(ErrMsg_AudioFile); } } delete mAudioThread.data(); if (mAlwaysHide) close(); } /****************************************************************************** * Constructor for audio thread. */ AudioThread::AudioThread(MessageWin* parent, const QString& audioFile, float volume, float fadeVolume, int fadeSeconds, int repeatPause) : QThread(parent), mFile(audioFile), mVolume(volume), mFadeVolume(fadeVolume), mFadeSeconds(fadeSeconds), mRepeatPause(repeatPause), mAudioObject(nullptr) { if (mAudioOwner) - qCCritical(KALARM_LOG) << "mAudioOwner already set"; + qCCritical(KALARM_LOG) << "MessageWin::AudioThread: mAudioOwner already set"; mAudioOwner = parent; } /****************************************************************************** * Destructor for audio thread. Waits for thread completion and tidies up. * Note that this destructor is executed in the parent thread. */ AudioThread::~AudioThread() { - qCDebug(KALARM_LOG); + qCDebug(KALARM_LOG) << "~MessageWin::AudioThread"; stop(true); // stop playing and tidy up (timeout 3 seconds) delete mAudioObject; mAudioObject = nullptr; if (mAudioOwner == parent()) mAudioOwner = nullptr; // Notify after deleting mAudioThread, so that isAudioPlaying() will // return the correct value. QTimer::singleShot(0, theApp(), &KAlarmApp::notifyAudioStopped); } /****************************************************************************** * Quits the thread and waits for thread completion and tidies up. */ void AudioThread::stop(bool waiT) { - qCDebug(KALARM_LOG); + qCDebug(KALARM_LOG) << "MessageWin::AudioThread::stop"; quit(); // stop playing and tidy up wait(3000); // wait for run() to exit (timeout 3 seconds) if (!isFinished()) { // Something has gone wrong - forcibly kill the thread terminate(); if (waiT) wait(); } } /****************************************************************************** * Kick off the thread to play the audio file. */ void AudioThread::run() { mMutex.lock(); if (mAudioObject) { mMutex.unlock(); return; } - qCDebug(KALARM_LOG) << QThread::currentThread() << mFile; + qCDebug(KALARM_LOG) << "MessageWin::AudioThread::run:" << QThread::currentThread() << mFile; const QString audioFile = mFile; const QUrl url = QUrl::fromUserInput(mFile); mFile = url.isLocalFile() ? url.toLocalFile() : url.toString(); Phonon::MediaSource source(url); if (source.type() == Phonon::MediaSource::Invalid) { mError = xi18nc("@info", "Cannot open audio file: %1", audioFile); mMutex.unlock(); - qCCritical(KALARM_LOG) << "Open failure:" << audioFile; + qCCritical(KALARM_LOG) << "MessageWin::AudioThread::run: Open failure:" << audioFile; return; } mAudioObject = new Phonon::MediaObject(); mAudioObject->setCurrentSource(source); mAudioObject->setTransitionTime(100); // workaround to prevent clipping of end of files in Xine backend Phonon::AudioOutput* output = new Phonon::AudioOutput(Phonon::NotificationCategory, mAudioObject); mPath = Phonon::createPath(mAudioObject, output); if (mVolume >= 0 || mFadeVolume >= 0) { const float vol = (mVolume >= 0) ? mVolume : output->volume(); const float maxvol = qMax(vol, mFadeVolume); output->setVolume(maxvol); if (mFadeVolume >= 0 && mFadeSeconds > 0) { Phonon::VolumeFaderEffect* fader = new Phonon::VolumeFaderEffect(mAudioObject); fader->setVolume(mFadeVolume / maxvol); fader->fadeTo(mVolume / maxvol, mFadeSeconds * 1000); mPath.insertEffect(fader); } } connect(mAudioObject, &Phonon::MediaObject::stateChanged, this, &AudioThread::playStateChanged, Qt::DirectConnection); connect(mAudioObject, &Phonon::MediaObject::finished, this, &AudioThread::checkAudioPlay, Qt::DirectConnection); mPlayedOnce = false; mPausing = false; mMutex.unlock(); Q_EMIT readyToPlay(); checkAudioPlay(); // Start an event loop. // The function will exit once exit() or quit() is called. // First, ensure that the thread object is deleted once it has completed. connect(this, &QThread::finished, this, &QObject::deleteLater); exec(); stopPlay(); } /****************************************************************************** * Called when the audio file has loaded and is ready to play, or when play * has completed. * If it is ready to play, start playing it (for the first time or repeated). * If play has not yet completed, wait a bit longer. */ void AudioThread::checkAudioPlay() { mMutex.lock(); if (!mAudioObject) { mMutex.unlock(); return; } if (mPausing) mPausing = false; else { // The file has loaded and is ready to play, or play has completed if (mPlayedOnce) { if (mRepeatPause < 0) { // Play has completed mMutex.unlock(); stopPlay(); return; } if (mRepeatPause > 0) { // Pause before playing the file again mPausing = true; QTimer::singleShot(mRepeatPause * 1000, this, &AudioThread::checkAudioPlay); mMutex.unlock(); return; } } mPlayedOnce = true; } // Start playing the file, either for the first time or again - qCDebug(KALARM_LOG) << "start"; + qCDebug(KALARM_LOG) << "MessageWin::AudioThread::checkAudioPlay: start"; mAudioObject->play(); mMutex.unlock(); } /****************************************************************************** * Called when the playback object changes state. * If an error has occurred, quit and return the error to the caller. */ void AudioThread::playStateChanged(Phonon::State newState) { if (newState == Phonon::ErrorState) { QMutexLocker locker(&mMutex); const QString err = mAudioObject->errorString(); if (!err.isEmpty()) { - qCCritical(KALARM_LOG) << "Play failure:" << mFile << ":" << err; + qCCritical(KALARM_LOG) << "MessageWin::AudioThread::playStateChanged: Play failure:" << mFile << ":" << err; mError = xi18nc("@info", "Error playing audio file: %1%2", mFile, err); exit(1); } } } /****************************************************************************** * Called when play completes, the Silence button is clicked, or the window is * closed, to terminate audio access. */ void AudioThread::stopPlay() { mMutex.lock(); if (mAudioObject) { mAudioObject->stop(); const QList effects = mPath.effects(); for (int i = 0; i < effects.count(); ++i) { mPath.removeEffect(effects[i]); delete effects[i]; } delete mAudioObject; mAudioObject = nullptr; } mMutex.unlock(); quit(); // exit the event loop, if it's still running } QString AudioThread::error() const { QMutexLocker locker(&mMutex); return mError; } /****************************************************************************** * Raise the alarm window, re-output any required audio notification, and * reschedule the alarm in the calendar file. */ void MessageWin::repeat(const KAAlarm& alarm) { if (!mInitialised) return; if (mDeferDlg) { // Cancel any deferral dialog so that the user notices something's going on, // and also because the deferral time limit will have changed. delete mDeferDlg; mDeferDlg = nullptr; } KAEvent* event = mEventId.isEmpty() ? nullptr : AlarmCalendar::resources()->event(mEventId); if (event) { mAlarmType = alarm.type(); // store new alarm type for use if it is later deferred if (mAlwaysHide) playAudio(); else { if (!mDeferDlg || Preferences::modalMessages()) { raise(); playAudio(); } if (mDeferButton->isVisible()) { mDeferButton->setEnabled(true); setDeferralLimit(*event); // ensure that button is disabled when alarm can't be deferred any more } } alarmShowing(*event); } } /****************************************************************************** * Display the window. * If windows are being positioned away from the mouse cursor, it is initially * positioned at the top left to slightly reduce the number of times the * windows need to be moved in showEvent(). */ void MessageWin::show() { if (mCloseTime.isValid()) { // Set a timer to auto-close the window int delay = QDateTime::currentDateTimeUtc().secsTo(mCloseTime); if (delay < 0) delay = 0; QTimer::singleShot(delay * 1000, this, &QWidget::close); if (!delay) return; // don't show the window if auto-closing is already due } if (Preferences::messageButtonDelay() == 0) move(0, 0); MainWindowBase::show(); } /****************************************************************************** * Returns the window's recommended size exclusive of its frame. */ QSize MessageWin::sizeHint() const { QSize desired; switch (mAction) { case KAEvent::MESSAGE: desired = MainWindowBase::sizeHint(); break; case KAEvent::COMMAND: if (mShown) { // For command output, expand the window to accommodate the text const QSize texthint = mCommandText->sizeHint(); int w = texthint.width() + 2 * style()->pixelMetric(QStyle::PM_DefaultChildMargin); if (w < width()) w = width(); const int ypadding = height() - mCommandText->height(); desired = QSize(w, texthint.height() + ypadding); break; } // fall through to default default: return MainWindowBase::sizeHint(); } // Limit the size to fit inside the working area of the desktop const QSize desktop = KAlarm::desktopWorkArea(mScreenNumber).size(); const QSize frameThickness = frameGeometry().size() - geometry().size(); // title bar & window frame return desired.boundedTo(desktop - frameThickness); } /****************************************************************************** * Called when the window is shown. * The first time, output any required audio notification, and reschedule or * delete the event from the calendar file. */ void MessageWin::showEvent(QShowEvent* se) { MainWindowBase::showEvent(se); if (mShown || !mInitialised) return; if (mErrorWindow || mAlarmType == KAAlarm::INVALID_ALARM) { // Don't bother repositioning error messages, // and invalid alarms should be deleted anyway. enableButtons(); } else { /* Set the window size. * Note that the frame thickness is not yet known when this * method is called, so for large windows the size needs to be * set again later. */ bool execComplete = true; QSize s = sizeHint(); // fit the window round the message if (mAction == KAEvent::FILE && !mErrorMsgs.count()) KAlarm::readConfigWindowSize("FileMessage", s); resize(s); const QRect desk = KAlarm::desktopWorkArea(mScreenNumber); const QRect frame = frameGeometry(); mButtonDelay = Preferences::messageButtonDelay() * 1000; if (mButtonDelay) { // Position the window in the middle of the screen, and // delay enabling the buttons. mPositioning = true; move((desk.width() - frame.width())/2, (desk.height() - frame.height())/2); execComplete = false; } else { /* Try to ensure that the window can't accidentally be acknowledged * by the user clicking the mouse just as it appears. * To achieve this, move the window so that the OK button is as far away * from the cursor as possible. If the buttons are still too close to the * cursor, disable the buttons for a short time. * N.B. This can't be done in show(), since the geometry of the window * is not known until it is displayed. Unfortunately by moving the * window in showEvent(), a flicker is unavoidable. * See the Qt documentation on window geometry for more details. */ // PROBLEM: The frame size is not known yet! const QPoint cursor = QCursor::pos(); const QRect rect = geometry(); // Find the offsets from the outside of the frame to the edges of the OK button const QRect button(mOkButton->mapToParent(QPoint(0, 0)), mOkButton->mapToParent(mOkButton->rect().bottomRight())); const int buttonLeft = button.left() + rect.left() - frame.left(); const int buttonRight = width() - button.right() + frame.right() - rect.right(); const int buttonTop = button.top() + rect.top() - frame.top(); const int buttonBottom = height() - button.bottom() + frame.bottom() - rect.bottom(); const int centrex = (desk.width() + buttonLeft - buttonRight) / 2; const int centrey = (desk.height() + buttonTop - buttonBottom) / 2; const int x = (cursor.x() < centrex) ? desk.right() - frame.width() : desk.left(); const int y = (cursor.y() < centrey) ? desk.bottom() - frame.height() : desk.top(); // Find the enclosing rectangle for the new button positions // and check if the cursor is too near QRect buttons = mOkButton->geometry().united(mKAlarmButton->geometry()); buttons.translate(rect.left() + x - frame.left(), rect.top() + y - frame.top()); const int minDistance = proximityMultiple * mOkButton->height(); if ((abs(cursor.x() - buttons.left()) < minDistance || abs(cursor.x() - buttons.right()) < minDistance) && (abs(cursor.y() - buttons.top()) < minDistance || abs(cursor.y() - buttons.bottom()) < minDistance)) mButtonDelay = proximityButtonDelay; // too near - disable buttons initially if (x != frame.left() || y != frame.top()) { mPositioning = true; move(x, y); execComplete = false; } } if (execComplete) displayComplete(); // play audio, etc. } // Set the window size etc. once the frame size is known QTimer::singleShot(0, this, &MessageWin::frameDrawn); mShown = true; } /****************************************************************************** * Called when the window has been moved. */ void MessageWin::moveEvent(QMoveEvent* e) { MainWindowBase::moveEvent(e); theApp()->setSpreadWindowsState(isSpread(KAlarm::desktopWorkArea(mScreenNumber).topLeft())); if (mPositioning) { // The window has just been initially positioned mPositioning = false; displayComplete(); // play audio, etc. } } /****************************************************************************** * Called after (hopefully) the window frame size is known. * Reset the initial window size if it exceeds the working area of the desktop. * Set the 'spread windows' menu item status. */ void MessageWin::frameDrawn() { if (!mErrorWindow && mAction == KAEvent::MESSAGE) { const QSize s = sizeHint(); if (width() > s.width() || height() > s.height()) resize(s); } theApp()->setSpreadWindowsState(isSpread(KAlarm::desktopWorkArea(mScreenNumber).topLeft())); } /****************************************************************************** * Called when the window has been displayed properly (in its correct position), * to play sounds and reschedule the event. */ void MessageWin::displayComplete() { playAudio(); if (mRescheduleEvent) alarmShowing(mEvent); if (!mAlwaysHide) { // Enable the window's buttons either now or after the configured delay if (mButtonDelay > 0) QTimer::singleShot(mButtonDelay, this, &MessageWin::enableButtons); else enableButtons(); } } /****************************************************************************** * Enable the window's buttons. */ void MessageWin::enableButtons() { mOkButton->setEnabled(true); mKAlarmButton->setEnabled(true); if (mDeferButton->isVisible() && !mDisableDeferral) mDeferButton->setEnabled(true); if (mEditButton) mEditButton->setEnabled(true); if (mKMailButton) mKMailButton->setEnabled(true); } /****************************************************************************** * Called when the window's size has changed (before it is painted). */ void MessageWin::resizeEvent(QResizeEvent* re) { if (mRestoreHeight) { // Restore the window height on session restoration if (mRestoreHeight != re->size().height()) { QSize size = re->size(); size.setHeight(mRestoreHeight); resize(size); } else if (isVisible()) mRestoreHeight = 0; } else { if (mShown && mAction == KAEvent::FILE && !mErrorMsgs.count()) KAlarm::writeConfigWindowSize("FileMessage", re->size()); MainWindowBase::resizeEvent(re); } } /****************************************************************************** * Called when a close event is received. * Only quits the application if there is no system tray icon displayed. */ void MessageWin::closeEvent(QCloseEvent* ce) { // Don't prompt or delete the alarm from the display calendar if the session is closing if (!mErrorWindow && !qApp->isSavingSession()) { if (mConfirmAck && !mNoCloseConfirm) { // Ask for confirmation of acknowledgement. Use warningYesNo() because its default is No. if (KAMessageBox::warningYesNo(this, i18nc("@info", "Do you really want to acknowledge this alarm?"), i18nc("@action:button", "Acknowledge Alarm"), KGuiItem(i18nc("@action:button", "Acknowledge")), KStandardGuiItem::cancel()) != KMessageBox::Yes) { ce->ignore(); return; } } if (!mEventId.isEmpty()) { // Delete from the display calendar KAlarm::deleteDisplayEvent(CalEvent::uid(mEventId.eventId(), CalEvent::DISPLAYING)); } } MainWindowBase::closeEvent(ce); } /****************************************************************************** * Called when the OK button is clicked. */ void MessageWin::slotOk() { if (mDontShowAgainCheck && mDontShowAgainCheck->isChecked()) KAlarm::setDontShowErrors(mEventId, mDontShowAgain); close(); } /****************************************************************************** * Called when the KMail button is clicked. * Tells KMail to display the email message displayed in this message window. */ void MessageWin::slotShowKMailMessage() { - qCDebug(KALARM_LOG); + qCDebug(KALARM_LOG) << "MessageWin::slotShowKMailMessage"; if (!mKMailSerialNumber) return; const QString err = KAlarm::runKMail(false); if (!err.isNull()) { KAMessageBox::sorry(this, err); return; } org::kde::kmail::kmail kmail(KMAIL_DBUS_SERVICE, KMAIL_DBUS_PATH, QDBusConnection::sessionBus()); QDBusReply reply = kmail.showMail((qint64)mKMailSerialNumber); if (!reply.isValid()) qCCritical(KALARM_LOG) << "kmail D-Bus call failed:" << reply.error().message(); else if (!reply.value()) KAMessageBox::sorry(this, xi18nc("@info", "Unable to locate this email in KMail")); } /****************************************************************************** * Called when the Edit... button is clicked. * Displays the alarm edit dialog. * * NOTE: The alarm edit dialog is made a child of the main window, not this * window, so that if this window closes before the dialog (e.g. on * auto-close), KAlarm doesn't crash. The dialog is set non-modal so that * the main window is unaffected, but modal mode is simulated so that * this window is inactive while the dialog is open. */ void MessageWin::slotEdit() { - qCDebug(KALARM_LOG); + qCDebug(KALARM_LOG) << "MessageWin::slotEdit"; MainWindow* mainWin = MainWindow::mainMainWindow(); mEditDlg = EditAlarmDlg::create(false, &mOriginalEvent, false, mainWin, EditAlarmDlg::RES_IGNORE); KWindowSystem::setMainWindow(mEditDlg, winId()); KWindowSystem::setOnAllDesktops(mEditDlg->winId(), false); setButtonsReadOnly(true); connect(mEditDlg, &QDialog::accepted, this, &MessageWin::editCloseOk); connect(mEditDlg, &QDialog::rejected, this, &MessageWin::editCloseCancel); connect(mEditDlg, &QObject::destroyed, this, &MessageWin::editCloseCancel); connect(KWindowSystem::self(), &KWindowSystem::activeWindowChanged, this, &MessageWin::activeWindowChanged); mainWin->editAlarm(mEditDlg, mOriginalEvent); } /****************************************************************************** * Called when OK is clicked in the alarm edit dialog invoked by the Edit button. * Closes the window. */ void MessageWin::editCloseOk() { mEditDlg = nullptr; mNoCloseConfirm = true; // allow window to close without confirmation prompt close(); } /****************************************************************************** * Called when Cancel is clicked in the alarm edit dialog invoked by the Edit * button, or when the dialog is deleted. */ void MessageWin::editCloseCancel() { mEditDlg = nullptr; setButtonsReadOnly(false); } /****************************************************************************** * Called when the active window has changed. If this window has become the * active window and there is an alarm edit dialog, simulate a modal dialog by * making the alarm edit dialog the active window instead. */ void MessageWin::activeWindowChanged(WId win) { if (mEditDlg && win == winId()) KWindowSystem::activateWindow(mEditDlg->winId()); } /****************************************************************************** * Set or clear the read-only state of the dialog buttons. */ void MessageWin::setButtonsReadOnly(bool ro) { mOkButton->setReadOnly(ro, true); mDeferButton->setReadOnly(ro, true); mEditButton->setReadOnly(ro, true); if (mSilenceButton) mSilenceButton->setReadOnly(ro, true); if (mKMailButton) mKMailButton->setReadOnly(ro, true); mKAlarmButton->setReadOnly(ro, true); } /****************************************************************************** * Set up to disable the defer button when the deferral limit is reached. */ void MessageWin::setDeferralLimit(const KAEvent& event) { mDeferLimit = event.deferralLimit().effectiveKDateTime().toUtc().qDateTime(); MidnightTimer::connect(this, SLOT(checkDeferralLimit())); // check every day mDisableDeferral = false; checkDeferralLimit(); } /****************************************************************************** * Check whether the deferral limit has been reached. * If so, disable the Defer button. * N.B. Ideally, just a single QTimer::singleShot() call would be made to disable * the defer button at the corret time. But for a 32-bit integer, the * milliseconds parameter overflows in about 25 days, so instead a daily * check is done until the day when the deferral limit is reached, followed * by a non-overflowing QTimer::singleShot() call. */ void MessageWin::checkDeferralLimit() { if (!mDeferButton->isEnabled() || !mDeferLimit.isValid()) return; int n = KADateTime::currentLocalDate().daysTo(KADateTime(mDeferLimit, KADateTime::LocalZone).date()); if (n > 0) return; MidnightTimer::disconnect(this, SLOT(checkDeferralLimit())); if (n == 0) { // The deferral limit will be reached today n = QDateTime::currentDateTimeUtc().secsTo(mDeferLimit); if (n > 0) { QTimer::singleShot(n * 1000, this, &MessageWin::checkDeferralLimit); return; } } mDeferButton->setEnabled(false); mDisableDeferral = true; } /****************************************************************************** * Called when the Defer... button is clicked. * Displays the defer message dialog. */ void MessageWin::slotDefer() { mDeferDlg = new DeferAlarmDlg(KADateTime::currentDateTime(Preferences::timeSpec()).addSecs(60), mDateTime.isDateOnly(), false, this); mDeferDlg->setObjectName(QStringLiteral("DeferDlg")); // used by LikeBack mDeferDlg->setDeferMinutes(mDefaultDeferMinutes > 0 ? mDefaultDeferMinutes : Preferences::defaultDeferTime()); mDeferDlg->setLimit(mEvent); if (!Preferences::modalMessages()) lower(); if (mDeferDlg->exec() == QDialog::Accepted) { const DateTime dateTime = mDeferDlg->getDateTime(); const int delayMins = mDeferDlg->deferMinutes(); // Fetch the up-to-date alarm from the calendar. Note that it could have // changed since it was displayed. const KAEvent* event = mEventId.isEmpty() ? nullptr : AlarmCalendar::resources()->event(mEventId); if (event) { // The event still exists in the active calendar - qCDebug(KALARM_LOG) << "Deferring event" << mEventId; + qCDebug(KALARM_LOG) << "MessageWin::slotDefer: Deferring event" << mEventId; KAEvent newev(*event); newev.defer(dateTime, (mAlarmType & KAAlarm::REMINDER_ALARM), true); newev.setDeferDefaultMinutes(delayMins); KAlarm::updateEvent(newev, mDeferDlg, true); if (newev.deferred()) mNoPostAction = true; } else { // Try to retrieve the event from the displaying or archive calendars Akonadi::Collection collection; KAEvent event; bool showEdit, showDefer; if (!retrieveEvent(event, collection, showEdit, showDefer)) { // The event doesn't exist any more !?!, so recurrence data, // flags, and more, have been lost. KAMessageBox::error(this, xi18nc("@info", "Cannot defer alarm:Alarm not found.")); raise(); delete mDeferDlg; mDeferDlg = nullptr; mDeferButton->setEnabled(false); mEditButton->setEnabled(false); return; } - qCDebug(KALARM_LOG) << "Deferring retrieved event" << mEventId; + qCDebug(KALARM_LOG) << "MessageWin::slotDefer: Deferring retrieved event" << mEventId; event.defer(dateTime, (mAlarmType & KAAlarm::REMINDER_ALARM), true); event.setDeferDefaultMinutes(delayMins); event.setCommandError(mCommandError); // Add the event back into the calendar file, retaining its ID // and not updating KOrganizer. KAlarm::addEvent(event, &collection, mDeferDlg, KAlarm::USE_EVENT_ID); if (event.deferred()) mNoPostAction = true; // Finally delete it from the archived calendar now that it has // been reactivated. event.setCategory(CalEvent::ARCHIVED); KAlarm::deleteEvent(event, false); } if (theApp()->wantShowInSystemTray()) { // Alarms are to be displayed only if the system tray icon is running, // so start it if necessary so that the deferred alarm will be shown. theApp()->displayTrayIcon(true); } mNoCloseConfirm = true; // allow window to close without confirmation prompt close(); } else raise(); delete mDeferDlg; mDeferDlg = nullptr; } /****************************************************************************** * Called when the KAlarm icon button in the message window is clicked. * Displays the main window, with the appropriate alarm selected. */ void MessageWin::displayMainWindow() { KAlarm::displayMainWindowSelected(mEventItemId); } /****************************************************************************** * Check whether the specified error message is already displayed for this * alarm, and note that it will now be displayed. * Reply = true if message is already displayed. */ bool MessageWin::haveErrorMessage(unsigned msg) const { if (!mErrorMessages.contains(mEventId)) mErrorMessages.insert(mEventId, 0); const bool result = (mErrorMessages[mEventId] & msg); mErrorMessages[mEventId] |= msg; return result; } void MessageWin::clearErrorMessage(unsigned msg) const { if (mErrorMessages.contains(mEventId)) { if (mErrorMessages[mEventId] == msg) mErrorMessages.remove(mEventId); else mErrorMessages[mEventId] &= ~msg; } } /****************************************************************************** * Check whether the message window should be modal, i.e. with title bar etc. * Normally this follows the Preferences setting, but if there is a full screen * window displayed, on X11 the message window has to bypass the window manager * in order to display on top of it (which has the side effect that it will have * no window decoration). * * Also find the usable area of the desktop (excluding panel etc.), on the * appropriate screen if there are multiple screens. */ bool MessageWin::getWorkAreaAndModal() { mScreenNumber = -1; const bool modal = Preferences::modalMessages(); #if KDEPIM_HAVE_X11 const QDesktopWidget* desktop = qApp->desktop(); const int numScreens = desktop->numScreens(); if (numScreens > 1) { // There are multiple screens. // Check for any full screen windows, even if they are not the active // window, and try not to show the alarm message their screens. mScreenNumber = desktop->screenNumber(MainWindow::mainMainWindow()); // default = KAlarm's screen if (desktop->isVirtualDesktop()) { // The screens form a single virtual desktop. // Xinerama, for example, uses this scheme. QVector screenTypes(numScreens); QVector screenRects(numScreens); for (int s = 0; s < numScreens; ++s) screenRects[s] = desktop->screenGeometry(s); const FullScreenType full = findFullScreenWindows(screenRects, screenTypes); if (full == NoFullScreen || screenTypes[mScreenNumber] == NoFullScreen) return modal; for (int s = 0; s < numScreens; ++s) { if (screenTypes[s] == NoFullScreen) { // There is no full screen window on this screen mScreenNumber = s; return modal; } } // All screens contain a full screen window: use one without // an active full screen window. for (int s = 0; s < numScreens; ++s) { if (screenTypes[s] == FullScreen) { mScreenNumber = s; return modal; } } } else { // The screens are completely separate from each other. int inactiveScreen = -1; FullScreenType full = haveFullScreenWindow(mScreenNumber); qCDebug(KALARM_LOG)<<"full="< * * 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 "alarmtimewidget.h" #include "buttongroup.h" #include "colourbutton.h" #include "editdlg.h" #include "editdlgtypes.h" #include "fontcolour.h" #include "functions.h" #include "kalarmapp.h" #include "kalocale.h" #include "kamail.h" #include "label.h" #include "latecancel.h" #include "mainwindow.h" #include "messagebox.h" #include "preferences.h" #include "radiobutton.h" #include "recurrenceedit.h" #include "sounddlg.h" #include "soundpicker.h" #include "specialactions.h" #include "stackedwidgets.h" #include "timeedit.h" #include "timespinbox.h" #include "timezonecombo.h" #include "traywindow.h" #include "prefdlg_p.h" #include "prefdlg.h" #include "config-kalarm.h" #include #include using namespace KHolidays; #include #include #include #include #include #include #include #if KDEPIM_HAVE_X11 #include #include #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "kalarm_debug.h" using namespace KCalCore; using namespace KAlarmCal; static const char PREF_DIALOG_NAME[] = "PrefDialog"; // Command strings for executing commands in different types of terminal windows. // %t = window title parameter // %c = command to execute in terminal // %w = command to execute in terminal, with 'sleep 86400' appended // %C = temporary command file to execute in terminal // %W = temporary command file to execute in terminal, with 'sleep 86400' appended static QString xtermCommands[] = { QStringLiteral("xterm -sb -hold -title %t -e %c"), QStringLiteral("konsole --noclose -p tabtitle=%t -e ${SHELL:-sh} -c %c"), QStringLiteral("gnome-terminal -t %t -e %W"), QStringLiteral("eterm --pause -T %t -e %C"), // some systems use eterm... QStringLiteral("Eterm --pause -T %t -e %C"), // while some use Eterm QStringLiteral("rxvt -title %t -e ${SHELL:-sh} -c %w"), QString() // end of list indicator - don't change! }; /*============================================================================= = Class KAlarmPrefDlg =============================================================================*/ KAlarmPrefDlg* KAlarmPrefDlg::mInstance = nullptr; void KAlarmPrefDlg::display() { if (!mInstance) { mInstance = new KAlarmPrefDlg; QSize s; if (KAlarm::readConfigWindowSize(PREF_DIALOG_NAME, s)) mInstance->resize(s); mInstance->show(); } else { #if KDEPIM_HAVE_X11 KWindowInfo info = KWindowInfo(mInstance->winId(), NET::WMGeometry | NET::WMDesktop); KWindowSystem::setCurrentDesktop(info.desktop()); #endif mInstance->setWindowState(mInstance->windowState() & ~Qt::WindowMinimized); // un-minimize it if necessary mInstance->raise(); mInstance->activateWindow(); } } KAlarmPrefDlg::KAlarmPrefDlg() : KPageDialog(), mShown(false) { setAttribute(Qt::WA_DeleteOnClose); setObjectName(QStringLiteral("PrefDlg")); // used by LikeBack setWindowTitle(i18nc("@title:window", "Configure")); setStandardButtons(QDialogButtonBox::Ok | QDialogButtonBox::Cancel | QDialogButtonBox::Help | QDialogButtonBox::RestoreDefaults | QDialogButtonBox::Apply); button(QDialogButtonBox::Ok)->setDefault(true); setFaceType(List); mTabScrollGroup = new StackedScrollGroup(this, this); mMiscPage = new MiscPrefTab(mTabScrollGroup); mMiscPageItem = new KPageWidgetItem(mMiscPage, i18nc("@title:tab General preferences", "General")); mMiscPageItem->setHeader(i18nc("@title General preferences", "General")); mMiscPageItem->setIcon(QIcon::fromTheme(QStringLiteral("preferences-other"))); addPage(mMiscPageItem); mTimePage = new TimePrefTab(mTabScrollGroup); mTimePageItem = new KPageWidgetItem(mTimePage, i18nc("@title:tab", "Time & Date")); mTimePageItem->setHeader(i18nc("@title", "Time and Date")); mTimePageItem->setIcon(QIcon::fromTheme(QStringLiteral("preferences-system-time"))); addPage(mTimePageItem); mStorePage = new StorePrefTab(mTabScrollGroup); mStorePageItem = new KPageWidgetItem(mStorePage, i18nc("@title:tab", "Storage")); mStorePageItem->setHeader(i18nc("@title", "Alarm Storage")); mStorePageItem->setIcon(QIcon::fromTheme(QStringLiteral("system-file-manager"))); addPage(mStorePageItem); mEmailPage = new EmailPrefTab(mTabScrollGroup); mEmailPageItem = new KPageWidgetItem(mEmailPage, i18nc("@title:tab Email preferences", "Email")); mEmailPageItem->setHeader(i18nc("@title", "Email Alarm Settings")); mEmailPageItem->setIcon(QIcon::fromTheme(QStringLiteral("internet-mail"))); addPage(mEmailPageItem); mViewPage = new ViewPrefTab(mTabScrollGroup); mViewPageItem = new KPageWidgetItem(mViewPage, i18nc("@title:tab", "View")); mViewPageItem->setHeader(i18nc("@title", "View Settings")); mViewPageItem->setIcon(QIcon::fromTheme(QStringLiteral("preferences-desktop-theme"))); addPage(mViewPageItem); mEditPage = new EditPrefTab(mTabScrollGroup); mEditPageItem = new KPageWidgetItem(mEditPage, i18nc("@title:tab", "Edit")); mEditPageItem->setHeader(i18nc("@title", "Default Alarm Edit Settings")); mEditPageItem->setIcon(QIcon::fromTheme(QStringLiteral("document-properties"))); addPage(mEditPageItem); connect(button(QDialogButtonBox::Ok), &QAbstractButton::clicked, this, &KAlarmPrefDlg::slotOk); connect(button(QDialogButtonBox::Cancel), &QAbstractButton::clicked, this, &KAlarmPrefDlg::slotCancel); connect(button(QDialogButtonBox::Apply), &QAbstractButton::clicked, this, &KAlarmPrefDlg::slotApply); connect(button(QDialogButtonBox::RestoreDefaults), &QAbstractButton::clicked, this, &KAlarmPrefDlg::slotDefault); connect(button(QDialogButtonBox::Help), &QAbstractButton::clicked, this, &KAlarmPrefDlg::slotHelp); restore(false); adjustSize(); } KAlarmPrefDlg::~KAlarmPrefDlg() { mInstance = nullptr; } void KAlarmPrefDlg::slotHelp() { KHelpClient::invokeHelp(QStringLiteral("preferences")); } // Apply the preferences that are currently selected void KAlarmPrefDlg::slotApply() { - qCDebug(KALARM_LOG); + qCDebug(KALARM_LOG) << "KAlarmPrefDlg::slotApply"; QString errmsg = mEmailPage->validate(); if (!errmsg.isEmpty()) { setCurrentPage(mEmailPageItem); if (KAMessageBox::warningYesNo(this, errmsg) != KMessageBox::Yes) { mValid = false; return; } } errmsg = mEditPage->validate(); if (!errmsg.isEmpty()) { setCurrentPage(mEditPageItem); KAMessageBox::sorry(this, errmsg); mValid = false; return; } mValid = true; mEmailPage->apply(false); mViewPage->apply(false); mEditPage->apply(false); mStorePage->apply(false); mTimePage->apply(false); mMiscPage->apply(false); Preferences::self()->save(); } // Apply the preferences that are currently selected void KAlarmPrefDlg::slotOk() { - qCDebug(KALARM_LOG); + qCDebug(KALARM_LOG) << "KAlarmPrefDlg::slotOk"; mValid = true; slotApply(); if (mValid) QDialog::accept(); } // Discard the current preferences and close the dialog void KAlarmPrefDlg::slotCancel() { - qCDebug(KALARM_LOG); + qCDebug(KALARM_LOG) << "KAlarmPrefDlg::slotCancel"; restore(false); KPageDialog::reject(); } // Reset all controls to the application defaults void KAlarmPrefDlg::slotDefault() { switch (KAMessageBox::questionYesNoCancel(this, i18nc("@info", "Reset all tabs to their default values, or only reset the current tab?"), QString(), KGuiItem(i18nc("@action:button Reset ALL tabs", "&All")), KGuiItem(i18nc("@action:button Reset the CURRENT tab", "C&urrent")))) { case KMessageBox::Yes: restore(true); // restore all tabs break; case KMessageBox::No: Preferences::self()->useDefaults(true); static_cast(currentPage()->widget())->restore(true, false); Preferences::self()->useDefaults(false); break; default: break; } } // Discard the current preferences and use the present ones void KAlarmPrefDlg::restore(bool defaults) { - qCDebug(KALARM_LOG) << (defaults ? "defaults" : ""); + qCDebug(KALARM_LOG) << "KAlarmPrefDlg::restore:" << (defaults ? "defaults" : ""); if (defaults) Preferences::self()->useDefaults(true); mEmailPage->restore(defaults, true); mViewPage->restore(defaults, true); mEditPage->restore(defaults, true); mStorePage->restore(defaults, true); mTimePage->restore(defaults, true); mMiscPage->restore(defaults, true); if (defaults) Preferences::self()->useDefaults(false); } /****************************************************************************** * Return the minimum size for the dialog. * If the minimum size would be too high to fit the desktop, the tab contents * are made scrollable. */ QSize KAlarmPrefDlg::minimumSizeHint() const { if (!mTabScrollGroup->sized()) { QSize s = mTabScrollGroup->adjustSize(); if (s.isValid()) { if (mTabScrollGroup->heightReduction()) { s = QSize(s.width(), s.height() - mTabScrollGroup->heightReduction()); const_cast(this)->resize(s); } return s; } } return KPageDialog::minimumSizeHint(); } void KAlarmPrefDlg::showEvent(QShowEvent* e) { KPageDialog::showEvent(e); if (!mShown) { mTabScrollGroup->adjustSize(true); mShown = true; } } /****************************************************************************** * Called when the dialog's size has changed. * Records the new size in the config file. */ void KAlarmPrefDlg::resizeEvent(QResizeEvent* re) { if (isVisible()) KAlarm::writeConfigWindowSize(PREF_DIALOG_NAME, re->size()); KPageDialog::resizeEvent(re); } /*============================================================================= = Class PrefsTabBase =============================================================================*/ int PrefsTabBase::mIndentWidth = 0; PrefsTabBase::PrefsTabBase(StackedScrollGroup* scrollGroup) : StackedScrollWidget(scrollGroup), mLabelsAligned(false) { QFrame* topWidget = new QFrame(this); setWidget(topWidget); mTopLayout = new QVBoxLayout(topWidget); mTopLayout->setMargin(0); mTopLayout->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); if (!mIndentWidth) { QRadioButton radio(this); QStyleOptionButton opt; opt.initFrom(&radio); mIndentWidth = style()->subElementRect(QStyle::SE_RadioButtonIndicator, &opt).width(); } } void PrefsTabBase::apply(bool syncToDisc) { if (syncToDisc) Preferences::self()->save(); } void PrefsTabBase::addAlignedLabel(QLabel* label) { mLabels += label; } void PrefsTabBase::showEvent(QShowEvent*) { if (!mLabelsAligned) { int wid = 0; int i; int end = mLabels.count(); QList xpos; for (i = 0; i < end; ++i) { int x = mLabels[i]->mapTo(this, QPoint(0, 0)).x(); xpos += x; int w = x + mLabels[i]->sizeHint().width(); if (w > wid) wid = w; } for (i = 0; i < end; ++i) { mLabels[i]->setFixedWidth(wid - xpos[i]); mLabels[i]->setAlignment(Qt::AlignRight | Qt::AlignVCenter); } mLabelsAligned = true; } } /*============================================================================= = Class MiscPrefTab =============================================================================*/ MiscPrefTab::MiscPrefTab(StackedScrollGroup* scrollGroup) : PrefsTabBase(scrollGroup) { QGroupBox* group = new QGroupBox(i18nc("@title:group", "Run Mode")); topLayout()->addWidget(group); QVBoxLayout* vlayout = new QVBoxLayout(group); vlayout->setMargin(style()->pixelMetric(QStyle::PM_DefaultChildMargin)); vlayout->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); // Start at login mAutoStart = new QCheckBox(i18nc("@option:check", "Start at login"), group); connect(mAutoStart, &QAbstractButton::clicked, this, &MiscPrefTab::slotAutostartClicked); mAutoStart->setWhatsThis(xi18nc("@info:whatsthis", "Automatically start KAlarm whenever you start KDE." "This option should always be checked unless you intend to discontinue use of KAlarm.")); vlayout->addWidget(mAutoStart, 0, Qt::AlignLeft); mQuitWarn = new QCheckBox(i18nc("@option:check", "Warn before quitting"), group); mQuitWarn->setWhatsThis(xi18nc("@info:whatsthis", "Check to display a warning prompt before quitting KAlarm.")); vlayout->addWidget(mQuitWarn, 0, Qt::AlignLeft); // Confirm alarm deletion? QWidget* widget = new QWidget; // this is for consistent left alignment topLayout()->addWidget(widget); QHBoxLayout* hbox = new QHBoxLayout(widget); hbox->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); mConfirmAlarmDeletion = new QCheckBox(i18nc("@option:check", "Confirm alarm deletions")); mConfirmAlarmDeletion->setMinimumSize(mConfirmAlarmDeletion->sizeHint()); mConfirmAlarmDeletion->setWhatsThis(i18nc("@info:whatsthis", "Check to be prompted for confirmation each time you delete an alarm.")); hbox->addWidget(mConfirmAlarmDeletion); hbox->addStretch(); // left adjust the controls // Default alarm deferral time widget = new QWidget; // this is to control the QWhatsThis text display area topLayout()->addWidget(widget); hbox = new QHBoxLayout(widget); hbox->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); QLabel* label = new QLabel(i18nc("@label:spinbox", "Default defer time interval:")); hbox->addWidget(label); mDefaultDeferTime = new TimeSpinBox(1, 5999); mDefaultDeferTime->setMinimumSize(mDefaultDeferTime->sizeHint()); hbox->addWidget(mDefaultDeferTime); widget->setWhatsThis(i18nc("@info:whatsthis", "Enter the default time interval (hours & minutes) to defer alarms, used by the Defer Alarm dialog.")); label->setBuddy(mDefaultDeferTime); hbox->addStretch(); // left adjust the controls // Terminal window to use for command alarms group = new QGroupBox(i18nc("@title:group", "Terminal for Command Alarms")); group->setWhatsThis(i18nc("@info:whatsthis", "Choose which application to use when a command alarm is executed in a terminal window")); topLayout()->addWidget(group); QGridLayout* grid = new QGridLayout(group); grid->setMargin(style()->pixelMetric(QStyle::PM_DefaultChildMargin)); grid->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); int row = 0; mXtermType = new ButtonGroup(group); int index = 0; mXtermFirst = -1; for (mXtermCount = 0; !xtermCommands[mXtermCount].isNull(); ++mXtermCount) { QString cmd = xtermCommands[mXtermCount]; QStringList args = KShell::splitArgs(cmd); if (args.isEmpty() || QStandardPaths::findExecutable(args[0]).isEmpty()) continue; QRadioButton* radio = new QRadioButton(args[0], group); radio->setMinimumSize(radio->sizeHint()); mXtermType->addButton(radio, mXtermCount); if (mXtermFirst < 0) mXtermFirst = mXtermCount; // note the id of the first button cmd.replace(QStringLiteral("%t"), KAboutData::applicationData().displayName()); cmd.replace(QStringLiteral("%c"), QStringLiteral("")); cmd.replace(QStringLiteral("%w"), QStringLiteral("")); cmd.replace(QStringLiteral("%C"), QStringLiteral("[command]")); cmd.replace(QStringLiteral("%W"), QStringLiteral("[command; sleep]")); radio->setWhatsThis( xi18nc("@info:whatsthis", "Check to execute command alarms in a terminal window by %1", cmd)); grid->addWidget(radio, (row = index/3), index % 3, Qt::AlignLeft); ++index; } // QHBox used here doesn't allow the QLineEdit to expand!? QHBoxLayout* hlayout = new QHBoxLayout(); hlayout->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); grid->addLayout(hlayout, row + 1, 0, 1, 3, Qt::AlignLeft); QRadioButton* radio = new QRadioButton(i18nc("@option:radio Other terminal window command", "Other:"), group); hlayout->addWidget(radio); connect(radio, &QAbstractButton::toggled, this, &MiscPrefTab::slotOtherTerminalToggled); mXtermType->addButton(radio, mXtermCount); if (mXtermFirst < 0) mXtermFirst = mXtermCount; // note the id of the first button mXtermCommand = new QLineEdit(group); mXtermCommand->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Minimum); hlayout->addWidget(mXtermCommand); QString wt = xi18nc("@info:whatsthis", "Enter the full command line needed to execute a command in your chosen terminal window. " "By default the alarm's command string will be appended to what you enter here. " "See the KAlarm Handbook for details of special codes to tailor the command line."); radio->setWhatsThis(wt); mXtermCommand->setWhatsThis(wt); topLayout()->addStretch(); // top adjust the widgets } void MiscPrefTab::restore(bool defaults, bool) { mAutoStart->setChecked(defaults ? true : Preferences::autoStart()); mQuitWarn->setChecked(Preferences::quitWarn()); mConfirmAlarmDeletion->setChecked(Preferences::confirmAlarmDeletion()); mDefaultDeferTime->setValue(Preferences::defaultDeferTime()); QString xtermCmd = Preferences::cmdXTermCommand(); int id = mXtermFirst; if (!xtermCmd.isEmpty()) { for ( ; id < mXtermCount; ++id) { if (mXtermType->find(id) && xtermCmd == xtermCommands[id]) break; } } mXtermType->setButton(id); mXtermCommand->setEnabled(id == mXtermCount); mXtermCommand->setText(id == mXtermCount ? xtermCmd : QString()); } void MiscPrefTab::apply(bool syncToDisc) { // First validate anything entered in Other X-terminal command int xtermID = mXtermType->selectedId(); if (xtermID >= mXtermCount) { QString cmd = mXtermCommand->text(); if (cmd.isEmpty()) xtermID = -1; // 'Other' is only acceptable if it's non-blank else { QStringList args = KShell::splitArgs(cmd); cmd = args.isEmpty() ? QString() : args[0]; if (QStandardPaths::findExecutable(cmd).isEmpty()) { mXtermCommand->setFocus(); if (KAMessageBox::warningContinueCancel(topLayout()->parentWidget(), xi18nc("@info", "Command to invoke terminal window not found: %1", cmd)) != KMessageBox::Continue) return; } } } if (xtermID < 0) { xtermID = mXtermFirst; mXtermType->setButton(mXtermFirst); } if (mQuitWarn->isEnabled()) { bool b = mQuitWarn->isChecked(); if (b != Preferences::quitWarn()) Preferences::setQuitWarn(b); } bool b = mAutoStart->isChecked(); if (b != Preferences::autoStart()) { Preferences::setAutoStart(b); Preferences::setAskAutoStart(true); // cancel any start-at-login prompt suppression if (b) Preferences::setNoAutoStart(false); Preferences::setAutoStartChangedByUser(true); // prevent prompting the user on quit, about start-at-login } b = mConfirmAlarmDeletion->isChecked(); if (b != Preferences::confirmAlarmDeletion()) Preferences::setConfirmAlarmDeletion(b); int i = mDefaultDeferTime->value(); if (i != Preferences::defaultDeferTime()) Preferences::setDefaultDeferTime(i); QString text = (xtermID < mXtermCount) ? xtermCommands[xtermID] : mXtermCommand->text(); if (text != Preferences::cmdXTermCommand()) Preferences::setCmdXTermCommand(text); PrefsTabBase::apply(syncToDisc); } void MiscPrefTab::slotAutostartClicked() { if (!mAutoStart->isChecked() && KAMessageBox::warningYesNo(topLayout()->parentWidget(), xi18nc("@info", "You should not uncheck this option unless you intend to discontinue use of KAlarm"), QString(), KStandardGuiItem::cont(), KStandardGuiItem::cancel() ) != KMessageBox::Yes) mAutoStart->setChecked(true); } void MiscPrefTab::slotOtherTerminalToggled(bool on) { mXtermCommand->setEnabled(on); } /*============================================================================= = Class TimePrefTab =============================================================================*/ TimePrefTab::TimePrefTab(StackedScrollGroup* scrollGroup) : PrefsTabBase(scrollGroup) { // Default time zone QHBoxLayout* itemBox = new QHBoxLayout(); itemBox->setMargin(0); qobject_cast(topLayout())->addLayout(itemBox); QWidget* widget = new QWidget; // this is to control the QWhatsThis text display area itemBox->addWidget(widget); QHBoxLayout* box = new QHBoxLayout(widget); box->setMargin(0); box->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); QLabel* label = new QLabel(i18nc("@label:listbox", "Time zone:")); box->addWidget(label); addAlignedLabel(label); mTimeZone = new TimeZoneCombo(widget); mTimeZone->setMaxVisibleItems(15); box->addWidget(mTimeZone); widget->setWhatsThis(xi18nc("@info:whatsthis", "Select the time zone which KAlarm should use " "as its default for displaying and entering dates and times.")); label->setBuddy(mTimeZone); itemBox->addStretch(); // Holiday region itemBox = new QHBoxLayout(); itemBox->setMargin(0); qobject_cast(topLayout())->addLayout(itemBox); widget = new QWidget; // this is to control the QWhatsThis text display area itemBox->addWidget(widget); box = new QHBoxLayout(widget); box->setMargin(0); box->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); label = new QLabel(i18nc("@label:listbox", "Holiday region:")); addAlignedLabel(label); box->addWidget(label); mHolidays = new QComboBox(); mHolidays->setSizeAdjustPolicy(QComboBox::AdjustToContentsOnFirstShow); box->addWidget(mHolidays); itemBox->addStretch(); label->setBuddy(mHolidays); widget->setWhatsThis(i18nc("@info:whatsthis", "Select which holiday region to use")); const QStringList regions = HolidayRegion::regionCodes(); QMap regionsMap; foreach (const QString& regionCode, regions) { const QString name = HolidayRegion::name(regionCode); const QString languageName = QLocale::languageToString(QLocale(HolidayRegion::languageCode(regionCode)).language()); const QString label = languageName.isEmpty() ? name : i18nc("Holiday region, region language", "%1 (%2)", name, languageName); regionsMap.insert(label, regionCode); } mHolidays->addItem(i18nc("No holiday region", "None"), QString()); for (QMapIterator it(regionsMap); it.hasNext(); ) { it.next(); mHolidays->addItem(it.key(), it.value()); } // Start-of-day time itemBox = new QHBoxLayout(); itemBox->setMargin(0); qobject_cast(topLayout())->addLayout(itemBox); widget = new QWidget; // this is to control the QWhatsThis text display area itemBox->addWidget(widget); box = new QHBoxLayout(widget); box->setMargin(0); box->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); label = new QLabel(i18nc("@label:spinbox", "Start of day for date-only alarms:")); addAlignedLabel(label); box->addWidget(label); mStartOfDay = new TimeEdit(); box->addWidget(mStartOfDay); label->setBuddy(mStartOfDay); widget->setWhatsThis(xi18nc("@info:whatsthis", "The earliest time of day at which a date-only alarm will be triggered." "%1", TimeSpinBox::shiftWhatsThis())); itemBox->addStretch(); // Working hours QGroupBox* group = new QGroupBox(i18nc("@title:group", "Working Hours")); topLayout()->addWidget(group); QBoxLayout* layout = new QVBoxLayout(group); layout->setMargin(style()->pixelMetric(QStyle::PM_DefaultChildMargin)); layout->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); QWidget* daybox = new QWidget(group); // this is to control the QWhatsThis text display area layout->addWidget(daybox); QGridLayout* wgrid = new QGridLayout(daybox); wgrid->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); const QLocale locale; for (int i = 0; i < 7; ++i) { int day = KAlarm::localeDayInWeek_to_weekDay(i); mWorkDays[i] = new QCheckBox(locale.dayName(day), daybox); wgrid->addWidget(mWorkDays[i], i/3, i%3, Qt::AlignLeft); } daybox->setWhatsThis(i18nc("@info:whatsthis", "Check the days in the week which are work days")); itemBox = new QHBoxLayout(); itemBox->setMargin(0); layout->addLayout(itemBox); widget = new QWidget; // this is to control the QWhatsThis text display area itemBox->addWidget(widget); box = new QHBoxLayout(widget); box->setMargin(0); box->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); label = new QLabel(i18nc("@label:spinbox", "Daily start time:")); addAlignedLabel(label); box->addWidget(label); mWorkStart = new TimeEdit(); box->addWidget(mWorkStart); label->setBuddy(mWorkStart); widget->setWhatsThis(xi18nc("@info:whatsthis", "Enter the start time of the working day." "%1", TimeSpinBox::shiftWhatsThis())); itemBox->addStretch(); itemBox = new QHBoxLayout(); itemBox->setMargin(0); layout->addLayout(itemBox); widget = new QWidget; // this is to control the QWhatsThis text display area itemBox->addWidget(widget); box = new QHBoxLayout(widget); box->setMargin(0); box->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); label = new QLabel(i18nc("@label:spinbox", "Daily end time:")); addAlignedLabel(label); box->addWidget(label); mWorkEnd = new TimeEdit(); box->addWidget(mWorkEnd); label->setBuddy(mWorkEnd); widget->setWhatsThis(xi18nc("@info:whatsthis", "Enter the end time of the working day." "%1", TimeSpinBox::shiftWhatsThis())); itemBox->addStretch(); // KOrganizer event duration group = new QGroupBox(i18nc("@title:group", "KOrganizer")); topLayout()->addWidget(group); layout = new QVBoxLayout(group); layout->setMargin(style()->pixelMetric(QStyle::PM_DefaultChildMargin)); layout->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); widget = new QWidget; // this is to control the QWhatsThis text display area layout->addWidget(widget); box = new QHBoxLayout(widget); box->setMargin(0); box->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); label = new QLabel(i18nc("@label:spinbox", "KOrganizer event duration:")); addAlignedLabel(label); box->addWidget(label); mKOrgEventDuration = new TimeSpinBox(0, 5999); mKOrgEventDuration->setMinimumSize(mKOrgEventDuration->sizeHint()); box->addWidget(mKOrgEventDuration); widget->setWhatsThis(xi18nc("@info:whatsthis", "Enter the event duration in hours and minutes, for alarms which are copied to KOrganizer." "%1", TimeSpinBox::shiftWhatsThis())); label->setBuddy(mKOrgEventDuration); box->addStretch(); // left adjust the controls topLayout()->addStretch(); // top adjust the widgets } void TimePrefTab::restore(bool, bool) { KADateTime::Spec timeSpec = Preferences::timeSpec(); mTimeZone->setTimeZone(timeSpec.type() == KADateTime::TimeZone ? timeSpec.timeZone() : QTimeZone()); int i = Preferences::holidays().isValid() ? mHolidays->findData(Preferences::holidays().regionCode()) : 0; mHolidays->setCurrentIndex(i); mStartOfDay->setValue(Preferences::startOfDay()); mWorkStart->setValue(Preferences::workDayStart()); mWorkEnd->setValue(Preferences::workDayEnd()); QBitArray days = Preferences::workDays(); for (int i = 0; i < 7; ++i) { bool x = days.testBit(KAlarm::localeDayInWeek_to_weekDay(i) - 1); mWorkDays[i]->setChecked(x); } mKOrgEventDuration->setValue(Preferences::kOrgEventDuration()); } void TimePrefTab::apply(bool syncToDisc) { Preferences::setTimeSpec(mTimeZone->timeZone()); QString hol = mHolidays->itemData(mHolidays->currentIndex()).toString(); if (hol != Preferences::holidays().regionCode()) Preferences::setHolidayRegion(hol); int t = mStartOfDay->value(); QTime sodt(t/60, t%60, 0); if (sodt != Preferences::startOfDay()) Preferences::setStartOfDay(sodt); t = mWorkStart->value(); Preferences::setWorkDayStart(QTime(t/60, t%60, 0)); t = mWorkEnd->value(); Preferences::setWorkDayEnd(QTime(t/60, t%60, 0)); QBitArray workDays(7); for (int i = 0; i < 7; ++i) if (mWorkDays[i]->isChecked()) workDays.setBit(KAlarm::localeDayInWeek_to_weekDay(i) - 1, 1); Preferences::setWorkDays(workDays); Preferences::setKOrgEventDuration(mKOrgEventDuration->value()); t = mKOrgEventDuration->value(); if (t != Preferences::kOrgEventDuration()) Preferences::setKOrgEventDuration(t); PrefsTabBase::apply(syncToDisc); } /*============================================================================= = Class StorePrefTab =============================================================================*/ StorePrefTab::StorePrefTab(StackedScrollGroup* scrollGroup) : PrefsTabBase(scrollGroup), mCheckKeepChanges(false) { // Which resource to save to QGroupBox* group = new QGroupBox(i18nc("@title:group", "New Alarms && Templates")); topLayout()->addWidget(group); QButtonGroup* bgroup = new QButtonGroup(group); QBoxLayout* layout = new QVBoxLayout(group); layout->setMargin(style()->pixelMetric(QStyle::PM_DefaultChildMargin)); layout->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); mDefaultResource = new QRadioButton(i18nc("@option:radio", "Store in default calendar"), group); bgroup->addButton(mDefaultResource); mDefaultResource->setWhatsThis(i18nc("@info:whatsthis", "Add all new alarms and alarm templates to the default calendars, without prompting.")); layout->addWidget(mDefaultResource, 0, Qt::AlignLeft); mAskResource = new QRadioButton(i18nc("@option:radio", "Prompt for which calendar to store in"), group); bgroup->addButton(mAskResource); mAskResource->setWhatsThis(xi18nc("@info:whatsthis", "When saving a new alarm or alarm template, prompt for which calendar to store it in, if there is more than one active calendar." "Note that archived alarms are always stored in the default archived alarm calendar.")); layout->addWidget(mAskResource, 0, Qt::AlignLeft); // Archived alarms group = new QGroupBox(i18nc("@title:group", "Archived Alarms")); topLayout()->addWidget(group); QGridLayout* grid = new QGridLayout(group); grid->setMargin(style()->pixelMetric(QStyle::PM_DefaultChildMargin)); grid->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); grid->setColumnStretch(1, 1); grid->setColumnMinimumWidth(0, indentWidth()); mKeepArchived = new QCheckBox(i18nc("@option:check", "Keep alarms after expiry"), group); connect(mKeepArchived, &QAbstractButton::toggled, this, &StorePrefTab::slotArchivedToggled); mKeepArchived->setWhatsThis( i18nc("@info:whatsthis", "Check to archive alarms after expiry or deletion (except deleted alarms which were never triggered).")); grid->addWidget(mKeepArchived, 0, 0, 1, 2, Qt::AlignLeft); QWidget* widget = new QWidget; QHBoxLayout* box = new QHBoxLayout(widget); box->setMargin(0); box->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); mPurgeArchived = new QCheckBox(i18nc("@option:check", "Discard archived alarms after:")); mPurgeArchived->setMinimumSize(mPurgeArchived->sizeHint()); box->addWidget(mPurgeArchived); connect(mPurgeArchived, &QAbstractButton::toggled, this, &StorePrefTab::slotArchivedToggled); mPurgeAfter = new SpinBox(); mPurgeAfter->setMinimum(1); mPurgeAfter->setSingleShiftStep(10); mPurgeAfter->setMinimumSize(mPurgeAfter->sizeHint()); box->addWidget(mPurgeAfter); mPurgeAfterLabel = new QLabel(i18nc("@label Time unit for user-entered number", "days")); mPurgeAfterLabel->setMinimumSize(mPurgeAfterLabel->sizeHint()); mPurgeAfterLabel->setBuddy(mPurgeAfter); box->addWidget(mPurgeAfterLabel); widget->setWhatsThis(i18nc("@info:whatsthis", "Uncheck to store archived alarms indefinitely. Check to enter how long archived alarms should be stored.")); grid->addWidget(widget, 1, 1, Qt::AlignLeft); mClearArchived = new QPushButton(i18nc("@action:button", "Clear Archived Alarms"), group); connect(mClearArchived, &QAbstractButton::clicked, this, &StorePrefTab::slotClearArchived); mClearArchived->setWhatsThis((CollectionControlModel::enabledCollections(CalEvent::ARCHIVED, false).count() <= 1) ? i18nc("@info:whatsthis", "Delete all existing archived alarms.") : i18nc("@info:whatsthis", "Delete all existing archived alarms (from the default archived alarm calendar only).")); grid->addWidget(mClearArchived, 2, 1, Qt::AlignLeft); topLayout()->addStretch(); // top adjust the widgets } void StorePrefTab::restore(bool defaults, bool) { mCheckKeepChanges = defaults; if (Preferences::askResource()) mAskResource->setChecked(true); else mDefaultResource->setChecked(true); int keepDays = Preferences::archivedKeepDays(); if (!defaults) mOldKeepArchived = keepDays; setArchivedControls(keepDays); mCheckKeepChanges = true; } void StorePrefTab::apply(bool syncToDisc) { bool b = mAskResource->isChecked(); if (b != Preferences::askResource()) Preferences::setAskResource(mAskResource->isChecked()); int days = !mKeepArchived->isChecked() ? 0 : mPurgeArchived->isChecked() ? mPurgeAfter->value() : -1; if (days != Preferences::archivedKeepDays()) Preferences::setArchivedKeepDays(days); PrefsTabBase::apply(syncToDisc); } void StorePrefTab::setArchivedControls(int purgeDays) { mKeepArchived->setChecked(purgeDays); mPurgeArchived->setChecked(purgeDays > 0); mPurgeAfter->setValue(purgeDays > 0 ? purgeDays : 0); slotArchivedToggled(true); } void StorePrefTab::slotArchivedToggled(bool) { bool keep = mKeepArchived->isChecked(); if (keep && !mOldKeepArchived && mCheckKeepChanges && !CollectionControlModel::getStandard(CalEvent::ARCHIVED).isValid()) { KAMessageBox::sorry(topLayout()->parentWidget(), xi18nc("@info", "A default calendar is required in order to archive alarms, but none is currently enabled." "If you wish to keep expired alarms, please first use the calendars view to select a default " "archived alarms calendar.")); mKeepArchived->setChecked(false); return; } mOldKeepArchived = keep; mPurgeArchived->setEnabled(keep); mPurgeAfter->setEnabled(keep && mPurgeArchived->isChecked()); mPurgeAfterLabel->setEnabled(keep); mClearArchived->setEnabled(keep); } void StorePrefTab::slotClearArchived() { bool single = CollectionControlModel::enabledCollections(CalEvent::ARCHIVED, false).count() <= 1; if (KAMessageBox::warningContinueCancel(topLayout()->parentWidget(), single ? i18nc("@info", "Do you really want to delete all archived alarms?") : i18nc("@info", "Do you really want to delete all alarms in the default archived alarm calendar?")) != KMessageBox::Continue) return; theApp()->purgeAll(); } /*============================================================================= = Class EmailPrefTab =============================================================================*/ EmailPrefTab::EmailPrefTab(StackedScrollGroup* scrollGroup) : PrefsTabBase(scrollGroup), mAddressChanged(false), mBccAddressChanged(false) { QWidget* widget = new QWidget; topLayout()->addWidget(widget); QHBoxLayout* box = new QHBoxLayout(widget); box->setSpacing(2 * style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); QLabel* label = new QLabel(i18nc("@label", "Email client:")); box->addWidget(label); mEmailClient = new ButtonGroup(widget); QString kmailOption = i18nc("@option:radio", "KMail"); QString sendmailOption = i18nc("@option:radio", "Sendmail"); mKMailButton = new RadioButton(kmailOption); mKMailButton->setMinimumSize(mKMailButton->sizeHint()); box->addWidget(mKMailButton); mEmailClient->addButton(mKMailButton, Preferences::kmail); mSendmailButton = new RadioButton(sendmailOption); mSendmailButton->setMinimumSize(mSendmailButton->sizeHint()); box->addWidget(mSendmailButton); mEmailClient->addButton(mSendmailButton, Preferences::sendmail); connect(mEmailClient, &ButtonGroup::buttonSet, this, &EmailPrefTab::slotEmailClientChanged); widget->setWhatsThis(xi18nc("@info:whatsthis", "Choose how to send email when an email alarm is triggered." "%1: The email is sent automatically via KMail. KMail is started first if necessary." "%2: The email is sent automatically. This option will only work if " "your system is configured to use sendmail or a sendmail compatible mail transport agent.", kmailOption, sendmailOption)); widget = new QWidget; // this is to allow left adjustment topLayout()->addWidget(widget); box = new QHBoxLayout(widget); mEmailCopyToKMail = new QCheckBox(xi18nc("@option:check", "Copy sent emails into KMail's %1 folder", KAMail::i18n_sent_mail())); mEmailCopyToKMail->setWhatsThis(xi18nc("@info:whatsthis", "After sending an email, store a copy in KMail's %1 folder", KAMail::i18n_sent_mail())); box->addWidget(mEmailCopyToKMail); box->setStretchFactor(new QWidget(widget), 1); // left adjust the controls widget = new QWidget; // this is to allow left adjustment topLayout()->addWidget(widget); box = new QHBoxLayout(widget); mEmailQueuedNotify = new QCheckBox(i18nc("@option:check", "Notify when remote emails are queued")); mEmailQueuedNotify->setWhatsThis( i18nc("@info:whatsthis", "Display a notification message whenever an email alarm has queued an email for sending to a remote system. " "This could be useful if, for example, you have a dial-up connection, so that you can then ensure that the email is actually transmitted.")); box->addWidget(mEmailQueuedNotify); box->setStretchFactor(new QWidget(widget), 1); // left adjust the controls // Your Email Address group box QGroupBox* group = new QGroupBox(i18nc("@title:group", "Your Email Address")); topLayout()->addWidget(group); QGridLayout* grid = new QGridLayout(group); grid->setMargin(style()->pixelMetric(QStyle::PM_DefaultChildMargin)); grid->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); grid->setColumnStretch(2, 1); // 'From' email address controls ... label = new Label(i18nc("@label 'From' email address", "From:"), group); grid->addWidget(label, 1, 0); mFromAddressGroup = new ButtonGroup(group); connect(mFromAddressGroup, &ButtonGroup::buttonSet, this, &EmailPrefTab::slotFromAddrChanged); // Line edit to enter a 'From' email address mFromAddrButton = new RadioButton(group); mFromAddressGroup->addButton(mFromAddrButton, Preferences::MAIL_FROM_ADDR); label->setBuddy(mFromAddrButton); grid->addWidget(mFromAddrButton, 1, 1); mEmailAddress = new QLineEdit(group); connect(mEmailAddress, &QLineEdit::textChanged, this, &EmailPrefTab::slotAddressChanged); QString whatsThis = i18nc("@info:whatsthis", "Your email address, used to identify you as the sender when sending email alarms."); mFromAddrButton->setWhatsThis(whatsThis); mEmailAddress->setWhatsThis(whatsThis); mFromAddrButton->setFocusWidget(mEmailAddress); grid->addWidget(mEmailAddress, 1, 2); // 'From' email address to be taken from System Settings mFromCCentreButton = new RadioButton(xi18nc("@option:radio", "Use default address from KMail or System Settings"), group); mFromAddressGroup->addButton(mFromCCentreButton, Preferences::MAIL_FROM_SYS_SETTINGS); mFromCCentreButton->setWhatsThis( xi18nc("@info:whatsthis", "Check to use the default email address set in KMail or KDE System Settings, to identify you as the sender when sending email alarms.")); grid->addWidget(mFromCCentreButton, 2, 1, 1, 2, Qt::AlignLeft); // 'From' email address to be picked from KMail's identities when the email alarm is configured mFromKMailButton = new RadioButton(xi18nc("@option:radio", "Use KMail identities"), group); mFromAddressGroup->addButton(mFromKMailButton, Preferences::MAIL_FROM_KMAIL); mFromKMailButton->setWhatsThis( xi18nc("@info:whatsthis", "Check to use KMail's email identities to identify you as the sender when sending email alarms. " "For existing email alarms, KMail's default identity will be used. " "For new email alarms, you will be able to pick which of KMail's identities to use.")); grid->addWidget(mFromKMailButton, 3, 1, 1, 2, Qt::AlignLeft); // 'Bcc' email address controls ... grid->setRowMinimumHeight(4, style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); label = new Label(i18nc("@label 'Bcc' email address", "Bcc:"), group); grid->addWidget(label, 5, 0); mBccAddressGroup = new ButtonGroup(group); connect(mBccAddressGroup, &ButtonGroup::buttonSet, this, &EmailPrefTab::slotBccAddrChanged); // Line edit to enter a 'Bcc' email address mBccAddrButton = new RadioButton(group); mBccAddressGroup->addButton(mBccAddrButton, Preferences::MAIL_FROM_ADDR); label->setBuddy(mBccAddrButton); grid->addWidget(mBccAddrButton, 5, 1); mEmailBccAddress = new QLineEdit(group); whatsThis = xi18nc("@info:whatsthis", "Your email address, used for blind copying email alarms to yourself. " "If you want blind copies to be sent to your account on the computer which KAlarm runs on, you can simply enter your user login name."); mBccAddrButton->setWhatsThis(whatsThis); mEmailBccAddress->setWhatsThis(whatsThis); mBccAddrButton->setFocusWidget(mEmailBccAddress); grid->addWidget(mEmailBccAddress, 5, 2); // 'Bcc' email address to be taken from System Settings mBccCCentreButton = new RadioButton(xi18nc("@option:radio", "Use default address from KMail or System Settings"), group); mBccAddressGroup->addButton(mBccCCentreButton, Preferences::MAIL_FROM_SYS_SETTINGS); mBccCCentreButton->setWhatsThis( xi18nc("@info:whatsthis", "Check to use the default email address set in KMail or KDE System Settings, for blind copying email alarms to yourself.")); grid->addWidget(mBccCCentreButton, 6, 1, 1, 2, Qt::AlignLeft); topLayout()->addStretch(); // top adjust the widgets } void EmailPrefTab::restore(bool defaults, bool) { mEmailClient->setButton(Preferences::emailClient()); mEmailCopyToKMail->setChecked(Preferences::emailCopyToKMail()); setEmailAddress(Preferences::emailFrom(), Preferences::emailAddress()); setEmailBccAddress((Preferences::emailBccFrom() == Preferences::MAIL_FROM_SYS_SETTINGS), Preferences::emailBccAddress()); mEmailQueuedNotify->setChecked(Preferences::emailQueuedNotify()); if (!defaults) mAddressChanged = mBccAddressChanged = false; } void EmailPrefTab::apply(bool syncToDisc) { int client = mEmailClient->selectedId(); if (client >= 0 && static_cast(client) != Preferences::emailClient()) Preferences::setEmailClient(static_cast(client)); bool b = mEmailCopyToKMail->isChecked(); if (b != Preferences::emailCopyToKMail()) Preferences::setEmailCopyToKMail(b); int from = mFromAddressGroup->selectedId(); QString text = mEmailAddress->text().trimmed(); if ((from >= 0 && static_cast(from) != Preferences::emailFrom()) || text != Preferences::emailAddress()) Preferences::setEmailAddress(static_cast(from), text); b = (mBccAddressGroup->checkedButton() == mBccCCentreButton); Preferences::MailFrom bfrom = b ? Preferences::MAIL_FROM_SYS_SETTINGS : Preferences::MAIL_FROM_ADDR;; text = mEmailBccAddress->text().trimmed(); if (bfrom != Preferences::emailBccFrom() || text != Preferences::emailBccAddress()) Preferences::setEmailBccAddress(b, text); b = mEmailQueuedNotify->isChecked(); if (b != Preferences::emailQueuedNotify()) Preferences::setEmailQueuedNotify(mEmailQueuedNotify->isChecked()); PrefsTabBase::apply(syncToDisc); } void EmailPrefTab::setEmailAddress(Preferences::MailFrom from, const QString& address) { mFromAddressGroup->setButton(from); mEmailAddress->setText(from == Preferences::MAIL_FROM_ADDR ? address.trimmed() : QString()); } void EmailPrefTab::setEmailBccAddress(bool useSystemSettings, const QString& address) { mBccAddressGroup->setButton(useSystemSettings ? Preferences::MAIL_FROM_SYS_SETTINGS : Preferences::MAIL_FROM_ADDR); mEmailBccAddress->setText(useSystemSettings ? QString() : address.trimmed()); } void EmailPrefTab::slotEmailClientChanged(QAbstractButton* button) { mEmailCopyToKMail->setEnabled(button == mSendmailButton); } void EmailPrefTab::slotFromAddrChanged(QAbstractButton* button) { mEmailAddress->setEnabled(button == mFromAddrButton); mAddressChanged = true; } void EmailPrefTab::slotBccAddrChanged(QAbstractButton* button) { mEmailBccAddress->setEnabled(button == mBccAddrButton); mBccAddressChanged = true; } QString EmailPrefTab::validate() { if (mAddressChanged) { mAddressChanged = false; QString errmsg = validateAddr(mFromAddressGroup, mEmailAddress, KAMail::i18n_NeedFromEmailAddress()); if (!errmsg.isEmpty()) return errmsg; } if (mBccAddressChanged) { mBccAddressChanged = false; return validateAddr(mBccAddressGroup, mEmailBccAddress, i18nc("@info", "No valid 'Bcc' email address is specified.")); } return QString(); } QString EmailPrefTab::validateAddr(ButtonGroup* group, QLineEdit* addr, const QString& msg) { QString errmsg = xi18nc("@info", "%1Are you sure you want to save your changes?", msg); switch (group->selectedId()) { case Preferences::MAIL_FROM_SYS_SETTINGS: if (!KAMail::controlCentreAddress().isEmpty()) return QString(); errmsg = xi18nc("@info", "No default email address is currently set in KMail or KDE System Settings. %1", errmsg); break; case Preferences::MAIL_FROM_KMAIL: if (Identities::identitiesExist()) return QString(); errmsg = xi18nc("@info", "No KMail identities currently exist. %1", errmsg); break; case Preferences::MAIL_FROM_ADDR: if (!addr->text().trimmed().isEmpty()) return QString(); break; } return errmsg; } /*============================================================================= = Class EditPrefTab =============================================================================*/ EditPrefTab::EditPrefTab(StackedScrollGroup* scrollGroup) : PrefsTabBase(scrollGroup) { KLocalizedString defsetting = kxi18nc("@info:whatsthis", "The default setting for %1 in the alarm edit dialog."); mTabs = new QTabWidget(); topLayout()->addWidget(mTabs); StackedGroupT* tabgroup = new StackedGroupT(mTabs); StackedWidgetT* topGeneral = new StackedWidgetT(tabgroup); QVBoxLayout* tgLayout = new QVBoxLayout(topGeneral); mTabGeneral = mTabs->addTab(topGeneral, i18nc("@title:tab", "General")); StackedWidgetT* topTypes = new StackedWidgetT(tabgroup); QVBoxLayout* ttLayout = new QVBoxLayout(topTypes); mTabTypes = mTabs->addTab(topTypes, i18nc("@title:tab", "Alarm Types")); StackedWidgetT* topFontColour = new StackedWidgetT(tabgroup); QVBoxLayout* tfLayout = new QVBoxLayout(topFontColour); mTabFontColour = mTabs->addTab(topFontColour, i18nc("@title:tab", "Font && Color")); // MISCELLANEOUS // Show in KOrganizer mCopyToKOrganizer = new QCheckBox(EditAlarmDlg::i18n_chk_ShowInKOrganizer()); mCopyToKOrganizer->setMinimumSize(mCopyToKOrganizer->sizeHint()); mCopyToKOrganizer->setWhatsThis(defsetting.subs(EditAlarmDlg::i18n_chk_ShowInKOrganizer()).toString()); tgLayout->addWidget(mCopyToKOrganizer); // Late cancellation QWidget* widget = new QWidget; tgLayout->addWidget(widget); QHBoxLayout* box = new QHBoxLayout(widget); box->setMargin(0); box->setSpacing(0); mLateCancel = new QCheckBox(LateCancelSelector::i18n_chk_CancelIfLate()); mLateCancel->setMinimumSize(mLateCancel->sizeHint()); mLateCancel->setWhatsThis(defsetting.subs(LateCancelSelector::i18n_chk_CancelIfLate()).toString()); box->addWidget(mLateCancel); // Recurrence widget = new QWidget; // this is to control the QWhatsThis text display area tgLayout->addWidget(widget); box = new QHBoxLayout(widget); box->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); box->setMargin(0); QLabel* label = new QLabel(i18nc("@label:listbox", "Recurrence:")); box->addWidget(label); mRecurPeriod = new ComboBox(); mRecurPeriod->addItem(RecurrenceEdit::i18n_combo_NoRecur()); mRecurPeriod->addItem(RecurrenceEdit::i18n_combo_AtLogin()); mRecurPeriod->addItem(RecurrenceEdit::i18n_combo_HourlyMinutely()); mRecurPeriod->addItem(RecurrenceEdit::i18n_combo_Daily()); mRecurPeriod->addItem(RecurrenceEdit::i18n_combo_Weekly()); mRecurPeriod->addItem(RecurrenceEdit::i18n_combo_Monthly()); mRecurPeriod->addItem(RecurrenceEdit::i18n_combo_Yearly()); box->addWidget(mRecurPeriod); box->addStretch(); label->setBuddy(mRecurPeriod); widget->setWhatsThis(i18nc("@info:whatsthis", "The default setting for the recurrence rule in the alarm edit dialog.")); // How to handle February 29th in yearly recurrences QWidget* febBox = new QWidget; // this is to control the QWhatsThis text display area tgLayout->addWidget(febBox); QVBoxLayout* vbox = new QVBoxLayout(febBox); vbox->setMargin(0); vbox->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); label = new QLabel(i18nc("@label", "In non-leap years, repeat yearly February 29th alarms on:")); label->setAlignment(Qt::AlignLeft); label->setWordWrap(true); vbox->addWidget(label); box = new QHBoxLayout(); vbox->addLayout(box); vbox->setMargin(0); box->setSpacing(2 * style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); mFeb29 = new ButtonGroup(febBox); widget = new QWidget(); widget->setFixedWidth(3 * style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); box->addWidget(widget); QRadioButton* radio = new QRadioButton(i18nc("@option:radio", "February 2&8th")); radio->setMinimumSize(radio->sizeHint()); box->addWidget(radio); mFeb29->addButton(radio, Preferences::Feb29_Feb28); radio = new QRadioButton(i18nc("@option:radio", "March &1st")); radio->setMinimumSize(radio->sizeHint()); box->addWidget(radio); mFeb29->addButton(radio, Preferences::Feb29_Mar1); radio = new QRadioButton(i18nc("@option:radio", "Do not repeat")); radio->setMinimumSize(radio->sizeHint()); box->addWidget(radio); mFeb29->addButton(radio, Preferences::Feb29_None); febBox->setWhatsThis(xi18nc("@info:whatsthis", "For yearly recurrences, choose what date, if any, alarms due on February 29th should occur in non-leap years." "The next scheduled occurrence of existing alarms is not re-evaluated when you change this setting.")); tgLayout->addStretch(); // top adjust the widgets // DISPLAY ALARMS QGroupBox* group = new QGroupBox(i18nc("@title:group", "Display Alarms")); ttLayout->addWidget(group); QVBoxLayout* vlayout = new QVBoxLayout(group); vlayout->setMargin(style()->pixelMetric(QStyle::PM_DefaultChildMargin)); vlayout->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); mConfirmAck = new QCheckBox(EditDisplayAlarmDlg::i18n_chk_ConfirmAck()); mConfirmAck->setMinimumSize(mConfirmAck->sizeHint()); mConfirmAck->setWhatsThis(defsetting.subs(EditDisplayAlarmDlg::i18n_chk_ConfirmAck()).toString()); vlayout->addWidget(mConfirmAck, 0, Qt::AlignLeft); mAutoClose = new QCheckBox(LateCancelSelector::i18n_chk_AutoCloseWinLC()); mAutoClose->setMinimumSize(mAutoClose->sizeHint()); mAutoClose->setWhatsThis(defsetting.subs(LateCancelSelector::i18n_chk_AutoCloseWin()).toString()); vlayout->addWidget(mAutoClose, 0, Qt::AlignLeft); widget = new QWidget; vlayout->addWidget(widget); box = new QHBoxLayout(widget); box->setMargin(0); box->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); label = new QLabel(i18nc("@label:listbox", "Reminder units:")); box->addWidget(label); mReminderUnits = new QComboBox(); mReminderUnits->addItem(i18nc("@item:inlistbox", "Minutes"), TimePeriod::Minutes); mReminderUnits->addItem(i18nc("@item:inlistbox", "Hours/Minutes"), TimePeriod::HoursMinutes); box->addWidget(mReminderUnits); label->setBuddy(mReminderUnits); widget->setWhatsThis(i18nc("@info:whatsthis", "The default units for the reminder in the alarm edit dialog, for alarms due soon.")); box->addStretch(); // left adjust the control mSpecialActionsButton = new SpecialActionsButton(true); box->addWidget(mSpecialActionsButton); // SOUND QGroupBox* bbox = new QGroupBox(i18nc("@title:group Audio options group", "Sound")); ttLayout->addWidget(bbox); vlayout = new QVBoxLayout(bbox); vlayout->setMargin(style()->pixelMetric(QStyle::PM_DefaultChildMargin)); vlayout->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); QHBoxLayout* hlayout = new QHBoxLayout; hlayout->setMargin(0); vlayout->addLayout(hlayout); mSound = new QComboBox(); mSound->addItem(SoundPicker::i18n_combo_None()); // index 0 mSound->addItem(SoundPicker::i18n_combo_Beep()); // index 1 mSound->addItem(SoundPicker::i18n_combo_File()); // index 2 if (KPIMTextEdit::TextToSpeech::self()->isReady()) mSound->addItem(SoundPicker::i18n_combo_Speak()); // index 3 mSound->setMinimumSize(mSound->sizeHint()); mSound->setWhatsThis(defsetting.subs(SoundPicker::i18n_label_Sound()).toString()); hlayout->addWidget(mSound); hlayout->addStretch(); mSoundRepeat = new QCheckBox(i18nc("@option:check", "Repeat sound file")); mSoundRepeat->setMinimumSize(mSoundRepeat->sizeHint()); mSoundRepeat->setWhatsThis( xi18nc("@info:whatsthis sound file 'Repeat' checkbox", "The default setting for sound file %1 in the alarm edit dialog.", SoundWidget::i18n_chk_Repeat())); hlayout->addWidget(mSoundRepeat); widget = new QWidget; // this is to control the QWhatsThis text display area box = new QHBoxLayout(widget); box->setMargin(0); box->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); mSoundFileLabel = new QLabel(i18nc("@label:textbox", "Sound file:")); box->addWidget(mSoundFileLabel); mSoundFile = new QLineEdit(); box->addWidget(mSoundFile); mSoundFileLabel->setBuddy(mSoundFile); mSoundFileBrowse = new QPushButton(); mSoundFileBrowse->setIcon(QIcon::fromTheme(QStringLiteral("document-open"))); connect(mSoundFileBrowse, &QAbstractButton::clicked, this, &EditPrefTab::slotBrowseSoundFile); mSoundFileBrowse->setToolTip(i18nc("@info:tooltip", "Choose a sound file")); box->addWidget(mSoundFileBrowse); widget->setWhatsThis(i18nc("@info:whatsthis", "Enter the default sound file to use in the alarm edit dialog.")); vlayout->addWidget(widget); // COMMAND ALARMS group = new QGroupBox(i18nc("@title:group", "Command Alarms")); ttLayout->addWidget(group); vlayout = new QVBoxLayout(group); vlayout->setMargin(style()->pixelMetric(QStyle::PM_DefaultChildMargin)); vlayout->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); hlayout = new QHBoxLayout(); hlayout->setMargin(0); vlayout->addLayout(hlayout); mCmdScript = new QCheckBox(EditCommandAlarmDlg::i18n_chk_EnterScript(), group); mCmdScript->setMinimumSize(mCmdScript->sizeHint()); mCmdScript->setWhatsThis(defsetting.subs(EditCommandAlarmDlg::i18n_chk_EnterScript()).toString()); hlayout->addWidget(mCmdScript); hlayout->addStretch(); mCmdXterm = new QCheckBox(EditCommandAlarmDlg::i18n_chk_ExecInTermWindow(), group); mCmdXterm->setMinimumSize(mCmdXterm->sizeHint()); mCmdXterm->setWhatsThis(defsetting.subs(EditCommandAlarmDlg::i18n_radio_ExecInTermWindow()).toString()); hlayout->addWidget(mCmdXterm); // EMAIL ALARMS group = new QGroupBox(i18nc("@title:group", "Email Alarms")); ttLayout->addWidget(group); vlayout = new QVBoxLayout(group); vlayout->setMargin(style()->pixelMetric(QStyle::PM_DefaultChildMargin)); vlayout->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); // BCC email to sender mEmailBcc = new QCheckBox(EditEmailAlarmDlg::i18n_chk_CopyEmailToSelf(), group); mEmailBcc->setMinimumSize(mEmailBcc->sizeHint()); mEmailBcc->setWhatsThis(defsetting.subs(EditEmailAlarmDlg::i18n_chk_CopyEmailToSelf()).toString()); vlayout->addWidget(mEmailBcc, 0, Qt::AlignLeft); ttLayout->addStretch(); // FONT / COLOUR TAB mFontChooser = new FontColourChooser(topFontColour, QStringList(), i18nc("@title:group", "Message Font && Color"), true); tfLayout->addWidget(mFontChooser); } void EditPrefTab::restore(bool, bool allTabs) { int index; if (allTabs || mTabs->currentIndex() == mTabGeneral) { mCopyToKOrganizer->setChecked(Preferences::defaultCopyToKOrganizer()); mLateCancel->setChecked(Preferences::defaultLateCancel()); switch (Preferences::defaultRecurPeriod()) { case Preferences::Recur_Yearly: index = 6; break; case Preferences::Recur_Monthly: index = 5; break; case Preferences::Recur_Weekly: index = 4; break; case Preferences::Recur_Daily: index = 3; break; case Preferences::Recur_SubDaily: index = 2; break; case Preferences::Recur_Login: index = 1; break; case Preferences::Recur_None: default: index = 0; break; } mRecurPeriod->setCurrentIndex(index); mFeb29->setButton(Preferences::defaultFeb29Type()); } if (allTabs || mTabs->currentIndex() == mTabTypes) { mConfirmAck->setChecked(Preferences::defaultConfirmAck()); mAutoClose->setChecked(Preferences::defaultAutoClose()); switch (Preferences::defaultReminderUnits()) { case TimePeriod::Weeks: index = 3; break; case TimePeriod::Days: index = 2; break; default: case TimePeriod::HoursMinutes: index = 1; break; case TimePeriod::Minutes: index = 0; break; } mReminderUnits->setCurrentIndex(index); KAEvent::ExtraActionOptions opts(0); if (Preferences::defaultExecPreActionOnDeferral()) opts |= KAEvent::ExecPreActOnDeferral; if (Preferences::defaultCancelOnPreActionError()) opts |= KAEvent::CancelOnPreActError; if (Preferences::defaultDontShowPreActionError()) opts |= KAEvent::DontShowPreActError; mSpecialActionsButton->setActions(Preferences::defaultPreAction(), Preferences::defaultPostAction(), opts); mSound->setCurrentIndex(soundIndex(Preferences::defaultSoundType())); mSoundFile->setText(Preferences::defaultSoundFile()); mSoundRepeat->setChecked(Preferences::defaultSoundRepeat()); mCmdScript->setChecked(Preferences::defaultCmdScript()); mCmdXterm->setChecked(Preferences::defaultCmdLogType() == Preferences::Log_Terminal); mEmailBcc->setChecked(Preferences::defaultEmailBcc()); } if (allTabs || mTabs->currentIndex() == mTabFontColour) { mFontChooser->setFgColour(Preferences::defaultFgColour()); mFontChooser->setBgColour(Preferences::defaultBgColour()); mFontChooser->setFont(Preferences::messageFont()); } } void EditPrefTab::apply(bool syncToDisc) { bool b = mAutoClose->isChecked(); if (b != Preferences::defaultAutoClose()) Preferences::setDefaultAutoClose(b); b = mConfirmAck->isChecked(); if (b != Preferences::defaultConfirmAck()) Preferences::setDefaultConfirmAck(b); TimePeriod::Units units; switch (mReminderUnits->currentIndex()) { case 3: units = TimePeriod::Weeks; break; case 2: units = TimePeriod::Days; break; default: case 1: units = TimePeriod::HoursMinutes; break; case 0: units = TimePeriod::Minutes; break; } if (units != Preferences::defaultReminderUnits()) Preferences::setDefaultReminderUnits(units); QString text = mSpecialActionsButton->preAction(); if (text != Preferences::defaultPreAction()) Preferences::setDefaultPreAction(text); text = mSpecialActionsButton->postAction(); if (text != Preferences::defaultPostAction()) Preferences::setDefaultPostAction(text); KAEvent::ExtraActionOptions opts = mSpecialActionsButton->options(); b = opts & KAEvent::ExecPreActOnDeferral; if (b != Preferences::defaultExecPreActionOnDeferral()) Preferences::setDefaultExecPreActionOnDeferral(b); b = opts & KAEvent::CancelOnPreActError; if (b != Preferences::defaultCancelOnPreActionError()) Preferences::setDefaultCancelOnPreActionError(b); b = opts & KAEvent::DontShowPreActError; if (b != Preferences::defaultDontShowPreActionError()) Preferences::setDefaultDontShowPreActionError(b); Preferences::SoundType snd; switch (mSound->currentIndex()) { case 3: snd = Preferences::Sound_Speak; break; case 2: snd = Preferences::Sound_File; break; case 1: snd = Preferences::Sound_Beep; break; case 0: default: snd = Preferences::Sound_None; break; } if (snd != Preferences::defaultSoundType()) Preferences::setDefaultSoundType(snd); text = mSoundFile->text(); if (text != Preferences::defaultSoundFile()) Preferences::setDefaultSoundFile(text); b = mSoundRepeat->isChecked(); if (b != Preferences::defaultSoundRepeat()) Preferences::setDefaultSoundRepeat(b); b = mCmdScript->isChecked(); if (b != Preferences::defaultCmdScript()) Preferences::setDefaultCmdScript(b); Preferences::CmdLogType log = mCmdXterm->isChecked() ? Preferences::Log_Terminal : Preferences::Log_Discard; if (log != Preferences::defaultCmdLogType()) Preferences::setDefaultCmdLogType(log); b = mEmailBcc->isChecked(); if (b != Preferences::defaultEmailBcc()) Preferences::setDefaultEmailBcc(b); b = mCopyToKOrganizer->isChecked(); if (b != Preferences::defaultCopyToKOrganizer()) Preferences::setDefaultCopyToKOrganizer(b); int i = mLateCancel->isChecked() ? 1 : 0; if (i != Preferences::defaultLateCancel()) Preferences::setDefaultLateCancel(i); Preferences::RecurType period; switch (mRecurPeriod->currentIndex()) { case 6: period = Preferences::Recur_Yearly; break; case 5: period = Preferences::Recur_Monthly; break; case 4: period = Preferences::Recur_Weekly; break; case 3: period = Preferences::Recur_Daily; break; case 2: period = Preferences::Recur_SubDaily; break; case 1: period = Preferences::Recur_Login; break; case 0: default: period = Preferences::Recur_None; break; } if (period != Preferences::defaultRecurPeriod()) Preferences::setDefaultRecurPeriod(period); int feb29 = mFeb29->selectedId(); if (feb29 >= 0 && static_cast(feb29) != Preferences::defaultFeb29Type()) Preferences::setDefaultFeb29Type(static_cast(feb29)); QColor colour = mFontChooser->fgColour(); if (colour != Preferences::defaultFgColour()) Preferences::setDefaultFgColour(colour); colour = mFontChooser->bgColour(); if (colour != Preferences::defaultBgColour()) Preferences::setDefaultBgColour(colour); QFont font = mFontChooser->font(); if (font != Preferences::messageFont()) Preferences::setMessageFont(font); PrefsTabBase::apply(syncToDisc); } void EditPrefTab::slotBrowseSoundFile() { QString defaultDir; QString url = SoundPicker::browseFile(defaultDir, mSoundFile->text()); if (!url.isEmpty()) mSoundFile->setText(url); } int EditPrefTab::soundIndex(Preferences::SoundType type) { switch (type) { case Preferences::Sound_Speak: return 3; case Preferences::Sound_File: return 2; case Preferences::Sound_Beep: return 1; case Preferences::Sound_None: default: return 0; } } QString EditPrefTab::validate() { if (mSound->currentIndex() == soundIndex(Preferences::Sound_File) && mSoundFile->text().isEmpty()) { mSoundFile->setFocus(); return xi18nc("@info", "You must enter a sound file when %1 is selected as the default sound type", SoundPicker::i18n_combo_File());; } return QString(); } /*============================================================================= = Class ViewPrefTab =============================================================================*/ ViewPrefTab::ViewPrefTab(StackedScrollGroup* scrollGroup) : PrefsTabBase(scrollGroup), mShowInSystemTrayCheck(nullptr), mShowInSystemTrayGroup(nullptr), mAutoHideSystemTray(nullptr), mAutoHideSystemTrayPeriod(nullptr) { mTabs = new QTabWidget(); topLayout()->addWidget(mTabs); QWidget* widget = new QWidget; QVBoxLayout* topGeneral = new QVBoxLayout(widget); topGeneral->setMargin(style()->pixelMetric(QStyle::PM_DefaultChildMargin) / 2); topGeneral->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); mTabGeneral = mTabs->addTab(widget, i18nc("@title:tab", "General")); widget = new QWidget; QVBoxLayout* topWindows = new QVBoxLayout(widget); topWindows->setMargin(style()->pixelMetric(QStyle::PM_DefaultChildMargin) / 2); topWindows->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); mTabWindows = mTabs->addTab(widget, i18nc("@title:tab", "Alarm Windows")); // Run-in-system-tray check box or group. static const QString showInSysTrayText = i18nc("@option:check", "Show in system tray"); static const QString showInSysTrayWhatsThis = xi18nc("@info:whatsthis", "Check to show KAlarm's icon in the system tray." " Showing it in the system tray provides easy access and a status indication."); if(Preferences::noAutoHideSystemTrayDesktops().contains(KAlarm::currentDesktopIdentityName())) { // Run-in-system-tray check box. // This desktop type doesn't provide GUI controls to view hidden system tray // icons, so don't show options to hide the system tray icon. widget = new QWidget; // this is to allow left adjustment topGeneral->addWidget(widget); QHBoxLayout* box = new QHBoxLayout(widget); mShowInSystemTrayCheck = new QCheckBox(showInSysTrayText); mShowInSystemTrayCheck->setWhatsThis(showInSysTrayWhatsThis); box->addWidget(mShowInSystemTrayCheck); box->setStretchFactor(new QWidget(widget), 1); // left adjust the controls } else { // Run-in-system-tray group box mShowInSystemTrayGroup = new QGroupBox(showInSysTrayText); mShowInSystemTrayGroup->setCheckable(true); mShowInSystemTrayGroup->setWhatsThis(showInSysTrayWhatsThis); topGeneral->addWidget(mShowInSystemTrayGroup); QGridLayout* grid = new QGridLayout(mShowInSystemTrayGroup); grid->setMargin(style()->pixelMetric(QStyle::PM_DefaultChildMargin)); grid->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); grid->setColumnStretch(1, 1); grid->setColumnMinimumWidth(0, indentWidth()); mAutoHideSystemTray = new ButtonGroup(mShowInSystemTrayGroup); connect(mAutoHideSystemTray, &ButtonGroup::buttonSet, this, &ViewPrefTab::slotAutoHideSysTrayChanged); QRadioButton* radio = new QRadioButton(i18nc("@option:radio Always show KAlarm icon", "Always show"), mShowInSystemTrayGroup); mAutoHideSystemTray->addButton(radio, 0); radio->setWhatsThis( xi18nc("@info:whatsthis", "Check to show KAlarm's icon in the system tray " "regardless of whether alarms are due.")); grid->addWidget(radio, 0, 0, 1, 2, Qt::AlignLeft); radio = new QRadioButton(i18nc("@option:radio", "Automatically hide if no active alarms"), mShowInSystemTrayGroup); mAutoHideSystemTray->addButton(radio, 1); radio->setWhatsThis( xi18nc("@info:whatsthis", "Check to automatically hide KAlarm's icon in " "the system tray if there are no active alarms. When hidden, the icon can " "always be made visible by use of the system tray option to show hidden icons.")); grid->addWidget(radio, 1, 0, 1, 2, Qt::AlignLeft); QString text = xi18nc("@info:whatsthis", "Check to automatically hide KAlarm's icon in the " "system tray if no alarms are due within the specified time period. When hidden, " "the icon can always be made visible by use of the system tray option to show hidden icons."); radio = new QRadioButton(i18nc("@option:radio", "Automatically hide if no alarm due within time period:"), mShowInSystemTrayGroup); radio->setWhatsThis(text); mAutoHideSystemTray->addButton(radio, 2); grid->addWidget(radio, 2, 0, 1, 2, Qt::AlignLeft); mAutoHideSystemTrayPeriod = new TimePeriod(true, mShowInSystemTrayGroup); mAutoHideSystemTrayPeriod->setWhatsThis(text); mAutoHideSystemTrayPeriod->setMaximumWidth(mAutoHideSystemTrayPeriod->sizeHint().width()); grid->addWidget(mAutoHideSystemTrayPeriod, 3, 1, 1, 1, Qt::AlignLeft); mShowInSystemTrayGroup->setMaximumHeight(mShowInSystemTrayGroup->sizeHint().height()); } // System tray tooltip group box QGroupBox* group = new QGroupBox(i18nc("@title:group", "System Tray Tooltip")); topGeneral->addWidget(group); QGridLayout* grid = new QGridLayout(group); grid->setMargin(style()->pixelMetric(QStyle::PM_DefaultChildMargin)); grid->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); grid->setColumnStretch(2, 1); grid->setColumnMinimumWidth(0, indentWidth()); grid->setColumnMinimumWidth(1, indentWidth()); mTooltipShowAlarms = new QCheckBox(i18nc("@option:check", "Show next &24 hours' alarms"), group); mTooltipShowAlarms->setMinimumSize(mTooltipShowAlarms->sizeHint()); connect(mTooltipShowAlarms, &QAbstractButton::toggled, this, &ViewPrefTab::slotTooltipAlarmsToggled); mTooltipShowAlarms->setWhatsThis( i18nc("@info:whatsthis", "Specify whether to include in the system tray tooltip, a summary of alarms due in the next 24 hours.")); grid->addWidget(mTooltipShowAlarms, 0, 0, 1, 3, Qt::AlignLeft); widget = new QWidget; QHBoxLayout* box = new QHBoxLayout(widget); box->setMargin(0); box->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); mTooltipMaxAlarms = new QCheckBox(i18nc("@option:check", "Maximum number of alarms to show:")); mTooltipMaxAlarms->setMinimumSize(mTooltipMaxAlarms->sizeHint()); box->addWidget(mTooltipMaxAlarms); connect(mTooltipMaxAlarms, &QAbstractButton::toggled, this, &ViewPrefTab::slotTooltipMaxToggled); mTooltipMaxAlarmCount = new SpinBox(1, 99); mTooltipMaxAlarmCount->setSingleShiftStep(5); mTooltipMaxAlarmCount->setMinimumSize(mTooltipMaxAlarmCount->sizeHint()); box->addWidget(mTooltipMaxAlarmCount); widget->setWhatsThis( i18nc("@info:whatsthis", "Uncheck to display all of the next 24 hours' alarms in the system tray tooltip. " "Check to enter an upper limit on the number to be displayed.")); grid->addWidget(widget, 1, 1, 1, 2, Qt::AlignLeft); mTooltipShowTime = new QCheckBox(MainWindow::i18n_chk_ShowAlarmTime(), group); mTooltipShowTime->setMinimumSize(mTooltipShowTime->sizeHint()); connect(mTooltipShowTime, &QAbstractButton::toggled, this, &ViewPrefTab::slotTooltipTimeToggled); mTooltipShowTime->setWhatsThis(i18nc("@info:whatsthis", "Specify whether to show in the system tray tooltip, the time at which each alarm is due.")); grid->addWidget(mTooltipShowTime, 2, 1, 1, 2, Qt::AlignLeft); mTooltipShowTimeTo = new QCheckBox(MainWindow::i18n_chk_ShowTimeToAlarm(), group); mTooltipShowTimeTo->setMinimumSize(mTooltipShowTimeTo->sizeHint()); connect(mTooltipShowTimeTo, &QAbstractButton::toggled, this, &ViewPrefTab::slotTooltipTimeToToggled); mTooltipShowTimeTo->setWhatsThis(i18nc("@info:whatsthis", "Specify whether to show in the system tray tooltip, how long until each alarm is due.")); grid->addWidget(mTooltipShowTimeTo, 3, 1, 1, 2, Qt::AlignLeft); widget = new QWidget; // this is to control the QWhatsThis text display area box = new QHBoxLayout(widget); box->setMargin(0); box->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); mTooltipTimeToPrefixLabel = new QLabel(i18nc("@label:textbox", "Prefix:")); box->addWidget(mTooltipTimeToPrefixLabel); mTooltipTimeToPrefix = new QLineEdit(); box->addWidget(mTooltipTimeToPrefix); mTooltipTimeToPrefixLabel->setBuddy(mTooltipTimeToPrefix); widget->setWhatsThis(i18nc("@info:whatsthis", "Enter the text to be displayed in front of the time until the alarm, in the system tray tooltip.")); grid->addWidget(widget, 4, 2, Qt::AlignLeft); group->setMaximumHeight(group->sizeHint().height()); group = new QGroupBox(i18nc("@title:group", "Alarm List")); topGeneral->addWidget(group); QHBoxLayout* hlayout = new QHBoxLayout(group); hlayout->setMargin(style()->pixelMetric(QStyle::PM_DefaultChildMargin)); QVBoxLayout* colourLayout = new QVBoxLayout(); colourLayout->setMargin(0); hlayout->addLayout(colourLayout); widget = new QWidget; // to group widgets for QWhatsThis text box = new QHBoxLayout(widget); box->setMargin(0); box->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing) / 2); colourLayout->addWidget(widget); QLabel* label1 = new QLabel(i18nc("@label:listbox", "Disabled alarm color:")); box->addWidget(label1); box->setStretchFactor(new QWidget(widget), 0); mDisabledColour = new ColourButton(); box->addWidget(mDisabledColour); label1->setBuddy(mDisabledColour); widget->setWhatsThis(i18nc("@info:whatsthis", "Choose the text color in the alarm list for disabled alarms.")); widget = new QWidget; // to group widgets for QWhatsThis text box = new QHBoxLayout(widget); box->setMargin(0); box->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing) / 2); colourLayout->addWidget(widget); QLabel* label2 = new QLabel(i18nc("@label:listbox", "Archived alarm color:")); box->addWidget(label2); box->setStretchFactor(new QWidget(widget), 0); mArchivedColour = new ColourButton(); box->addWidget(mArchivedColour); label2->setBuddy(mArchivedColour); widget->setWhatsThis(i18nc("@info:whatsthis", "Choose the text color in the alarm list for archived alarms.")); hlayout->addStretch(); if (topGeneral) topGeneral->addStretch(); // top adjust the widgets group = new QGroupBox(i18nc("@title:group", "Alarm Message Windows")); topWindows->addWidget(group); grid = new QGridLayout(group); grid->setMargin(style()->pixelMetric(QStyle::PM_DefaultChildMargin)); grid->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); grid->setColumnStretch(1, 1); grid->setColumnMinimumWidth(0, indentWidth()); mWindowPosition = new ButtonGroup(group); connect(mWindowPosition, &ButtonGroup::buttonSet, this, &ViewPrefTab::slotWindowPosChanged); QString whatsthis = xi18nc("@info:whatsthis", "Choose how to reduce the chance of alarm messages being accidentally acknowledged:" "Position alarm message windows as far as possible from the current mouse cursor location, or" "Position alarm message windows in the center of the screen, but disable buttons for a short time after the window is displayed."); QRadioButton* radio = new QRadioButton(i18nc("@option:radio", "Position windows far from mouse cursor"), group); mWindowPosition->addButton(radio, 0); radio->setWhatsThis(whatsthis); grid->addWidget(radio, 0, 0, 1, 2, Qt::AlignLeft); radio = new QRadioButton(i18nc("@option:radio", "Center windows, delay activating window buttons"), group); mWindowPosition->addButton(radio, 1); radio->setWhatsThis(whatsthis); grid->addWidget(radio, 1, 0, 1, 2, Qt::AlignLeft); widget = new QWidget; // this is to control the QWhatsThis text display area box = new QHBoxLayout(widget); box->setMargin(0); box->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); mWindowButtonDelayLabel = new QLabel(i18nc("@label:spinbox", "Button activation delay (seconds):")); box->addWidget(mWindowButtonDelayLabel); mWindowButtonDelay = new QSpinBox(); mWindowButtonDelay->setRange(1, 10); mWindowButtonDelayLabel->setBuddy(mWindowButtonDelay); box->addWidget(mWindowButtonDelay); widget->setWhatsThis(i18nc("@info:whatsthis", "Enter how long its buttons should remain disabled after the alarm message window is shown.")); box->setStretchFactor(new QWidget(widget), 1); // left adjust the controls grid->addWidget(widget, 2, 1, Qt::AlignLeft); grid->setRowMinimumHeight(3, style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); mModalMessages = new QCheckBox(i18nc("@option:check", "Message windows have a title bar and take keyboard focus"), group); mModalMessages->setMinimumSize(mModalMessages->sizeHint()); mModalMessages->setWhatsThis(xi18nc("@info:whatsthis", "Specify the characteristics of alarm message windows:" "If checked, the window is a normal window with a title bar, which grabs keyboard input when it is displayed." "If unchecked, the window does not interfere with your typing when " "it is displayed, but it has no title bar and cannot be moved or resized.")); grid->addWidget(mModalMessages, 4, 0, 1, 2, Qt::AlignLeft); if (topWindows) topWindows->addStretch(); // top adjust the widgets } void ViewPrefTab::restore(bool, bool allTabs) { if (allTabs || mTabs->currentIndex() == mTabGeneral) { if (mShowInSystemTrayGroup) mShowInSystemTrayGroup->setChecked(Preferences::showInSystemTray()); else mShowInSystemTrayCheck->setChecked(Preferences::showInSystemTray()); int id; int mins = Preferences::autoHideSystemTray(); switch (mins) { case -1: id = 1; break; // hide if no active alarms case 0: id = 0; break; // never hide default: { id = 2; int days = 0; int secs = 0; if (mins % 1440) secs = mins * 60; else days = mins / 1440; TimePeriod::Units units = secs ? TimePeriod::HoursMinutes : (days % 7) ? TimePeriod::Days : TimePeriod::Weeks; Duration duration((secs ? secs : days), (secs ? Duration::Seconds : Duration::Days)); mAutoHideSystemTrayPeriod->setPeriod(duration, false, units); break; } } if (mAutoHideSystemTray) mAutoHideSystemTray->setButton(id); setTooltip(Preferences::tooltipAlarmCount(), Preferences::showTooltipAlarmTime(), Preferences::showTooltipTimeToAlarm(), Preferences::tooltipTimeToPrefix()); mDisabledColour->setColor(Preferences::disabledColour()); mArchivedColour->setColor(Preferences::archivedColour()); } if (allTabs || mTabs->currentIndex() == mTabWindows) { mWindowPosition->setButton(Preferences::messageButtonDelay() ? 1 : 0); mWindowButtonDelay->setValue(Preferences::messageButtonDelay()); mModalMessages->setChecked(Preferences::modalMessages()); } } void ViewPrefTab::apply(bool syncToDisc) { QColor colour = mDisabledColour->color(); if (colour != Preferences::disabledColour()) Preferences::setDisabledColour(colour); colour = mArchivedColour->color(); if (colour != Preferences::archivedColour()) Preferences::setArchivedColour(colour); int n = mTooltipShowAlarms->isChecked() ? -1 : 0; if (n && mTooltipMaxAlarms->isChecked()) n = mTooltipMaxAlarmCount->value(); if (n != Preferences::tooltipAlarmCount()) Preferences::setTooltipAlarmCount(n); bool b = mTooltipShowTime->isChecked(); if (b != Preferences::showTooltipAlarmTime()) Preferences::setShowTooltipAlarmTime(b); b = mTooltipShowTimeTo->isChecked(); if (b != Preferences::showTooltipTimeToAlarm()) Preferences::setShowTooltipTimeToAlarm(b); QString text = mTooltipTimeToPrefix->text(); if (text != Preferences::tooltipTimeToPrefix()) Preferences::setTooltipTimeToPrefix(text); b = mShowInSystemTrayGroup ? mShowInSystemTrayGroup->isChecked() : mShowInSystemTrayCheck->isChecked(); if (b != Preferences::showInSystemTray()) Preferences::setShowInSystemTray(b); if (b && mAutoHideSystemTray) { switch (mAutoHideSystemTray->selectedId()) { case 0: n = 0; break; // never hide case 1: n = -1; break; // hide if no active alarms case 2: // hide if no alarms due within period n = mAutoHideSystemTrayPeriod->period().asSeconds() / 60; break; } if (n != Preferences::autoHideSystemTray()) Preferences::setAutoHideSystemTray(n); } n = mWindowPosition->selectedId(); if (n) n = mWindowButtonDelay->value(); if (n != Preferences::messageButtonDelay()) Preferences::setMessageButtonDelay(n); b = mModalMessages->isChecked(); if (b != Preferences::modalMessages()) Preferences::setModalMessages(b); PrefsTabBase::apply(syncToDisc); } void ViewPrefTab::setTooltip(int maxAlarms, bool time, bool timeTo, const QString& prefix) { if (!timeTo) time = true; // ensure that at least one time option is ticked // Set the states of the controls without calling signal // handlers, since these could change the checkboxes' states. mTooltipShowAlarms->blockSignals(true); mTooltipShowTime->blockSignals(true); mTooltipShowTimeTo->blockSignals(true); mTooltipShowAlarms->setChecked(maxAlarms); mTooltipMaxAlarms->setChecked(maxAlarms > 0); mTooltipMaxAlarmCount->setValue(maxAlarms > 0 ? maxAlarms : 1); mTooltipShowTime->setChecked(time); mTooltipShowTimeTo->setChecked(timeTo); mTooltipTimeToPrefix->setText(prefix); mTooltipShowAlarms->blockSignals(false); mTooltipShowTime->blockSignals(false); mTooltipShowTimeTo->blockSignals(false); // Enable/disable controls according to their states slotTooltipTimeToToggled(timeTo); slotTooltipAlarmsToggled(maxAlarms); } void ViewPrefTab::slotTooltipAlarmsToggled(bool on) { mTooltipMaxAlarms->setEnabled(on); mTooltipMaxAlarmCount->setEnabled(on && mTooltipMaxAlarms->isChecked()); mTooltipShowTime->setEnabled(on); mTooltipShowTimeTo->setEnabled(on); on = on && mTooltipShowTimeTo->isChecked(); mTooltipTimeToPrefix->setEnabled(on); mTooltipTimeToPrefixLabel->setEnabled(on); } void ViewPrefTab::slotTooltipMaxToggled(bool on) { mTooltipMaxAlarmCount->setEnabled(on && mTooltipMaxAlarms->isEnabled()); } void ViewPrefTab::slotTooltipTimeToggled(bool on) { if (!on && !mTooltipShowTimeTo->isChecked()) mTooltipShowTimeTo->setChecked(true); } void ViewPrefTab::slotTooltipTimeToToggled(bool on) { if (!on && !mTooltipShowTime->isChecked()) mTooltipShowTime->setChecked(true); on = on && mTooltipShowTimeTo->isEnabled(); mTooltipTimeToPrefix->setEnabled(on); mTooltipTimeToPrefixLabel->setEnabled(on); } void ViewPrefTab::slotAutoHideSysTrayChanged(QAbstractButton* button) { if (mAutoHideSystemTray) mAutoHideSystemTrayPeriod->setEnabled(mAutoHideSystemTray->id(button) == 2); } void ViewPrefTab::slotWindowPosChanged(QAbstractButton* button) { bool enable = mWindowPosition->id(button); mWindowButtonDelay->setEnabled(enable); mWindowButtonDelayLabel->setEnabled(enable); } #include "moc_prefdlg_p.cpp" #include "moc_prefdlg.cpp" // vim: et sw=4: diff --git a/src/preferences.cpp b/src/preferences.cpp index eff40e99..14a7706a 100644 --- a/src/preferences.cpp +++ b/src/preferences.cpp @@ -1,507 +1,507 @@ /* * preferences.cpp - program preference settings * Program: kalarm * Copyright © 2001-2018 by 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); Q_FOREACH(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) << "No writable autostart file path"; + 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) << "Autostart file is not read/write:" << autostartFileRW; + 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) << "Error reading autostart file:" << autostartFile; + 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(); 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) << "Error writing autostart file:" << autostartFileRW; + 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) << "Written" << autostartFileRW; + 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) { 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; 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; 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; if (from == FROM_SYS_SETTINGS) return MAIL_FROM_SYS_SETTINGS; return MAIL_FROM_ADDR; } QString Preferences::emailBccAddress() { 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 == '\''); 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()) { case '\\': ++i; continue; case '"': case '\'': if (cmd[i] != quote) continue; // fall through to ' ' 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 ccd47c7f..04178445 100644 --- a/src/recurrenceedit.cpp +++ b/src/recurrenceedit.cpp @@ -1,1743 +1,1743 @@ /* * recurrenceedit.cpp - widget to edit the event's recurrence definition * Program: kalarm * Copyright © 2002-2018 by 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 KCalCore; #include #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) { - qCDebug(KALARM_LOG); + qCDebug(KALARM_LOG) << "RecurrenceEdit:"; QVBoxLayout* topLayout = new QVBoxLayout(this); topLayout->setMargin(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); hlayout->setMargin(style()->pixelMetric(QStyle::PM_DefaultChildMargin)); 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->setMargin(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->setMargin(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->setMargin(style()->pixelMetric(QStyle::PM_DefaultChildMargin)); 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->setMargin(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->setMargin(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(0) : 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->setMargin(style()->pixelMetric(QStyle::PM_DefaultChildMargin)); hlayout->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); vlayout = new QVBoxLayout(); vlayout->setMargin(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->setMargin(0); hlayout->addLayout(vlayout); mExceptionDateEdit = new KDateComboBox(mExceptionGroup); mExceptionDateEdit->setOptions(mReadOnly ? KDateComboBox::Options(0) : 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->setMargin(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); 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(); mEndDateEdit->setEnabled(endDate); mEndTimeEdit->setEnabled(endDate && ((mAtLoginButton->isChecked() && !mEndAnyTimeCheckBox->isChecked()) || mSubDailyButton->isChecked())); 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(); 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 && mExceptionDateList->isItemSelected(item)) { int index = mExceptionDateList->row(item); QDate olddate = mExceptionDates[index]; 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 && mExceptionDateList->isItemSelected(item)) { 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; 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(); 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(); 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(); 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) mDailyRule->setDays(rDays); else mDailyRule->setDays(true); break; } case KARecurrence::WEEKLY: { mWeeklyButton->setChecked(true); QBitArray rDays = recurrence->days(); mWeeklyRule->setDays(rDays); break; } case KARecurrence::MONTHLY_POS: // on nth (Tuesday) of the month { 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) { if (!posns[i].pos()) rDays.setBit(posns[i].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(); 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(); mYearlyRule->setDate(day); mYearlyRule->setFeb29Type(recurrence->feb29Type()); } else if (rtype == KARecurrence::ANNUAL_POS) { mYearlyButton->setChecked(true); 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(); 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); 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; 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(); QVector daynums(1, daynum); event.setRecurMonthlyByDate(frequency, daynums, repeatCount, endDate); } } else if (button == mYearlyButton) { 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->setMargin(0); mLayout->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); QHBoxLayout* freqLayout = new QHBoxLayout(); freqLayout->setMargin(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->setMargin(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) { QGridLayout* grid = new QGridLayout(); grid->setMargin(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->setMargin(0); dgrid->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); const KCalendarSystem* calendar = KLocale::global()->calendar(); for (int i = 0; i < 7; ++i) { int day = KAlarm::localeDayInWeek_to_weekDay(i); mDayBox[i] = new CheckBox(calendar->weekDayName(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) { 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) { mButtonGroup = new ButtonGroup(this); // Month day selector QGridLayout* boxLayout = new QGridLayout(); boxLayout->setMargin(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); const KCalendarSystem* calendar = KLocale::global()->calendar(); for (int i = 0; i < 7; ++i) { int day = KAlarm::localeDayInWeek_to_weekDay(i); mDayOfWeekCombo->addItem(calendar->weekDayName(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; 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); 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->setMargin(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->setMargin(0); grid->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); const KCalendarSystem* calendar = KLocale::global()->calendar(); int year = KADateTime::currentLocalDate().year(); for (int i = 0; i < 12; ++i) { mMonthBox[i] = new CheckBox(calendar->monthName(i + 1, year, KCalendarSystem::ShortName), 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->setMargin(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); 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()); 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/traywindow.cpp b/src/traywindow.cpp index 581bd685..a6f8491a 100644 --- a/src/traywindow.cpp +++ b/src/traywindow.cpp @@ -1,429 +1,429 @@ /* * traywindow.cpp - the KDE system tray applet * Program: kalarm * Copyright © 2002-2018 by 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 "traywindow.h" #include "alarmcalendar.h" #include "alarmlistview.h" #include "functions.h" #include "kalarmapp.h" #include "mainwindow.h" #include "messagewin.h" #include "newalarmaction.h" #include "prefdlg.h" #include "preferences.h" #include "synchtimer.h" #include "templatemenuaction.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "kalarm_debug.h" #include #include using namespace KAlarmCal; struct TipItem { QDateTime dateTime; QString text; }; /*============================================================================= = Class: TrayWindow = The KDE system tray window. =============================================================================*/ TrayWindow::TrayWindow(MainWindow* parent) : KStatusNotifierItem(parent), mAssocMainWindow(parent), mAlarmsModel(nullptr), mStatusUpdateTimer(new QTimer(this)), mHaveDisabledAlarms(false) { - qCDebug(KALARM_LOG); + qCDebug(KALARM_LOG) << "TrayWindow:"; setToolTipIconByName(QStringLiteral("kalarm")); setToolTipTitle(KAboutData::applicationData().displayName()); setIconByName(QStringLiteral("kalarm")); setStatus(KStatusNotifierItem::Active); // Set up the context menu mActionEnabled = KAlarm::createAlarmEnableAction(this); addAction(QStringLiteral("tAlarmsEnable"), mActionEnabled); contextMenu()->addAction(mActionEnabled); connect(theApp(), &KAlarmApp::alarmEnabledToggled, this, &TrayWindow::setEnabledStatus); contextMenu()->addSeparator(); mActionNew = new NewAlarmAction(false, i18nc("@action", "&New Alarm"), this); addAction(QStringLiteral("tNew"), mActionNew); contextMenu()->addAction(mActionNew); connect(mActionNew, &NewAlarmAction::selected, this, &TrayWindow::slotNewAlarm); connect(mActionNew->fromTemplateAlarmAction(QString()), &TemplateMenuAction::selected, this, &TrayWindow::slotNewFromTemplate); contextMenu()->addSeparator(); QAction* a = KAlarm::createStopPlayAction(this); addAction(QStringLiteral("tStopPlay"), a); contextMenu()->addAction(a); QObject::connect(theApp(), &KAlarmApp::audioPlaying, a, &QAction::setVisible); QObject::connect(theApp(), &KAlarmApp::audioPlaying, this, &TrayWindow::updateStatus); a = KAlarm::createSpreadWindowsAction(this); addAction(QStringLiteral("tSpread"), a); contextMenu()->addAction(a); contextMenu()->addSeparator(); contextMenu()->addAction(KStandardAction::preferences(this, SLOT(slotPreferences()), this)); // Disable standard quit behaviour. We have to intercept the quit even, // if the main window is hidden. QAction* act = action(QStringLiteral("quit")); if (act) { act->disconnect(SIGNAL(triggered(bool)), this, SLOT(maybeQuit())); connect(act, &QAction::triggered, this, &TrayWindow::slotQuit); } // Set icon to correspond with the alarms enabled menu status setEnabledStatus(theApp()->alarmsEnabled()); connect(AlarmCalendar::resources(), &AlarmCalendar::haveDisabledAlarmsChanged, this, &TrayWindow::slotHaveDisabledAlarms); connect(this, &TrayWindow::activateRequested, this, &TrayWindow::slotActivateRequested); connect(this, &TrayWindow::secondaryActivateRequested, this, &TrayWindow::slotSecondaryActivateRequested); slotHaveDisabledAlarms(AlarmCalendar::resources()->haveDisabledAlarms()); // Hack: KSNI does not let us know when it is about to show the tooltip, // so we need to update it whenever something change in it. // This timer ensures that updateToolTip() is not called several times in a row mToolTipUpdateTimer = new QTimer(this); mToolTipUpdateTimer->setInterval(0); mToolTipUpdateTimer->setSingleShot(true); connect(mToolTipUpdateTimer, &QTimer::timeout, this, &TrayWindow::updateToolTip); // Update every minute to show accurate deadlines MinuteTimer::connect(mToolTipUpdateTimer, SLOT(start())); // Update when alarms are modified connect(AlarmListModel::all(), SIGNAL(dataChanged(QModelIndex,QModelIndex)), mToolTipUpdateTimer, SLOT(start())); connect(AlarmListModel::all(), SIGNAL(rowsInserted(QModelIndex,int,int)), mToolTipUpdateTimer, SLOT(start())); connect(AlarmListModel::all(), SIGNAL(rowsMoved(QModelIndex,int,int,QModelIndex,int)), mToolTipUpdateTimer, SLOT(start())); connect(AlarmListModel::all(), SIGNAL(rowsRemoved(QModelIndex,int,int)), mToolTipUpdateTimer, SLOT(start())); connect(AlarmListModel::all(), SIGNAL(modelReset()), mToolTipUpdateTimer, SLOT(start())); // Set auto-hide status when next alarm or preferences change mStatusUpdateTimer->setSingleShot(true); connect(mStatusUpdateTimer, &QTimer::timeout, this, &TrayWindow::updateStatus); connect(AlarmCalendar::resources(), &AlarmCalendar::earliestAlarmChanged, this, &TrayWindow::updateStatus); Preferences::connect(SIGNAL(autoHideSystemTrayChanged(int)), this, SLOT(updateStatus())); updateStatus(); // Update when tooltip preferences are modified Preferences::connect(SIGNAL(tooltipPreferencesChanged()), mToolTipUpdateTimer, SLOT(start())); } TrayWindow::~TrayWindow() { - qCDebug(KALARM_LOG); + qCDebug(KALARM_LOG) << "~TrayWindow"; theApp()->removeWindow(this); Q_EMIT deleted(); } /****************************************************************************** * Called when the "New Alarm" menu item is selected to edit a new alarm. */ void TrayWindow::slotNewAlarm(EditAlarmDlg::Type type) { KAlarm::editNewAlarm(type); } /****************************************************************************** * Called when the "New Alarm" menu item is selected to edit a new alarm from a * template. */ void TrayWindow::slotNewFromTemplate(const KAEvent* event) { KAlarm::editNewAlarm(event); } /****************************************************************************** * Called when the "Configure KAlarm" menu item is selected. */ void TrayWindow::slotPreferences() { KAlarmPrefDlg::display(); } /****************************************************************************** * Called when the Quit context menu item is selected. * Note that KAlarmApp::doQuit() must be called by the event loop, not directly * from the menu item, since otherwise the tray icon will be deleted while still * processing the menu, resulting in a crash. * Ideally, the connect() call setting up this slot in the constructor would use * Qt::QueuedConnection, but the slot is never called in that case. */ void TrayWindow::slotQuit() { // Note: QTimer::singleShot(0, ...) never calls the slot. QTimer::singleShot(1, this, &TrayWindow::slotQuitAfter); } void TrayWindow::slotQuitAfter() { theApp()->doQuit(static_cast(parent())); } /****************************************************************************** * Called when the Alarms Enabled action status has changed. * Updates the alarms enabled menu item check state, and the icon pixmap. */ void TrayWindow::setEnabledStatus(bool status) { - qCDebug(KALARM_LOG) << status; + qCDebug(KALARM_LOG) << "TrayWindow::setEnabledStatus:" << status; updateIcon(); updateStatus(); updateToolTip(); } /****************************************************************************** * Called when individual alarms are enabled or disabled. * Set the enabled icon to show or hide a disabled indication. */ void TrayWindow::slotHaveDisabledAlarms(bool haveDisabled) { - qCDebug(KALARM_LOG) << haveDisabled; + qCDebug(KALARM_LOG) << "TrayWindow::slotHaveDisabledAlarms:" << haveDisabled; mHaveDisabledAlarms = haveDisabled; updateIcon(); updateToolTip(); } /****************************************************************************** * Show the associated main window. */ void TrayWindow::showAssocMainWindow() { if (mAssocMainWindow) { mAssocMainWindow->show(); mAssocMainWindow->raise(); mAssocMainWindow->activateWindow(); } } /****************************************************************************** * A left click displays the KAlarm main window. */ void TrayWindow::slotActivateRequested() { // Left click: display/hide the first main window if (mAssocMainWindow && mAssocMainWindow->isVisible()) { mAssocMainWindow->raise(); mAssocMainWindow->activateWindow(); } } /****************************************************************************** * A middle button click displays the New Alarm window. */ void TrayWindow::slotSecondaryActivateRequested() { if (mActionNew->isEnabled()) mActionNew->trigger(); // display a New Alarm dialog } /****************************************************************************** * Adjust icon auto-hide status according to when the next alarm is due. * The icon is always shown if audio is playing, to give access to the 'stop' * menu option. */ void TrayWindow::updateStatus() { mStatusUpdateTimer->stop(); int period = Preferences::autoHideSystemTray(); // If the icon is always to be shown (AutoHideSystemTray = 0), // or audio is playing, show the icon. bool active = !period || MessageWin::isAudioPlaying(); if (!active) { // Show the icon only if the next active alarm complies active = theApp()->alarmsEnabled(); if (active) { KAEvent* event = AlarmCalendar::resources()->earliestAlarm(); active = static_cast(event); if (event && period > 0) { const KADateTime dt = event->nextTrigger(KAEvent::ALL_TRIGGER).effectiveKDateTime(); qint64 delay = KADateTime::currentLocalDateTime().secsTo(dt); delay -= static_cast(period) * 60; // delay until icon to be shown active = (delay <= 0); if (!active) { // First alarm trigger is too far in future, so tray icon is to // be auto-hidden. Set timer for when it should be shown again. delay *= 1000; // convert to msec int delay_int = static_cast(delay); if (delay_int != delay) delay_int = INT_MAX; mStatusUpdateTimer->setInterval(delay_int); mStatusUpdateTimer->start(); } } } } setStatus(active ? Active : Passive); } /****************************************************************************** * Adjust tooltip according to the app state. * The tooltip text shows alarms due in the next 24 hours. The limit of 24 * hours is because only times, not dates, are displayed. */ void TrayWindow::updateToolTip() { bool enabled = theApp()->alarmsEnabled(); QString subTitle; if (enabled && Preferences::tooltipAlarmCount()) subTitle = tooltipAlarmText(); if (!enabled) subTitle = i18n("Disabled"); else if (mHaveDisabledAlarms) { if (!subTitle.isEmpty()) subTitle += QLatin1String("
"); subTitle += i18nc("@info:tooltip Brief: some alarms are disabled", "(Some alarms disabled)"); } setToolTipSubTitle(subTitle); } /****************************************************************************** * Adjust icon according to the app state. */ void TrayWindow::updateIcon() { setIconByName(!theApp()->alarmsEnabled() ? QStringLiteral("kalarm-disabled") : mHaveDisabledAlarms ? QStringLiteral("kalarm-partdisabled") : QStringLiteral("kalarm")); } /****************************************************************************** * Return the tooltip text showing alarms due in the next 24 hours. * The limit of 24 hours is because only times, not dates, are displayed. */ QString TrayWindow::tooltipAlarmText() const { KAEvent event; const QString& prefix = Preferences::tooltipTimeToPrefix(); int maxCount = Preferences::tooltipAlarmCount(); const KADateTime now = KADateTime::currentLocalDateTime(); const KADateTime tomorrow = now.addDays(1); // Get today's and tomorrow's alarms, sorted in time order int i, iend; QList items; QVector events = KAlarm::getSortedActiveEvents(const_cast(this), &mAlarmsModel); for (i = 0, iend = events.count(); i < iend; ++i) { KAEvent* event = &events[i]; if (event->actionSubType() == KAEvent::MESSAGE) { TipItem item; QDateTime dateTime = event->nextTrigger(KAEvent::DISPLAY_TRIGGER).effectiveKDateTime().toLocalZone().qDateTime(); if (dateTime > tomorrow.qDateTime()) break; // ignore alarms after tomorrow at the current clock time item.dateTime = dateTime; // The alarm is due today, or early tomorrow if (Preferences::showTooltipAlarmTime()) { item.text += QLocale().toString(item.dateTime.time(), QLocale::ShortFormat); item.text += QLatin1Char(' '); } if (Preferences::showTooltipTimeToAlarm()) { int mins = (now.qDateTime().secsTo(item.dateTime) + 59) / 60; if (mins < 0) mins = 0; char minutes[3] = "00"; minutes[0] = static_cast((mins%60) / 10 + '0'); minutes[1] = static_cast((mins%60) % 10 + '0'); if (Preferences::showTooltipAlarmTime()) item.text += i18nc("@info prefix + hours:minutes", "(%1%2:%3)", prefix, mins/60, QLatin1String(minutes)); else item.text += i18nc("@info prefix + hours:minutes", "%1%2:%3", prefix, mins/60, QLatin1String(minutes)); item.text += QLatin1Char(' '); } item.text += AlarmText::summary(*event); // Insert the item into the list in time-sorted order int it = 0; for (int itend = items.count(); it < itend; ++it) { if (item.dateTime <= items[it].dateTime) break; } items.insert(it, item); } } - qCDebug(KALARM_LOG); + qCDebug(KALARM_LOG) << "TrayWindow::tooltipAlarmText"; QString text; int count = 0; for (i = 0, iend = items.count(); i < iend; ++i) { - qCDebug(KALARM_LOG) << "--" << (count+1) << ")" << items[i].text; + qCDebug(KALARM_LOG) << "TrayWindow::tooltipAlarmText: --" << (count+1) << ")" << items[i].text; if (i > 0) text += QLatin1String("
"); text += items[i].text; if (++count == maxCount) break; } return text; } /****************************************************************************** * Called when the associated main window is closed. */ void TrayWindow::removeWindow(MainWindow* win) { if (win == mAssocMainWindow) mAssocMainWindow = nullptr; } // vim: et sw=4: diff --git a/src/undo.cpp b/src/undo.cpp index 1d1dd18d..8f22ae0a 100644 --- a/src/undo.cpp +++ b/src/undo.cpp @@ -1,1270 +1,1270 @@ /* * undo.cpp - undo/redo facility * Program: kalarm * Copyright © 2005-2014 by 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 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::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(); if (undoCount + redoCount >= maxCount - 1) { if (undoCount) mUndoList.pop_back(); else mRedoList.pop_back(); } // Append the new item List* 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; 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; if (!list) return; 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; 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); 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); 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; if (!list) return ids; for (int i = 0, end = list->count(); i < end; ++i) { // 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) { QString evid = (*undos)[u]->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]); } } 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; if (list) { for (int i = 0, end = list->count(); i < end; ++i) { if ((*list)[i]->id() == id) return (*list)[i]; } } return nullptr; } /****************************************************************************** * Find an item with the specified ID. */ int Undo::findItem(int id, Undo::Type type) { List& list = (type == UNDO) ? mUndoList : mRedoList; int i = 0; for (int end = list.count(); i < end; ++i) { if (list[i]->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) { 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])); } /****************************************************************************** * 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()) { 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()) { 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 { 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) << mEventId; + 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; 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; 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) { 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) << mNewEventId; + 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; 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 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) { 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) { 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) << mEvent->id(); + 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); 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); 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; 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; 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) { switch ((*mUndos)[i]->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); + 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; 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); + 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; 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; 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) { if (e.category() == CalEvent::ACTIVE) dontShowErrors = KAlarm::dontShowErrors(EventId(e)); } // vim: et sw=4: