diff --git a/src/akonadimodel.cpp b/src/akonadimodel.cpp index ec1bf0a0..68a2bafc 100644 --- a/src/akonadimodel.cpp +++ b/src/akonadimodel.cpp @@ -1,975 +1,975 @@ /* * akonadimodel.cpp - KAlarm calendar file access using Akonadi * Program: kalarm * Copyright © 2007-2019 David Jarvie * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "akonadimodel.h" #include "alarmtime.h" #include "autoqpointer.h" #include "calendarmigrator.h" #include "mainwindow.h" #include "preferences.h" #include "synchtimer.h" #include "resources/resources.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 "kalarm_debug.h" using namespace Akonadi; using namespace KAlarmCal; // Ensure ResourceDataModelBase::UserRole is valid. ResourceDataModelBase does // not include Akonadi headers, so here we check that it has been set to be // compatible with EntityTreeModel::UserRole. static_assert((int)ResourceDataModelBase::UserRole>=(int)Akonadi::EntityTreeModel::UserRole, "ResourceDataModelBase::UserRole wrong value"); /*============================================================================= = Class: AkonadiModel =============================================================================*/ AkonadiModel* AkonadiModel::mInstance = nullptr; 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) , ResourceDataModelBase() , mMonitor(monitor) , mMigrationChecked(false) , mMigrating(false) { // Populate all collections, selected/enabled or unselected/disabled. setItemPopulationStrategy(ImmediatePopulation); // Restrict monitoring to collections containing the KAlarm mime types monitor->setCollectionMonitored(Collection::root()); monitor->setResourceMonitored("akonadi_kalarm_resource"); monitor->setResourceMonitored("akonadi_kalarm_dir_resource"); monitor->setMimeTypeMonitored(KAlarmCal::MIME_ACTIVE); monitor->setMimeTypeMonitored(KAlarmCal::MIME_ARCHIVED); monitor->setMimeTypeMonitored(KAlarmCal::MIME_TEMPLATE); monitor->itemFetchScope().fetchFullPayload(); monitor->itemFetchScope().fetchAttribute(); AttributeFactory::registerAttribute(); AttributeFactory::registerAttribute(); AttributeFactory::registerAttribute(); connect(monitor, SIGNAL(collectionChanged(Akonadi::Collection,QSet)), SLOT(slotCollectionChanged(Akonadi::Collection,QSet))); connect(monitor, &Monitor::collectionRemoved, this, &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(Resources::instance(), &Resources::resourceMessage, this, &AkonadiModel::slotResourceMessage, Qt::QueuedConnection); connect(this, &AkonadiModel::rowsInserted, this, &AkonadiModel::slotRowsInserted); connect(this, &AkonadiModel::rowsAboutToBeRemoved, this, &AkonadiModel::slotRowsAboutToBeRemoved); connect(this, &Akonadi::EntityTreeModel::collectionTreeFetched, this, &AkonadiModel::slotCollectionTreeFetched); connect(this, &Akonadi::EntityTreeModel::collectionPopulated, this, &AkonadiModel::slotCollectionPopulated); connect(monitor, &Monitor::itemChanged, this, &AkonadiModel::slotMonitoredItemChanged); connect(ServerManager::self(), &ServerManager::stateChanged, this, &AkonadiModel::checkResources); checkResources(ServerManager::state()); } /****************************************************************************** * Destructor. */ 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 (!mMigrationChecked) { qCDebug(KALARM_LOG) << "AkonadiModel::checkResources: Server running"; mMigrationChecked = true; mMigrating = true; CalendarMigrator::execute(); } break; case ServerManager::NotRunning: qCDebug(KALARM_LOG) << "AkonadiModel::checkResources: Server stopped"; mMigrationChecked = false; mMigrating = false; 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 mMigrationChecked && !mMigrating; } ChangeRecorder* AkonadiModel::monitor() { return instance()->mMonitor; } /****************************************************************************** * Return the data for a given role, for a specified item. */ QVariant AkonadiModel::data(const QModelIndex& index, int role) const { if (role == ResourceIdRole) role = CollectionIdRole; if (roleHandled(role)) { const Collection collection = EntityTreeModel::data(index, CollectionRole).value(); if (collection.isValid()) { // This is a Collection row // Update the collection's resource with the current collection value. const Resource& res = updateResource(collection); bool handled; const QVariant value = resourceData(role, res, handled); if (handled) return value; } else { Item item = EntityTreeModel::data(index, ItemRole).value(); if (item.isValid()) { // This is an Item row const QString mime = item.mimeType(); if ((mime != KAlarmCal::MIME_ACTIVE && mime != KAlarmCal::MIME_ARCHIVED && mime != KAlarmCal::MIME_TEMPLATE) || !item.hasPayload()) return QVariant(); Resource res; const KAEvent ev(event(item, index, res)); // this sets item.parentCollection() if (role == ParentResourceIdRole) return item.parentCollection().id(); bool handled; const QVariant value = eventData(role, index.column(), ev, res, handled); if (handled) return value; } } } return EntityTreeModel::data(index, role); } /****************************************************************************** * Return the number of columns for either a collection or an item. */ int AkonadiModel::entityColumnCount(HeaderGroup group) const { switch (group) { case CollectionTreeHeaders: return 1; case ItemListHeaders: return ColumnCount; default: return EntityTreeModel::entityColumnCount(group); } } /****************************************************************************** * Return offset to add to headerData() role, for item models. */ int AkonadiModel::headerDataEventRoleOffset() const { return TerminalUserRole * ItemListHeaders; } /****************************************************************************** * Return data for a column heading. */ QVariant AkonadiModel::entityHeaderData(int section, Qt::Orientation orientation, int role, HeaderGroup group) const { bool eventHeaders = false; switch (group) { case ItemListHeaders: eventHeaders = true; Q_FALLTHROUGH(); // fall through to CollectionTreeHeaders case CollectionTreeHeaders: { bool handled; const QVariant value = ResourceDataModelBase::headerData(section, orientation, role, eventHeaders, handled); if (handled) return value; break; } default: break; } return EntityTreeModel::entityHeaderData(section, orientation, role, group); } /****************************************************************************** * Recursive function to Q_EMIT the dataChanged() signal for all items in a * specified column range. */ void 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 = ix.data(ItemRole).value(); const bool isItem = item.isValid(); if (isItem) { if ((*checkFunc)(item)) { // For efficiency, Q_EMIT a single signal for each group of // consecutive items, rather than a separate signal for each item. if (start < 0) start = row; end = row; continue; } } if (start >= 0) Q_EMIT dataChanged(index(start, startColumn, parent), index(end, endColumn, parent)); start = -1; if (!isItem) signalDataChanged(checkFunc, startColumn, endColumn, ix); } if (start >= 0) Q_EMIT dataChanged(index(start, startColumn, parent), index(end, endColumn, parent)); } /****************************************************************************** * Signal every minute that the time-to-alarm values have changed. */ static bool checkItem_isActive(const Item& item) { return item.mimeType() == KAlarmCal::MIME_ACTIVE; } void AkonadiModel::slotUpdateTimeTo() { signalDataChanged(&checkItem_isActive, TimeToColumn, TimeToColumn, QModelIndex()); } /****************************************************************************** * Called when the colour used to display archived alarms has changed. */ static bool checkItem_isArchived(const Item& item) { return item.mimeType() == KAlarmCal::MIME_ARCHIVED; } void AkonadiModel::slotUpdateArchivedColour(const QColor&) { qCDebug(KALARM_LOG) << "AkonadiModel::slotUpdateArchivedColour"; signalDataChanged(&checkItem_isArchived, 0, ColumnCount - 1, QModelIndex()); } /****************************************************************************** * Called when the colour used to display disabled alarms has changed. */ static bool checkItem_isDisabled(const Item& item) { if (item.hasPayload()) { const KAEvent event = item.payload(); if (event.isValid()) return !event.enabled(); } return false; } void AkonadiModel::slotUpdateDisabledColour(const QColor&) { qCDebug(KALARM_LOG) << "AkonadiModel::slotUpdateDisabledColour"; signalDataChanged(&checkItem_isDisabled, 0, ColumnCount - 1, QModelIndex()); } /****************************************************************************** * Called when the definition of holidays has changed. */ static bool checkItem_excludesHolidays(const Item& item) { if (item.hasPayload()) { const KAEvent event = item.payload(); if (event.isValid() && event.holidaysExcluded()) return true; } return false; } void AkonadiModel::slotUpdateHolidays() { qCDebug(KALARM_LOG) << "AkonadiModel::slotUpdateHolidays"; Q_ASSERT(TimeToColumn == TimeColumn + 1); // signal should be emitted only for TimeTo and Time columns signalDataChanged(&checkItem_excludesHolidays, TimeColumn, TimeToColumn, QModelIndex()); } /****************************************************************************** * Called when the definition of working hours has changed. */ static bool checkItem_workTimeOnly(const Item& item) { if (item.hasPayload()) { const KAEvent event = item.payload(); if (event.isValid() && event.workTimeOnly()) return true; } return false; } void AkonadiModel::slotUpdateWorkingHours() { qCDebug(KALARM_LOG) << "AkonadiModel::slotUpdateWorkingHours"; Q_ASSERT(TimeToColumn == TimeColumn + 1); // signal should be emitted only for TimeTo and Time columns signalDataChanged(&checkItem_workTimeOnly, TimeColumn, TimeToColumn, QModelIndex()); } /****************************************************************************** * Reload a collection from Akonadi storage. The backend data is not reloaded. */ bool AkonadiModel::reloadResource(const Resource& resource) { if (!resource.isValid()) return false; qCDebug(KALARM_LOG) << "AkonadiModel::reloadResource:" << resource.id(); Collection collection(resource.id()); mMonitor->setCollectionMonitored(collection, false); mMonitor->setCollectionMonitored(collection, true); return true; } /****************************************************************************** * Reload all collections from Akonadi storage. The backend data is not reloaded. */ void AkonadiModel::reload() { qCDebug(KALARM_LOG) << "AkonadiModel::reload"; const Collection::List collections = mMonitor->collectionsMonitored(); for (const Collection& collection : collections) { mMonitor->setCollectionMonitored(collection, false); mMonitor->setCollectionMonitored(collection, true); } } /****************************************************************************** * Returns the index to a specified event. */ QModelIndex AkonadiModel::eventIndex(const KAEvent& event) const { return itemIndex(Item(mEventIds.value(event.id()).itemId)); } /****************************************************************************** * Returns the index to a specified event. */ QModelIndex AkonadiModel::eventIndex(const QString& eventId) const { return itemIndex(Item(mEventIds.value(eventId).itemId)); } /****************************************************************************** * Return all events belonging to a collection. */ QList AkonadiModel::events(ResourceId id) const { QList list; const QModelIndex ix = modelIndexForCollection(this, Collection(id)); if (ix.isValid()) getChildEvents(ix, list); for (KAEvent& ev : list) ev.setResourceId(id); return list; } /****************************************************************************** * Recursive function to append all child Events with a given mime type. */ void AkonadiModel::getChildEvents(const QModelIndex& parent, QList& events) const { for (int row = 0, count = rowCount(parent); row < count; ++row) { const QModelIndex ix = index(row, 0, parent); const Item item = ix.data(ItemRole).value(); if (item.isValid()) { if (item.hasPayload()) { KAEvent event = item.payload(); if (event.isValid()) events += event; } } else { const Collection c = ix.data(CollectionRole).value(); if (c.isValid()) getChildEvents(ix, events); } } } KAEvent AkonadiModel::event(const QString& eventId) const { return event(eventIndex(eventId)); } KAEvent AkonadiModel::event(const QModelIndex& ix) const { if (!ix.isValid()) return KAEvent(); Item item = ix.data(ItemRole).value(); Resource r; return event(item, ix, r); } /****************************************************************************** * Return the event for an Item at a specified model index. * The item's parent collection is set, as is the event's collection ID. */ KAEvent AkonadiModel::event(Akonadi::Item& item, const QModelIndex& ix, Resource& res) const { //TODO: Tune performance: This function is called very frequently with the same parameters if (ix.isValid()) { const Collection pc = ix.data(ParentCollectionRole).value(); item.setParentCollection(pc); res = resource(pc.id()); if (res.isValid()) { // Fetch the KAEvent defined by the Item, including commandError. return AkonadiResource::event(res, item); } } res = Resource::null(); return KAEvent(); } /****************************************************************************** * Return the up to date Item for a specified Akonadi ID. */ Item AkonadiModel::itemById(Item::Id id) const { Item item(id); if (!refresh(item)) return Item(); return item; } /****************************************************************************** * Return the Item for a given event. */ Item AkonadiModel::itemForEvent(const QString& eventId) const { const QModelIndex ix = eventIndex(eventId); if (!ix.isValid()) return Item(); return ix.data(ItemRole).value(); } #if 0 /****************************************************************************** * Add an event to a specified Collection. * If the event is scheduled to be added to the collection, it is updated with * its Akonadi item ID. * The event's 'updated' flag is cleared. * Reply = true if item creation has been scheduled. */ bool AkonadiModel::addEvent(KAEvent& event, Resource& resource) { qCDebug(KALARM_LOG) << "AkonadiModel::addEvent: ID:" << event.id(); if (!resource.addEvent(event)) return false; // Note that the item ID will be inserted in mEventIds after the Akonadi // Item has been created by ItemCreateJob, when slotRowsInserted() is called. mEventIds[event.id()] = EventIds(resource.id()); return true; } #endif /****************************************************************************** * Called when rows have been inserted into the model. */ void AkonadiModel::slotRowsInserted(const QModelIndex& parent, int start, int end) { qCDebug(KALARM_LOG) << "AkonadiModel::slotRowsInserted:" << start << "-" << end << "(parent =" << parent << ")"; QHash> events; for (int row = start; row <= end; ++row) { const QModelIndex ix = index(row, 0, parent); const Collection collection = ix.data(CollectionRole).value(); if (collection.isValid()) { // A collection has been inserted. Create a new resource to hold it. qCDebug(KALARM_LOG) << "AkonadiModel::slotRowsInserted: Collection" << collection.id() << collection.name(); Resource& resource = updateResource(collection); // Ignore it if it isn't owned by a valid Akonadi resource. if (resource.isValid()) { setCollectionChanged(resource, collection, true); Resources::notifyNewResourceInitialised(resource); if (!collection.hasAttribute()) { // If the compatibility attribute is missing at this point, // it doesn't always get notified later, so fetch the // collection to ensure that we see it. AgentInstance agent = AgentManager::self()->instance(collection.resource()); CollectionFetchJob* job = new CollectionFetchJob(Collection::root(), CollectionFetchJob::Recursive); job->fetchScope().setResource(agent.identifier()); connect(job, &CollectionFetchJob::result, instance(), &AkonadiModel::collectionFetchResult); } } } else { // An item has been inserted Item item = ix.data(ItemRole).value(); if (item.isValid()) { qCDebug(KALARM_LOG) << "item id=" << item.id() << ", revision=" << item.revision(); Resource res; const KAEvent evnt = event(item, ix, res); // this sets item.parentCollection() if (evnt.isValid()) { qCDebug(KALARM_LOG) << "AkonadiModel::slotRowsInserted: Event" << evnt.id(); // Only notify new events if the collection is already populated. // If not populated, all events will be notified when it is // eventually populated. if (res.isPopulated()) events[res] += evnt; mEventIds[evnt.id()] = EventIds(item.parentCollection().id(), item.id()); } // Notify the resource containing the item. AkonadiResource::notifyItemChanged(res, item, true); } } } for (auto it = events.constBegin(); it != events.constEnd(); ++it) { Resource res = it.key(); AkonadiResource::notifyEventsChanged(res, it.value()); } } /****************************************************************************** * Called when a CollectionFetchJob has completed. * Check for and process changes in attribute values. */ void AkonadiModel::collectionFetchResult(KJob* j) { CollectionFetchJob* job = qobject_cast(j); if (j->error()) qCWarning(KALARM_LOG) << "AkonadiModel::collectionFetchResult: CollectionFetchJob" << job->fetchScope().resource()<< "error: " << j->errorString(); else { const Collection::List collections = job->collections(); for (const Collection& c : collections) { qCDebug(KALARM_LOG) << "AkonadiModel::collectionFetchResult:" << c.id(); auto it = mResources.find(c.id()); if (it == mResources.end()) continue; Resource& resource = it.value(); setCollectionChanged(resource, c, false); } } } /****************************************************************************** * Called when rows are about to be removed from the model. */ void AkonadiModel::slotRowsAboutToBeRemoved(const QModelIndex& parent, int start, int end) { qCDebug(KALARM_LOG) << "AkonadiModel::slotRowsAboutToBeRemoved:" << start << "-" << end << "(parent =" << parent << ")"; QHash> events; for (int row = start; row <= end; ++row) { const QModelIndex ix = index(row, 0, parent); Item item = ix.data(ItemRole).value(); Resource res; const KAEvent evnt = event(item, ix, res); // this sets item.parentCollection() if (evnt.isValid()) { qCDebug(KALARM_LOG) << "AkonadiModel::slotRowsAboutToBeRemoved: Collection:" << item.parentCollection().id() << ", Event ID:" << evnt.id(); events[res] += evnt; mEventIds.remove(evnt.id()); } } for (auto it = events.constBegin(); it != events.constEnd(); ++it) { Resource res = it.key(); AkonadiResource::notifyEventsToBeDeleted(res, it.value()); } } /****************************************************************************** * Called when a monitored collection has changed. * Updates the collection held by the collection's resource, and notifies * changes of interest. */ void AkonadiModel::slotCollectionChanged(const Akonadi::Collection& c, const QSet& attributeNames) { qCDebug(KALARM_LOG) << "AkonadiModel::slotCollectionChanged:" << c.id() << attributeNames; auto it = mResources.find(c.id()); if (it != mResources.end()) { // The Monitor::collectionChanged() signal is not always emitted when // attributes are created! So check whether any attributes not // included in 'attributeNames' have been created. Resource& resource = it.value(); setCollectionChanged(resource, c, attributeNames.contains(CompatibilityAttribute::name())); } } /****************************************************************************** * Called when a monitored collection's properties or content have changed. * Optionally emits a signal if properties of interest have changed. */ void AkonadiModel::setCollectionChanged(Resource& resource, const Collection& collection, bool checkCompat) { AkonadiResource::notifyCollectionChanged(resource, collection, checkCompat); if (mMigrating) { mCollectionIdsBeingCreated.removeAll(collection.id()); if (mCollectionsBeingCreated.isEmpty() && mCollectionIdsBeingCreated.isEmpty() && CalendarMigrator::completed()) { qCDebug(KALARM_LOG) << "AkonadiModel::setCollectionChanged: Migration completed"; mMigrating = false; Q_EMIT migrationCompleted(); } } } /****************************************************************************** * Called when a monitored collection is removed. */ void AkonadiModel::slotCollectionRemoved(const Collection& collection) { const Collection::Id id = collection.id(); qCDebug(KALARM_LOG) << "AkonadiModel::slotCollectionRemoved:" << id; mResources.remove(collection.id()); // AkonadiResource will remove the resource from Resources. } /****************************************************************************** * 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 the collection tree has been fetched for the first time. */ void AkonadiModel::slotCollectionTreeFetched() { Resources::notifyResourcesCreated(); } /****************************************************************************** * Called when a collection has been populated. */ void AkonadiModel::slotCollectionPopulated(Akonadi::Collection::Id id) { qCDebug(KALARM_LOG) << "AkonadiModel::slotCollectionPopulated:" << id; AkonadiResource::notifyCollectionLoaded(id, events(id)); } /****************************************************************************** * Called when calendar migration has completed. */ void AkonadiModel::slotMigrationCompleted() { if (mCollectionsBeingCreated.isEmpty() && mCollectionIdsBeingCreated.isEmpty()) { qCDebug(KALARM_LOG) << "AkonadiModel: Migration completed"; mMigrating = false; Q_EMIT migrationCompleted(); } } /****************************************************************************** * Called when an item in the monitored collections has changed. */ void AkonadiModel::slotMonitoredItemChanged(const Akonadi::Item& item, const QSet&) { qCDebug(KALARM_LOG) << "AkonadiModel::slotMonitoredItemChanged: item id=" << item.id() << ", revision=" << item.revision(); const QModelIndex ix = itemIndex(item); if (ix.isValid()) { Resource res; Item itm = item; KAEvent evnt = event(itm, ix, res); // this sets item.parentCollection() if (evnt.isValid()) { // Notify the resource containing the item. if (res.isValid()) AkonadiResource::notifyItemChanged(res, itm, false); // Wait to ensure that the base EntityTreeModel has processed the // itemChanged() signal first, before we notify AkonadiResource // that the event has changed. mPendingEventChanges.enqueue(evnt); QTimer::singleShot(0, this, &AkonadiModel::slotEmitEventUpdated); } } } /****************************************************************************** * Called to Q_EMIT a signal when an event in the monitored collections has * changed. */ void AkonadiModel::slotEmitEventUpdated() { while (!mPendingEventChanges.isEmpty()) { const KAEvent event = mPendingEventChanges.dequeue(); Resource res = Resources::resource(event.resourceId()); AkonadiResource::notifyEventsChanged(res, {event}); } } /****************************************************************************** * Refresh the specified Collection with up to date data. * Return: true if successful, false if collection not found. */ bool AkonadiModel::refresh(Akonadi::Collection& collection) const { const QModelIndex ix = modelIndexForCollection(this, collection); if (!ix.isValid()) return false; collection = ix.data(CollectionRole).value(); // Also update our own copy of the collection. updateResource(collection); return true; } /****************************************************************************** * Refresh the specified Item with up to date data. * Return: true if successful, false if item not found. */ bool AkonadiModel::refresh(Akonadi::Item& item) const { const QModelIndex ix = itemIndex(item); if (!ix.isValid()) return false; item = ix.data(ItemRole).value(); return true; } /****************************************************************************** * Return the AkonadiResource object for a collection ID. */ Resource AkonadiModel::resource(Collection::Id id) const { return mResources.value(id, AkonadiResource::nullResource()); } /****************************************************************************** * Return the resource at a specified index, with up to date data. */ Resource AkonadiModel::resource(const QModelIndex& ix) const { return mResources.value(ix.data(CollectionIdRole).toLongLong(), AkonadiResource::nullResource()); } /****************************************************************************** * Find the QModelIndex of a resource. */ QModelIndex AkonadiModel::resourceIndex(const Resource& resource) const { const Collection& collection = AkonadiResource::collection(resource); const QModelIndex ix = modelIndexForCollection(this, collection); if (!ix.isValid()) return QModelIndex(); return ix; } /****************************************************************************** * Find the QModelIndex of a resource with a given ID. */ QModelIndex AkonadiModel::resourceIndex(Akonadi::Collection::Id id) const { const QModelIndex ix = modelIndexForCollection(this, Collection(id)); if (!ix.isValid()) return QModelIndex(); return ix; } /****************************************************************************** * Return a reference to the collection held in a Resource. This is the * definitive copy of the collection used by this model. * Return: the collection held by the model, or null if not found. */ Collection* AkonadiModel::collection(Collection::Id id) const { auto it = mResources.find(id); if (it != mResources.end()) { Collection& c = AkonadiResource::collection(it.value()); if (c.isValid()) return &c; } return nullptr; } /****************************************************************************** * Return a reference to the collection held in a Resource. This is the * definitive copy of the collection used by this model. * Return: the collection held by the model, or null if not found. */ Collection* AkonadiModel::collection(const Resource& resource) const { return collection(resource.id()); } /****************************************************************************** * Find the QModelIndex of an item. */ QModelIndex AkonadiModel::itemIndex(const Akonadi::Item& item) const { const QModelIndexList ixs = modelIndexesForItem(this, item); for (const QModelIndex& ix : ixs) { if (ix.isValid()) return ix; } return QModelIndex(); } /****************************************************************************** * Update the resource which holds a given Collection, by copying the Collection * value into it. If there is no resource, a new resource is created. * Param: collection - this should have been fetched from the model to ensure * that its value is up to date. */ Resource& AkonadiModel::updateResource(const Collection& collection) const { auto it = mResources.find(collection.id()); if (it != mResources.end()) { Collection& resourceCol = AkonadiResource::collection(it.value()); if (&collection != &resourceCol) resourceCol = collection; } else { // Create a new resource for the collection. it = mResources.insert(collection.id(), AkonadiResource::create(collection)); } return it.value(); } /****************************************************************************** * Display a message to the user. */ void AkonadiModel::slotResourceMessage(Resource&, ResourceType::MessageType type, const QString& message, const QString& details) { handleResourceMessage(type, message, details); } // vim: et sw=4: diff --git a/src/alarmcalendar.cpp b/src/alarmcalendar.cpp index 1b071a71..a90b3d2f 100644 --- a/src/alarmcalendar.cpp +++ b/src/alarmcalendar.cpp @@ -1,1570 +1,1570 @@ /* * alarmcalendar.cpp - KAlarm calendar file access * Program: kalarm * Copyright © 2001-2019 David Jarvie * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "alarmcalendar.h" #include "kalarm.h" #include "filedialog.h" #include "functions.h" #include "kalarmapp.h" #include "mainwindow.h" #include "messagebox.h" #include "preferences.h" #include "resources/resources.h" #include "kalarm_debug.h" #include #include #include #include #include #include -#include +#include #include #include #include #include using namespace KCalendarCore; using namespace KAlarmCal; static KACalendar::Compat fix(const KCalendarCore::FileStorage::Ptr&); static const QString displayCalendarName = QStringLiteral("displaying.ics"); static const ResourceId DISPLAY_COL_ID = -1; // resource ID used for displaying calendar AlarmCalendar* AlarmCalendar::mResourcesCalendar = nullptr; AlarmCalendar* AlarmCalendar::mDisplayCalendar = nullptr; QUrl AlarmCalendar::mLastImportUrl; /****************************************************************************** * Initialise the alarm calendars, and ensure that their file names are different. * There are 2 calendars: * 1) A resources calendar containing the active alarms, archived alarms and * alarm templates; * 2) A user-specific one which contains details of alarms which are currently * being displayed to that user and which have not yet been acknowledged; * Reply = true if success, false if calendar name error. */ bool AlarmCalendar::initialiseCalendars() { QDir dir; dir.mkpath(QStandardPaths::writableLocation(QStandardPaths::DataLocation)); QString displayCal = QStandardPaths::writableLocation(QStandardPaths::DataLocation) + QLatin1Char('/') + displayCalendarName; AkonadiModel::instance(); Preferences::setBackend(Preferences::Akonadi); Preferences::self()->save(); mResourcesCalendar = new AlarmCalendar(); mDisplayCalendar = new AlarmCalendar(displayCal, CalEvent::DISPLAYING); KACalendar::setProductId(KALARM_NAME, KALARM_VERSION); CalFormat::setApplication(QStringLiteral(KALARM_NAME), QString::fromLatin1(KACalendar::icalProductId())); return true; } /****************************************************************************** * Terminate access to all calendars. */ void AlarmCalendar::terminateCalendars() { delete mResourcesCalendar; mResourcesCalendar = nullptr; delete mDisplayCalendar; mDisplayCalendar = nullptr; } /****************************************************************************** * Return the display calendar, opening it first if necessary. */ AlarmCalendar* AlarmCalendar::displayCalendarOpen() { if (mDisplayCalendar->open()) return mDisplayCalendar; qCCritical(KALARM_LOG) << "AlarmCalendar::displayCalendarOpen: Open error"; return nullptr; } /****************************************************************************** * Find and return the event with the specified ID. * The calendar searched is determined by the calendar identifier in the ID. */ KAEvent* AlarmCalendar::getEvent(const EventId& eventId) { if (eventId.eventId().isEmpty()) return nullptr; return mResourcesCalendar->event(eventId); } /****************************************************************************** * Constructor for the resources calendar. */ AlarmCalendar::AlarmCalendar() : mCalType(RESOURCES) , mEventType(CalEvent::EMPTY) { Resources* resources = Resources::instance(); connect(resources, &Resources::eventsAdded, this, &AlarmCalendar::slotEventsAdded); connect(resources, &Resources::eventsToBeRemoved, this, &AlarmCalendar::slotEventsToBeRemoved); connect(resources, &Resources::eventUpdated, this, &AlarmCalendar::slotEventUpdated); connect(resources, &Resources::resourcesPopulated, this, &AlarmCalendar::slotResourcesPopulated); connect(resources, &Resources::settingsChanged, this, &AlarmCalendar::slotResourceSettingsChanged); } /****************************************************************************** * Constructor for a calendar file. */ AlarmCalendar::AlarmCalendar(const QString& path, CalEvent::Type type) : mEventType(type) { switch (type) { case CalEvent::ACTIVE: case CalEvent::ARCHIVED: case CalEvent::TEMPLATE: case CalEvent::DISPLAYING: break; default: Q_ASSERT(false); // invalid event type for a calendar break; } mUrl = QUrl::fromUserInput(path, QString(), QUrl::AssumeLocalFile); QString icalPath = path; icalPath.replace(QStringLiteral("\\.vcs$"), QStringLiteral(".ics")); mICalUrl = QUrl::fromUserInput(icalPath, QString(), QUrl::AssumeLocalFile); mCalType = (path == icalPath) ? LOCAL_ICAL : LOCAL_VCAL; // is the calendar in ICal or VCal format? } AlarmCalendar::~AlarmCalendar() { close(); } /****************************************************************************** * Check whether the calendar is open. */ bool AlarmCalendar::isOpen() { return mOpen; } /****************************************************************************** * Open the calendar if not already open, and load it into memory. */ bool AlarmCalendar::open() { if (isOpen()) return true; if (mCalType == RESOURCES) { mOpen = true; } else { if (!mUrl.isValid()) return false; qCDebug(KALARM_LOG) << "AlarmCalendar::open:" << mUrl.toDisplayString(); if (!mCalendarStorage) { MemoryCalendar::Ptr calendar(new MemoryCalendar(Preferences::timeSpecAsZone())); mCalendarStorage = FileStorage::Ptr(new FileStorage(calendar)); } // Check for file's existence, assuming that it does exist when uncertain, // to avoid overwriting it. auto statJob = KIO::stat(mUrl, KIO::StatJob::SourceSide, 2); KJobWidgets::setWindow(statJob, MainWindow::mainMainWindow()); if (!statJob->exec() || load() == 0) { // The calendar file doesn't yet exist, or it's zero length, so create a new one bool created = false; if (mICalUrl.isLocalFile()) created = saveCal(mICalUrl.toLocalFile()); else { QTemporaryFile tmpFile; tmpFile.setAutoRemove(false); tmpFile.open(); created = saveCal(tmpFile.fileName()); } if (created) load(); } } if (!mOpen) { mCalendarStorage->calendar().clear(); mCalendarStorage.clear(); } return isOpen(); } /****************************************************************************** * Load the calendar into memory. * Reply = 1 if success * = 0 if zero-length file exists. * = -1 if failure to load calendar file * = -2 if instance uninitialised. */ int AlarmCalendar::load() { if (mCalType == RESOURCES) { } else { if (!mCalendarStorage) return -2; QString filename; qCDebug(KALARM_LOG) << "AlarmCalendar::load:" << mUrl.toDisplayString(); if (!mUrl.isLocalFile()) { auto getJob = KIO::storedGet(mUrl); KJobWidgets::setWindow(getJob, MainWindow::mainMainWindow()); if (!getJob->exec()) { qCCritical(KALARM_LOG) << "AlarmCalendar::load: Download failure"; KAMessageBox::error(MainWindow::mainMainWindow(), xi18nc("@info", "Cannot download calendar: %1", mUrl.toDisplayString())); return -1; } QTemporaryFile tmpFile; tmpFile.setAutoRemove(false); tmpFile.write(getJob->data()); qCDebug(KALARM_LOG) << "--- Downloaded to" << tmpFile.fileName(); filename = tmpFile.fileName(); } else filename = mUrl.toLocalFile(); mCalendarStorage->calendar()->setTimeZone(Preferences::timeSpecAsZone()); mCalendarStorage->setFileName(filename); if (!mCalendarStorage->load()) { // Check if the file is zero length if (mUrl.isLocalFile()) { auto statJob = KIO::stat(KIO::upUrl(mUrl)); KJobWidgets::setWindow(statJob, MainWindow::mainMainWindow()); statJob->exec(); KFileItem fi(statJob->statResult(), mUrl); if (!fi.size()) return 0; // file is zero length } qCCritical(KALARM_LOG) << "AlarmCalendar::load: Error loading calendar file '" << filename <<"'"; KAMessageBox::error(MainWindow::mainMainWindow(), xi18nc("@info", "Error loading calendar:%1Please fix or delete the file.", mUrl.toDisplayString())); // load() could have partially populated the calendar, so clear it out mCalendarStorage->calendar()->close(); mCalendarStorage->calendar().clear(); mCalendarStorage.clear(); mOpen = false; return -1; } if (!mLocalFile.isEmpty()) { if (mLocalFile.startsWith(QDir::tempPath())) QFile::remove(mLocalFile); } mLocalFile = filename; fix(mCalendarStorage); // convert events to current KAlarm format for when calendar is saved updateDisplayKAEvents(); } mOpen = true; return 1; } /****************************************************************************** * Reload the calendar file into memory. */ bool AlarmCalendar::reload() { if (mCalType == RESOURCES) return true; if (!mCalendarStorage) return false; { qCDebug(KALARM_LOG) << "AlarmCalendar::reload:" << mUrl.toDisplayString(); close(); return open(); } } /****************************************************************************** * Save the calendar from memory to file. * If a filename is specified, create a new calendar file. */ bool AlarmCalendar::saveCal(const QString& newFile) { if (mCalType == RESOURCES) return true; if (!mCalendarStorage) return false; { if (!mOpen && newFile.isNull()) return false; qCDebug(KALARM_LOG) << "AlarmCalendar::saveCal:" << "\"" << newFile << "\"," << mEventType; QString saveFilename = newFile.isNull() ? mLocalFile : newFile; if (mCalType == LOCAL_VCAL && newFile.isNull() && mUrl.isLocalFile()) saveFilename = mICalUrl.toLocalFile(); mCalendarStorage->setFileName(saveFilename); mCalendarStorage->setSaveFormat(new ICalFormat); if (!mCalendarStorage->save()) { qCCritical(KALARM_LOG) << "AlarmCalendar::saveCal: Saving" << saveFilename << "failed."; KAMessageBox::error(MainWindow::mainMainWindow(), xi18nc("@info", "Failed to save calendar to %1", mICalUrl.toDisplayString())); return false; } if (!mICalUrl.isLocalFile()) { QFile file(saveFilename); file.open(QIODevice::ReadOnly); auto putJob = KIO::storedPut(&file, mICalUrl, -1); KJobWidgets::setWindow(putJob, MainWindow::mainMainWindow()); if (!putJob->exec()) { qCCritical(KALARM_LOG) << "AlarmCalendar::saveCal:" << saveFilename << "upload failed."; KAMessageBox::error(MainWindow::mainMainWindow(), xi18nc("@info", "Cannot upload calendar to %1", mICalUrl.toDisplayString())); return false; } } if (mCalType == LOCAL_VCAL) { // The file was in vCalendar format, but has now been saved in iCalendar format. mUrl = mICalUrl; mCalType = LOCAL_ICAL; } Q_EMIT calendarSaved(this); } mUpdateSave = false; return true; } /****************************************************************************** * Delete any temporary file at program exit. */ void AlarmCalendar::close() { if (mCalType != RESOURCES) { if (!mLocalFile.isEmpty()) { if (mLocalFile.startsWith(QDir::tempPath())) // removes it only if it IS a temporary file QFile::remove(mLocalFile); mLocalFile = QStringLiteral(""); } } // Flag as closed now to prevent removeKAEvents() doing silly things // when it's called again mOpen = false; if (mCalendarStorage) { mCalendarStorage->calendar()->close(); mCalendarStorage->calendar().clear(); mCalendarStorage.clear(); } // Resource map should be empty, but just in case... while (!mResourceMap.isEmpty()) removeKAEvents(mResourceMap.begin().key(), true, CalEvent::ACTIVE | CalEvent::ARCHIVED | CalEvent::TEMPLATE | CalEvent::DISPLAYING); } /****************************************************************************** * Create a KAEvent instance corresponding to each KCalendarCore::Event in the * display calendar, and store them in the event map in place of the old set. * Called after the display calendar has completed loading. */ void AlarmCalendar::updateDisplayKAEvents() { if (mCalType == RESOURCES) return; qCDebug(KALARM_LOG) << "AlarmCalendar::updateDisplayKAEvents"; const ResourceId key = DISPLAY_COL_ID; KAEvent::List& events = mResourceMap[key]; for (KAEvent* event : events) { mEventMap.remove(EventId(key, event->id())); delete event; } events.clear(); mEarliestAlarm[key] = nullptr; Calendar::Ptr cal = mCalendarStorage->calendar(); if (!cal) return; const Event::List kcalevents = cal->rawEvents(); for (Event::Ptr kcalevent : kcalevents) { if (kcalevent->alarms().isEmpty()) continue; // ignore events without alarms KAEvent* event = new KAEvent(kcalevent); if (!event->isValid()) { qCWarning(KALARM_LOG) << "AlarmCalendar::updateDisplayKAEvents: Ignoring unusable event" << kcalevent->uid(); delete event; continue; // ignore events without usable alarms } event->setCollectionId(key); events += event; mEventMap[EventId(key, kcalevent->uid())] = event; } } /****************************************************************************** * Delete a calendar and all its KAEvent instances of specified alarm types from * the lists. * Called after the calendar is deleted or alarm types have been disabled, or * the AlarmCalendar is closed. */ void AlarmCalendar::removeKAEvents(ResourceId key, bool closing, CalEvent::Types types) { bool removed = false; ResourceMap::Iterator rit = mResourceMap.find(key); if (rit != mResourceMap.end()) { KAEvent::List retained; KAEvent::List& events = rit.value(); for (int i = 0, end = events.count(); i < end; ++i) { KAEvent* event = events[i]; bool remove = (event->collectionId() != key); if (remove) { if (key != DISPLAY_COL_ID) qCCritical(KALARM_LOG) << "AlarmCalendar::removeKAEvents: Event" << event->id() << ", resource" << event->collectionId() << "Indexed under resource" << key; } else remove = event->category() & types; if (remove) { mEventMap.remove(EventId(key, event->id())); delete event; removed = true; } else retained.push_back(event); } if (retained.empty()) mResourceMap.erase(rit); else events.swap(retained); } if (removed) { mEarliestAlarm.remove(key); // Emit signal only if we're not in the process of closing the calendar if (!closing && mOpen) { Q_EMIT earliestAlarmChanged(); if (mHaveDisabledAlarms) checkForDisabledAlarms(); } } } /****************************************************************************** * Called when the enabled or read-only status of a resource has changed. * If the resource is now disabled, remove its events from the calendar. */ void AlarmCalendar::slotResourceSettingsChanged(Resource& resource, ResourceType::Changes change) { if (change & ResourceType::Enabled) { if (resource.isValid()) { // For each alarm type which has been disabled, remove the // collection's events from the map, but not from the resource. const CalEvent::Types enabled = resource.enabledTypes(); const CalEvent::Types disabled = ~enabled & (CalEvent::ACTIVE | CalEvent::ARCHIVED | CalEvent::TEMPLATE); removeKAEvents(resource.id(), false, disabled); // For each alarm type which has been enabled, add the collection's // events to the map. if (enabled != CalEvent::EMPTY) slotEventsAdded(resource, resource.events()); } } } /****************************************************************************** * Called when all resources have been populated for the first time. */ void AlarmCalendar::slotResourcesPopulated() { // Now that all calendars have been processed, all repeat-at-login alarms // will have been triggered. Prevent any new or updated repeat-at-login // alarms (e.g. when they are edited by the user) triggering from now on. mIgnoreAtLogin = true; } /****************************************************************************** * Called when events have been added to a resource. * Add corresponding KAEvent instances to those held by AlarmCalendar. * All events must have their resource ID set. */ void AlarmCalendar::slotEventsAdded(Resource& resource, const QList& events) { for (const KAEvent& event : events) slotEventUpdated(resource, event); } /****************************************************************************** * Called when an event has been changed in a resource. * Change the corresponding KAEvent instance held by AlarmCalendar. * The event must have its resource ID set. */ void AlarmCalendar::slotEventUpdated(Resource& resource, const KAEvent& event) { bool added = true; bool updated = false; KAEventMap::Iterator it = mEventMap.find(EventId(event)); if (it != mEventMap.end()) { // The event ID already exists - remove the existing event first KAEvent* storedEvent = it.value(); if (event.category() == storedEvent->category()) { // The existing event is the same type - update it in place *storedEvent = event; addNewEvent(resource, storedEvent, true); updated = true; } else delete storedEvent; added = false; } if (!updated) addNewEvent(resource, new KAEvent(event)); if (event.category() == CalEvent::ACTIVE) { bool enabled = event.enabled(); checkForDisabledAlarms(!enabled, enabled); if (!mIgnoreAtLogin && added && enabled && event.repeatAtLogin()) Q_EMIT atLoginEventAdded(event); } } /****************************************************************************** * Called when events are about to be removed from a resource. * Remove the corresponding KAEvent instances held by AlarmCalendar. */ void AlarmCalendar::slotEventsToBeRemoved(Resource& resource, const QList& events) { for (const KAEvent& event : events) { if (mEventMap.contains(EventId(event))) deleteEventInternal(event, resource, false); } } /****************************************************************************** * Import alarms from an external calendar and merge them into KAlarm's calendar. * The alarms are given new unique event IDs. * Parameters: parent = parent widget for error message boxes * Reply = true if all alarms in the calendar were successfully imported * = false if any alarms failed to be imported. */ bool AlarmCalendar::importAlarms(QWidget* parent, Resource* resourceptr) { if (mCalType != RESOURCES) return false; Resource nullresource; Resource& resource(resourceptr ? *resourceptr : nullresource); qCDebug(KALARM_LOG) << "AlarmCalendar::importAlarms"; const QUrl url = QFileDialog::getOpenFileUrl(parent, QString(), mLastImportUrl, QStringLiteral("%1 (*.vcs *.ics)").arg(i18nc("@info", "Calendar Files"))); if (url.isEmpty()) { qCCritical(KALARM_LOG) << "AlarmCalendar::importAlarms: Empty URL"; return false; } if (!url.isValid()) { qCDebug(KALARM_LOG) << "AlarmCalendar::importAlarms: Invalid URL"; return false; } mLastImportUrl = url.adjusted(QUrl::RemoveFilename); qCDebug(KALARM_LOG) << "AlarmCalendar::importAlarms:" << url.toDisplayString(); bool success = true; QString filename; bool local = url.isLocalFile(); if (local) { filename = url.toLocalFile(); if (!QFile::exists(filename)) { qCDebug(KALARM_LOG) << "AlarmCalendar::importAlarms: File '" << url.toDisplayString() <<"' not found"; KAMessageBox::error(parent, xi18nc("@info", "Could not load calendar %1.", url.toDisplayString())); return false; } } else { auto getJob = KIO::storedGet(url); KJobWidgets::setWindow(getJob, MainWindow::mainMainWindow()); if (!getJob->exec()) { qCCritical(KALARM_LOG) << "AlarmCalendar::importAlarms: Download failure"; KAMessageBox::error(parent, xi18nc("@info", "Cannot download calendar: %1", url.toDisplayString())); return false; } QTemporaryFile tmpFile; tmpFile.setAutoRemove(false); tmpFile.write(getJob->data()); tmpFile.seek(0); filename = tmpFile.fileName(); qCDebug(KALARM_LOG) << "--- Downloaded to" << filename; } // Read the calendar and add its alarms to the current calendars MemoryCalendar::Ptr cal(new MemoryCalendar(Preferences::timeSpecAsZone())); FileStorage::Ptr calStorage(new FileStorage(cal, filename)); success = calStorage->load(); if (!success) { qCDebug(KALARM_LOG) << "AlarmCalendar::importAlarms: Error loading calendar '" << filename <<"'"; KAMessageBox::error(parent, xi18nc("@info", "Could not load calendar %1.", url.toDisplayString())); } else { const KACalendar::Compat caltype = fix(calStorage); const CalEvent::Types wantedTypes = resource.alarmTypes(); const Event::List events = cal->rawEvents(); for (Event::Ptr event : events) { if (event->alarms().isEmpty() || !KAEvent(event).isValid()) continue; // ignore events without alarms, or usable alarms CalEvent::Type type = CalEvent::status(event); if (type == CalEvent::TEMPLATE) { // If we know the event was not created by KAlarm, don't treat it as a template if (caltype == KACalendar::Incompatible) type = CalEvent::ACTIVE; } Resource res; if (resource.isValid()) { if (!(type & wantedTypes)) continue; res = resource; } else { switch (type) { case CalEvent::ACTIVE: case CalEvent::ARCHIVED: case CalEvent::TEMPLATE: break; default: continue; } res = Resources::destination(type); } Event::Ptr newev(new Event(*event)); // If there is a display alarm without display text, use the event // summary text instead. if (type == CalEvent::ACTIVE && !newev->summary().isEmpty()) { const Alarm::List& alarms = newev->alarms(); for (Alarm::Ptr alarm : alarms) { if (alarm->type() == Alarm::Display && alarm->text().isEmpty()) alarm->setText(newev->summary()); } newev->setSummary(QString()); // KAlarm only uses summary for template names } // Give the event a new ID and add it to the calendars newev->setUid(CalEvent::uid(CalFormat::createUniqueId(), type)); if (!res.addEvent(KAEvent(newev))) success = false; } } if (!local) QFile::remove(filename); return success; } /****************************************************************************** * Export all selected alarms to an external calendar. * The alarms are given new unique event IDs. * Parameters: parent = parent widget for error message boxes * Reply = true if all alarms in the calendar were successfully exported * = false if any alarms failed to be exported. */ bool AlarmCalendar::exportAlarms(const KAEvent::List& events, QWidget* parent) { bool append; QString file = FileDialog::getSaveFileName(QUrl(QStringLiteral("kfiledialog:///exportalarms")), QStringLiteral("*.ics|%1").arg(i18nc("@info", "Calendar Files")), parent, i18nc("@title:window", "Choose Export Calendar"), &append); if (file.isEmpty()) return false; const QUrl url = QUrl::fromLocalFile(file); if (!url.isValid()) { qCDebug(KALARM_LOG) << "AlarmCalendar::exportAlarms: Invalid URL" << url; return false; } qCDebug(KALARM_LOG) << "AlarmCalendar::exportAlarms:" << url.toDisplayString(); MemoryCalendar::Ptr calendar(new MemoryCalendar(Preferences::timeSpecAsZone())); FileStorage::Ptr calStorage(new FileStorage(calendar, file)); if (append && !calStorage->load()) { KIO::UDSEntry uds; auto statJob = KIO::stat(url, KIO::StatJob::SourceSide, 2); KJobWidgets::setWindow(statJob, parent); statJob->exec(); KFileItem fi(statJob->statResult(), url); if (fi.size()) { qCCritical(KALARM_LOG) << "AlarmCalendar::exportAlarms: Error loading calendar file" << file << "for append"; KAMessageBox::error(MainWindow::mainMainWindow(), xi18nc("@info", "Error loading calendar to append to:%1", url.toDisplayString())); return false; } } KACalendar::setKAlarmVersion(calendar); // Add the alarms to the calendar bool success = true; bool exported = false; for (int i = 0, end = events.count(); i < end; ++i) { const KAEvent* event = events[i]; Event::Ptr kcalEvent(new Event); const CalEvent::Type type = event->category(); const QString id = CalEvent::uid(kcalEvent->uid(), type); kcalEvent->setUid(id); event->updateKCalEvent(kcalEvent, KAEvent::UID_IGNORE); if (calendar->addEvent(kcalEvent)) exported = true; else success = false; } if (exported) { // One or more alarms have been exported to the calendar. // Save the calendar to file. QTemporaryFile* tempFile = nullptr; bool local = url.isLocalFile(); if (!local) { tempFile = new QTemporaryFile; file = tempFile->fileName(); } calStorage->setFileName(file); calStorage->setSaveFormat(new ICalFormat); if (!calStorage->save()) { qCCritical(KALARM_LOG) << "AlarmCalendar::exportAlarms:" << file << ": failed"; KAMessageBox::error(MainWindow::mainMainWindow(), xi18nc("@info", "Failed to save new calendar to:%1", url.toDisplayString())); success = false; } else if (!local) { QFile qFile(file); qFile.open(QIODevice::ReadOnly); auto uploadJob = KIO::storedPut(&qFile, url, -1); KJobWidgets::setWindow(uploadJob, parent); if (!uploadJob->exec()) { qCCritical(KALARM_LOG) << "AlarmCalendar::exportAlarms:" << file << ": upload failed"; KAMessageBox::error(MainWindow::mainMainWindow(), xi18nc("@info", "Cannot upload new calendar to:%1", url.toDisplayString())); success = false; } } delete tempFile; } calendar->close(); return success; } /****************************************************************************** * Flag the start of a group of calendar update calls. * The purpose is to avoid multiple calendar saves during a group of operations. */ void AlarmCalendar::startUpdate() { ++mUpdateCount; } /****************************************************************************** * Flag the end of a group of calendar update calls. * The calendar is saved if appropriate. */ bool AlarmCalendar::endUpdate() { if (mUpdateCount > 0) --mUpdateCount; if (!mUpdateCount) { if (mUpdateSave) return saveCal(); } return true; } /****************************************************************************** * Save the calendar, or flag it for saving if in a group of calendar update calls. * Note that this method has no effect for Akonadi calendars. */ bool AlarmCalendar::save() { if (mUpdateCount) { mUpdateSave = true; return true; } else return saveCal(); } /****************************************************************************** * This method must only be called from the main KAlarm queue processing loop, * to prevent asynchronous calendar operations interfering with one another. * * Purge a list of archived events from the calendar. */ void AlarmCalendar::purgeEvents(const KAEvent::List& events) { for (const KAEvent* event : events) { deleteEventInternal(*event); } if (mHaveDisabledAlarms) checkForDisabledAlarms(); saveCal(); } /****************************************************************************** * Add the specified event to the calendar. * If it is an active event and 'useEventID' is false, a new event ID is * created. In all other cases, the event ID is taken from 'event' (if non-null). * 'event' is updated with the actual event ID. * The event is added to 'resource' if specified; otherwise the default resource * is used or the user is prompted, depending on policy. If 'noPrompt' is true, * the user will not be prompted so that if no default resource is defined, the * function will fail. * Reply = true if 'event' was written to the calendar, in which case (not * Akonadi) ownership of 'event' is taken by the calendar. 'event' * is updated. * = false if an error occurred, in which case 'event' is unchanged. */ bool AlarmCalendar::addEvent(KAEvent& evnt, QWidget* promptParent, bool useEventID, Resource* resourceptr, bool noPrompt, bool* cancelled) { if (cancelled) *cancelled = false; if (!mOpen) return false; // Check that the event type is valid for the calendar Resource nullresource; Resource& resource(resourceptr ? *resourceptr : nullresource); qCDebug(KALARM_LOG) << "AlarmCalendar::addEvent:" << evnt.id() << ", resource" << resource.id(); const CalEvent::Type type = evnt.category(); if (type != mEventType) { switch (type) { case CalEvent::ACTIVE: case CalEvent::ARCHIVED: case CalEvent::TEMPLATE: if (mEventType == CalEvent::EMPTY) break; // fall through to default Q_FALLTHROUGH(); default: return false; } } ResourceId key = resource.id(); Event::Ptr kcalEvent((mCalType == RESOURCES) ? (Event*)nullptr : new Event); KAEvent* event = new KAEvent(evnt); QString id = event->id(); if (type == CalEvent::ACTIVE) { if (id.isEmpty()) useEventID = false; else if (!useEventID) id.clear(); } else useEventID = true; if (id.isEmpty()) id = (mCalType == RESOURCES) ? CalFormat::createUniqueId() : kcalEvent->uid(); if (useEventID) { id = CalEvent::uid(id, type); if (kcalEvent) kcalEvent->setUid(id); } event->setEventId(id); bool ok = false; bool remove = false; if (mCalType == RESOURCES) { Resource res; if (resource.isEnabled(type)) res = resource; else { res = Resources::destination(type, promptParent, noPrompt, cancelled); if (!res.isValid()) { const char* typeStr = (type == CalEvent::ACTIVE) ? "Active alarm" : (type == CalEvent::ARCHIVED) ? "Archived alarm" : "alarm Template"; qCWarning(KALARM_LOG) << "AlarmCalendar::addEvent: Error! Cannot create" << typeStr << "(No default calendar is defined)"; } } if (res.isValid()) { // Don't add event to mEventMap yet - its Akonadi item id is not yet known. // It will be added once it is inserted into AkonadiModel. ok = res.addEvent(*event); remove = ok; // if success, delete the local event instance on exit if (ok && type == CalEvent::ACTIVE && !event->enabled()) checkForDisabledAlarms(true, false); } } else { // It's the display calendar event->updateKCalEvent(kcalEvent, KAEvent::UID_IGNORE); key = DISPLAY_COL_ID; if (!mEventMap.contains(EventId(key, event->id()))) { addNewEvent(Resource(), event); ok = mCalendarStorage->calendar()->addEvent(kcalEvent); remove = !ok; } } if (!ok) { if (remove) { // Adding to mCalendar failed, so undo AlarmCalendar::addEvent() mEventMap.remove(EventId(key, event->id())); KAEvent::List& events = mResourceMap[key]; int i = events.indexOf(event); if (i >= 0) events.remove(i); if (mEarliestAlarm[key] == event) findEarliestAlarm(key); } delete event; return false; } evnt = *event; if (remove) delete event; return true; } /****************************************************************************** * Internal method to add an already checked event to the calendar. * mEventMap takes ownership of the KAEvent. * If 'replace' is true, an existing event is being updated (NOTE: its category() * must remain the same). */ void AlarmCalendar::addNewEvent(const Resource& resource, KAEvent* event, bool replace) { const ResourceId key = resource.id(); event->setCollectionId(key); if (!replace) { mResourceMap[key] += event; mEventMap[EventId(key, event->id())] = event; } if ((resource.alarmTypes() & CalEvent::ACTIVE) && event->category() == CalEvent::ACTIVE) { // Update the earliest alarm to trigger const KAEvent* earliest = mEarliestAlarm.value(key, (KAEvent*)nullptr); if (replace && earliest == event) findEarliestAlarm(key); else { const KADateTime dt = event->nextTrigger(KAEvent::ALL_TRIGGER).effectiveKDateTime(); if (dt.isValid() && (!earliest || dt < earliest->nextTrigger(KAEvent::ALL_TRIGGER))) { mEarliestAlarm[key] = event; Q_EMIT earliestAlarmChanged(); } } } } /****************************************************************************** * Modify the specified event in the calendar with its new contents. * The new event must have a different event ID from the old one. * It is assumed to be of the same event type as the old one (active, etc.) * Reply = true if 'newEvent' was written to the calendar, in which case (not * Akonadi) ownership of 'newEvent' is taken by the calendar. * 'newEvent' is updated. * = false if an error occurred, in which case 'newEvent' is unchanged. */ bool AlarmCalendar::modifyEvent(const EventId& oldEventId, KAEvent& newEvent) { const EventId newId(oldEventId.collectionId(), newEvent.id()); qCDebug(KALARM_LOG) << "AlarmCalendar::modifyEvent:" << oldEventId << "->" << newId; bool noNewId = newId.isEmpty(); if (!noNewId && oldEventId == newId) { qCCritical(KALARM_LOG) << "AlarmCalendar::modifyEvent: Same IDs"; return false; } if (!mOpen) return false; if (mCalType == RESOURCES) { // Set the event's ID and Akonadi ID, and update the old // event in Akonadi. const KAEvent* storedEvent = event(oldEventId); if (!storedEvent) { qCCritical(KALARM_LOG) << "AlarmCalendar::modifyEvent: Old event not found"; return false; } if (noNewId) newEvent.setEventId(CalFormat::createUniqueId()); Resource resource = Resources::resource(oldEventId.collectionId()); if (!resource.isValid()) return false; // Don't add new event to mEventMap yet - its Akonadi item id is not yet known if (!resource.addEvent(newEvent)) return false; // Note: deleteEventInternal() will delete storedEvent before using the // event parameter, so need to pass a copy as the parameter. deleteEventInternal(KAEvent(*storedEvent), resource); if (mHaveDisabledAlarms) checkForDisabledAlarms(); } else { // This functionality isn't needed for the display calendar. // The calendar would take ownership of newEvent. return false; } return true; } /****************************************************************************** * Update the specified event in the calendar with its new contents. * The event retains the same ID. The event must be in the resource calendar. * Reply = event which has been updated * = 0 if error. */ KAEvent* AlarmCalendar::updateEvent(const KAEvent& evnt) { return updateEvent(&evnt); } KAEvent* AlarmCalendar::updateEvent(const KAEvent* evnt) { if (!mOpen || mCalType != RESOURCES) return nullptr; KAEvent* kaevnt = event(EventId(*evnt)); if (kaevnt) { Resource resource = Resources::resourceForEvent(evnt->id()); if (resource.updateEvent(*evnt)) { *kaevnt = *evnt; return kaevnt; } } qCDebug(KALARM_LOG) << "AlarmCalendar::updateEvent: error"; return nullptr; } /****************************************************************************** * Delete the specified event from the resource calendar, if it exists. * The calendar is then optionally saved. */ bool AlarmCalendar::deleteEvent(const KAEvent& event, bool saveit) { if (mOpen && mCalType == RESOURCES) { const CalEvent::Type status = deleteEventInternal(event); if (mHaveDisabledAlarms) checkForDisabledAlarms(); if (status != CalEvent::EMPTY) { if (saveit) return save(); return true; } } return false; } /****************************************************************************** * Delete the specified event from the calendar, if it exists. * The calendar is then optionally saved. */ bool AlarmCalendar::deleteDisplayEvent(const QString& eventID, bool saveit) { if (mOpen && mCalType != RESOURCES) { Resource resource; const CalEvent::Type status = deleteEventInternal(eventID, KAEvent(), resource, false); if (mHaveDisabledAlarms) checkForDisabledAlarms(); if (status != CalEvent::EMPTY) { if (saveit) return save(); return true; } } return false; } /****************************************************************************** * Internal method to delete the specified event from the calendar and lists. * Reply = event status, if it was found in the resource calendar/calendar * resource or local calendar * = CalEvent::EMPTY otherwise. */ CalEvent::Type AlarmCalendar::deleteEventInternal(const KAEvent& event, bool deleteFromAkonadi) { Resource resource = Resources::resource(event.collectionId()); if (!resource.isValid()) return CalEvent::EMPTY; return deleteEventInternal(event.id(), event, resource, deleteFromAkonadi); } CalEvent::Type AlarmCalendar::deleteEventInternal(const KAEvent& event, Resource& resource, bool deleteFromAkonadi) { if (!resource.isValid()) return CalEvent::EMPTY; if (event.collectionId() != resource.id()) { qCCritical(KALARM_LOG) << "AlarmCalendar::deleteEventInternal: Event" << event.id() << ": resource" << event.collectionId() << "differs from 'resource'" << resource.id(); return CalEvent::EMPTY; } return deleteEventInternal(event.id(), event, resource, deleteFromAkonadi); } CalEvent::Type AlarmCalendar::deleteEventInternal(const QString& eventID, const KAEvent& event, Resource& resource, bool deleteFromAkonadi) { // Make a copy of the KAEvent and the ID QString, since the supplied // references might be destructed when the event is deleted below. const QString id = eventID; const KAEvent paramEvent = event; Event::Ptr kcalEvent; if (mCalendarStorage) kcalEvent = mCalendarStorage->calendar()->event(id); const ResourceId key = resource.id(); KAEventMap::Iterator it = mEventMap.find(EventId(key, id)); if (it != mEventMap.end()) { KAEvent* ev = it.value(); mEventMap.erase(it); KAEvent::List& events = mResourceMap[key]; int i = events.indexOf(ev); if (i >= 0) events.remove(i); delete ev; if (mEarliestAlarm[key] == ev) findEarliestAlarm(resource); } else { for (EarliestMap::Iterator eit = mEarliestAlarm.begin(); eit != mEarliestAlarm.end(); ++eit) { KAEvent* ev = eit.value(); if (ev && ev->id() == id) { findEarliestAlarm(eit.key()); break; } } } CalEvent::Type status = CalEvent::EMPTY; if (kcalEvent) { status = CalEvent::status(kcalEvent); mCalendarStorage->calendar()->deleteEvent(kcalEvent); } else if (deleteFromAkonadi) { // It's an Akonadi event CalEvent::Type s = paramEvent.category(); if (resource.deleteEvent(paramEvent)) status = s; } return status; } /****************************************************************************** * Return the event with the specified ID. * If 'checkDuplicates' is true, and the resource ID is invalid, if there is * a unique event with the given ID, it will be returned. */ KAEvent* AlarmCalendar::event(const EventId& uniqueID, bool checkDuplicates) { if (!isValid()) return nullptr; const QString eventId = uniqueID.eventId(); if (uniqueID.collectionId() == -1 && checkDuplicates) { // The resource isn't known, but use the event ID if it is unique among // all resources. const KAEvent::List list = events(eventId); if (list.count() > 1) { qCWarning(KALARM_LOG) << "AlarmCalendar::event: Multiple events found with ID" << eventId; return nullptr; } if (list.isEmpty()) return nullptr; return list[0]; } KAEventMap::ConstIterator it = mEventMap.constFind(uniqueID); if (it == mEventMap.constEnd()) return nullptr; return it.value(); } /****************************************************************************** * Return the event with the specified ID. * For the Akonadi version, this method is for the display calendar only. */ Event::Ptr AlarmCalendar::kcalEvent(const QString& uniqueID) { Q_ASSERT(mCalType != RESOURCES); // only allowed for display calendar if (!mCalendarStorage) return Event::Ptr(); return mCalendarStorage->calendar()->event(uniqueID); } /****************************************************************************** * Find the alarm template with the specified name. * Reply = 0 if not found. */ KAEvent* AlarmCalendar::templateEvent(const QString& templateName) { if (templateName.isEmpty()) return nullptr; const KAEvent::List eventlist = events(CalEvent::TEMPLATE); for (KAEvent* event : eventlist) { if (event->templateName() == templateName) return event; } return nullptr; } /****************************************************************************** * Return all events with the specified ID, from all calendars. */ KAEvent::List AlarmCalendar::events(const QString& uniqueId) const { KAEvent::List list; if (mCalType == RESOURCES && isValid()) { for (ResourceMap::ConstIterator rit = mResourceMap.constBegin(); rit != mResourceMap.constEnd(); ++rit) { const ResourceId id = rit.key(); KAEventMap::ConstIterator it = mEventMap.constFind(EventId(id, uniqueId)); if (it != mEventMap.constEnd()) list += it.value(); } } return list; } /****************************************************************************** * Return all events in the calendar which contain alarms. * Optionally the event type can be filtered, using an OR of event types. */ KAEvent::List AlarmCalendar::events(const Resource& resource, CalEvent::Types type) const { KAEvent::List list; if (mCalType != RESOURCES && (!mCalendarStorage || resource.isValid())) return list; if (resource.isValid()) { const ResourceId key = resource.id(); ResourceMap::ConstIterator rit = mResourceMap.constFind(key); if (rit == mResourceMap.constEnd()) return list; const KAEvent::List events = rit.value(); if (type == CalEvent::EMPTY) return events; for (KAEvent* const event : events) if (type & event->category()) list += event; } else { for (ResourceMap::ConstIterator rit = mResourceMap.constBegin(); rit != mResourceMap.constEnd(); ++rit) { const KAEvent::List events = rit.value(); if (type == CalEvent::EMPTY) list += events; else { for (KAEvent* const event : events) if (type & event->category()) list += event; } } } return list; } /****************************************************************************** * Return all events in the calendar which contain usable alarms. * For the Akonadi version, this method is for the display calendar only. * Optionally the event type can be filtered, using an OR of event types. */ Event::List AlarmCalendar::kcalEvents(CalEvent::Type type) { Event::List list; Q_ASSERT(mCalType != RESOURCES); // only allowed for display calendar if (!mCalendarStorage) return list; list = mCalendarStorage->calendar()->rawEvents(); for (int i = 0; i < list.count(); ) { Event::Ptr event = list.at(i); if (event->alarms().isEmpty() || (type != CalEvent::EMPTY && !(type & CalEvent::status(event))) || !KAEvent(event).isValid()) list.remove(i); else ++i; } return list; } /****************************************************************************** * Return whether an event is read-only. * Display calendar events are always returned as read-only. */ bool AlarmCalendar::eventReadOnly(const QString& eventId) const { if (mCalType != RESOURCES) return true; KAEvent event; const Resource resource = Resources::resourceForEvent(eventId, event); return !event.isValid() || event.isReadOnly() || !resource.isWritable(event.category()); //TODO || compatibility(event) != KACalendar::Current; } /****************************************************************************** * Called when an alarm's enabled status has changed. */ void AlarmCalendar::disabledChanged(const KAEvent* event) { if (event->category() == CalEvent::ACTIVE) { bool status = event->enabled(); checkForDisabledAlarms(!status, status); } } /****************************************************************************** * Check whether there are any individual disabled alarms, following an alarm * creation or modification. Must only be called for an ACTIVE alarm. */ void AlarmCalendar::checkForDisabledAlarms(bool oldEnabled, bool newEnabled) { if (mCalType == RESOURCES && newEnabled != oldEnabled) { if (newEnabled && mHaveDisabledAlarms) checkForDisabledAlarms(); else if (!newEnabled && !mHaveDisabledAlarms) { mHaveDisabledAlarms = true; Q_EMIT haveDisabledAlarmsChanged(true); } } } /****************************************************************************** * Check whether there are any individual disabled alarms. */ void AlarmCalendar::checkForDisabledAlarms() { if (mCalType != RESOURCES) return; bool disabled = false; const KAEvent::List eventlist = events(CalEvent::ACTIVE); for (const KAEvent* const event : eventlist) { if (!event->enabled()) { disabled = true; break; } } if (disabled != mHaveDisabledAlarms) { mHaveDisabledAlarms = disabled; Q_EMIT haveDisabledAlarmsChanged(disabled); } } /****************************************************************************** * Find and note the active alarm with the earliest trigger time for a calendar. */ void AlarmCalendar::findEarliestAlarm(const Resource& resource) { if (mCalType != RESOURCES) return; if (!(resource.alarmTypes() & CalEvent::ACTIVE)) return; findEarliestAlarm(resource.id()); } void AlarmCalendar::findEarliestAlarm(ResourceId key) { EarliestMap::Iterator eit = mEarliestAlarm.find(key); if (eit != mEarliestAlarm.end()) eit.value() = nullptr; if (mCalType != RESOURCES || key < 0) return; ResourceMap::ConstIterator rit = mResourceMap.constFind(key); if (rit == mResourceMap.constEnd()) return; const KAEvent::List& events = rit.value(); KAEvent* earliest = nullptr; KADateTime earliestTime; for (KAEvent* event : events) { if (event->category() != CalEvent::ACTIVE || mPendingAlarms.contains(event->id())) continue; const KADateTime dt = event->nextTrigger(KAEvent::ALL_TRIGGER).effectiveKDateTime(); if (dt.isValid() && (!earliest || dt < earliestTime)) { earliestTime = dt; earliest = event; } } mEarliestAlarm[key] = earliest; Q_EMIT earliestAlarmChanged(); } /****************************************************************************** * Return the active alarm with the earliest trigger time. * Reply = 0 if none. */ KAEvent* AlarmCalendar::earliestAlarm() const { KAEvent* earliest = nullptr; KADateTime earliestTime; for (EarliestMap::ConstIterator eit = mEarliestAlarm.constBegin(); eit != mEarliestAlarm.constEnd(); ++eit) { KAEvent* event = eit.value(); if (!event) continue; const KADateTime dt = event->nextTrigger(KAEvent::ALL_TRIGGER).effectiveKDateTime(); if (dt.isValid() && (!earliest || dt < earliestTime)) { earliestTime = dt; earliest = event; } } return earliest; } /****************************************************************************** * Note that an alarm which has triggered is now being processed. While pending, * it will be ignored for the purposes of finding the earliest trigger time. */ void AlarmCalendar::setAlarmPending(KAEvent* event, bool pending) { const QString id = event->id(); bool wasPending = mPendingAlarms.contains(id); qCDebug(KALARM_LOG) << "AlarmCalendar::setAlarmPending:" << id << "," << pending << "(was" << wasPending << ")"; if (pending) { if (wasPending) return; mPendingAlarms += id; } else { if (!wasPending) return; mPendingAlarms.remove(id); } // Now update the earliest alarm to trigger for its calendar findEarliestAlarm(Resources::resourceForEvent(event->id())); } /****************************************************************************** * Called when the user changes the start-of-day time. * Adjust the start times of all date-only alarms' recurrences. */ void AlarmCalendar::adjustStartOfDay() { if (!isValid()) return; for (ResourceMap::ConstIterator rit = mResourceMap.constBegin(); rit != mResourceMap.constEnd(); ++rit) KAEvent::adjustStartOfDay(rit.value()); } /****************************************************************************** * Find the version of KAlarm which wrote the calendar file, and do any * necessary conversions to the current format. */ KACalendar::Compat fix(const FileStorage::Ptr& fileStorage) { QString versionString; int version = KACalendar::updateVersion(fileStorage, versionString); if (version == KACalendar::IncompatibleFormat) return KACalendar::Incompatible; // calendar was created by another program, or an unknown version of KAlarm return KACalendar::Current; } // vim: et sw=4: diff --git a/src/alarmlistdelegate.cpp b/src/alarmlistdelegate.cpp index f3b6b309..e031557c 100644 --- a/src/alarmlistdelegate.cpp +++ b/src/alarmlistdelegate.cpp @@ -1,125 +1,125 @@ /* * alarmlistdelegate.cpp - handles editing and display of alarm list * Program: kalarm * Copyright © 2007-2019 David Jarvie * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "alarmlistdelegate.h" #include "akonadimodel.h" #include "functions.h" #include "resources/eventmodel.h" #include "kalarm_debug.h" #include -#include +#include #include void AlarmListDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const { QStyleOptionViewItem opt = option; if (index.isValid()) { if (opt.state & QStyle::State_Selected && !index.data(ResourceDataModelBase::EnabledRole).toBool()) { // Make the text colour for selected disabled alarms // distinguishable from enabled alarms. KColorScheme::adjustForeground(opt.palette, KColorScheme::InactiveText, QPalette::HighlightedText, KColorScheme::Selection); } switch (index.column()) { case AlarmListModel::TimeColumn: { const QString str = index.data(ResourceDataModelBase::TimeDisplayRole).toString(); // Need to pad out spacing to align times without leading zeroes int i = str.indexOf(QLatin1Char('~')); // look for indicator of a leading zero to be omitted if (i >= 0) { painter->save(); opt.displayAlignment = Qt::AlignRight; const QVariant value = index.data(Qt::ForegroundRole); if (value.isValid()) opt.palette.setColor(QPalette::Text, value.value()); QRect displayRect; { QString str0 = str; str0[i] = QLatin1Char('0'); QRect r = opt.rect; r.setWidth(INT_MAX/256); displayRect = textRectangle(painter, r, opt.font, str0); displayRect = QStyle::alignedRect(opt.direction, opt.displayAlignment, displayRect.size().boundedTo(opt.rect.size()), opt.rect); } const QString date = str.left(i); const QString time = str.mid(i + 1); if (i > 0) { opt.displayAlignment = Qt::AlignLeft; drawDisplay(painter, opt, displayRect, date); opt.displayAlignment = Qt::AlignRight; } drawDisplay(painter, opt, displayRect, time); painter->restore(); return; } break; } case AlarmListModel::ColourColumn: { const KAEvent event = static_cast(index.model())->event(index); if (event.isValid() && event.commandError() != KAEvent::CMD_NO_ERROR) { opt.font.setBold(true); opt.font.setStyleHint(QFont::Serif); opt.font.setPointSize(opt.rect.height() - 2); } break; } default: break; } } QItemDelegate::paint(painter, opt, index); } QSize AlarmListDelegate::sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index) const { if (index.isValid()) { switch (index.column()) { case AlarmListModel::ColourColumn: { int h = option.fontMetrics.lineSpacing(); return QSize(h * 3 / 4, h); } } } return QItemDelegate::sizeHint(option, index); } void AlarmListDelegate::edit(KAEvent* event, EventListView* view) { KAlarm::editAlarm(event, static_cast(view)); // edit alarm (view-only mode if archived or read-only) } // vim: et sw=4: diff --git a/src/alarmlistview.cpp b/src/alarmlistview.cpp index b0635682..e95f55de 100644 --- a/src/alarmlistview.cpp +++ b/src/alarmlistview.cpp @@ -1,190 +1,190 @@ /* * alarmlistview.cpp - widget showing list of alarms * Program: kalarm * Copyright © 2007,2008,2010,2019 David Jarvie * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "alarmlistview.h" #include "resources/resourcedatamodelbase.h" #include "resources/eventmodel.h" -#include -#include +#include +#include #include #include #include #include AlarmListView::AlarmListView(const QByteArray& configGroup, QWidget* parent) : EventListView(parent) , mConfigGroup(configGroup) { setEditOnSingleClick(true); connect(header(), &QHeaderView::sectionMoved, this, &AlarmListView::sectionMoved); } void AlarmListView::setModel(QAbstractItemModel* model) { EventListView::setModel(model); KConfigGroup config(KSharedConfig::openConfig(), mConfigGroup.constData()); const QByteArray settings = config.readEntry("ListHead", QByteArray()); if (!settings.isEmpty()) header()->restoreState(settings); header()->setSectionsMovable(true); header()->setStretchLastSection(false); header()->setSectionResizeMode(AlarmListModel::TimeColumn, QHeaderView::ResizeToContents); header()->setSectionResizeMode(AlarmListModel::TimeToColumn, QHeaderView::ResizeToContents); header()->setSectionResizeMode(AlarmListModel::RepeatColumn, QHeaderView::ResizeToContents); header()->setSectionResizeMode(AlarmListModel::ColourColumn, QHeaderView::Fixed); header()->setSectionResizeMode(AlarmListModel::TypeColumn, QHeaderView::Fixed); header()->setSectionResizeMode(AlarmListModel::TextColumn, QHeaderView::Stretch); header()->setStretchLastSection(true); // necessary to ensure ResizeToContents columns do resize to contents! const int minWidth = viewOptions().fontMetrics.lineSpacing() * 3 / 4; header()->setMinimumSectionSize(minWidth); const int margin = QApplication::style()->pixelMetric(QStyle::PM_FocusFrameHMargin); header()->resizeSection(AlarmListModel::ColourColumn, minWidth); header()->resizeSection(AlarmListModel::TypeColumn, AlarmListModel::iconWidth() + 2*margin + 2); header()->setContextMenuPolicy(Qt::CustomContextMenu); connect(header(), &QWidget::customContextMenuRequested, this, &AlarmListView::headerContextMenuRequested); } /****************************************************************************** * Return which of the optional columns are currently shown. * Note that the column order must be the same as in setColumnsVisible(). */ QList AlarmListView::columnsVisible() const { if (!model()) return {}; return { !header()->isSectionHidden(AlarmListModel::TimeColumn), !header()->isSectionHidden(AlarmListModel::TimeToColumn), !header()->isSectionHidden(AlarmListModel::RepeatColumn), !header()->isSectionHidden(AlarmListModel::ColourColumn), !header()->isSectionHidden(AlarmListModel::TypeColumn) }; } /****************************************************************************** * Set which of the optional columns are to be shown. * Note that the column order must be the same as in columnsVisible(). */ void AlarmListView::setColumnsVisible(const QList& show) { if (!model()) return; const QList vis = (show.size() < 5) ? QList{true, false, true, true, true} : show; header()->setSectionHidden(AlarmListModel::TimeColumn, !vis[0]); header()->setSectionHidden(AlarmListModel::TimeToColumn, !vis[1]); header()->setSectionHidden(AlarmListModel::RepeatColumn, !vis[2]); header()->setSectionHidden(AlarmListModel::ColourColumn, !vis[3]); header()->setSectionHidden(AlarmListModel::TypeColumn, !vis[4]); sortByColumn(vis[0] ? AlarmListModel::TimeColumn : AlarmListModel::TimeToColumn, Qt::AscendingOrder); } /****************************************************************************** * Called when the column order is changed. * Save the new order for restoration on program restart. */ void AlarmListView::sectionMoved() { KConfigGroup config(KSharedConfig::openConfig(), mConfigGroup.constData()); config.writeEntry("ListHead", header()->saveState()); config.sync(); } /****************************************************************************** * Called when a context menu is requested for the header. * Allow the user to choose which columns to display. */ void AlarmListView::headerContextMenuRequested(const QPoint& pt) { QAbstractItemModel* almodel = model(); int count = header()->count(); QMenu menu; for (int col = 0; col < count; ++col) { const QString title = almodel->headerData(col, Qt::Horizontal, ResourceDataModelBase::ColumnTitleRole).toString(); if (!title.isEmpty()) { QAction* act = menu.addAction(title); act->setData(col); act->setCheckable(true); act->setChecked(!header()->isSectionHidden(col)); if (col == AlarmListModel::TextColumn) act->setEnabled(false); // don't allow text column to be hidden else QObject::connect(act, &QAction::triggered, this, [this, &menu, act] { showHideColumn(menu, act); }); } } enableTimeColumns(&menu); menu.exec(header()->mapToGlobal(pt)); } /****************************************************************************** * Show or hide a column according to the header context menu. */ void AlarmListView::showHideColumn(QMenu& menu, QAction* act) { int col = act->data().toInt(); if (col < 0 || col >= header()->count()) return; bool show = act->isChecked(); header()->setSectionHidden(col, !show); if (col == AlarmListModel::TimeColumn || col == AlarmListModel::TimeToColumn) enableTimeColumns(&menu); Q_EMIT columnsVisibleChanged(); } /****************************************************************************** * Disable Time or Time To in the context menu if the other one is not * selected to be displayed, to ensure that at least one is always shown. */ void AlarmListView::enableTimeColumns(QMenu* menu) { bool timeShown = !header()->isSectionHidden(AlarmListModel::TimeColumn); bool timeToShown = !header()->isSectionHidden(AlarmListModel::TimeToColumn); const QList actions = menu->actions(); if (!timeToShown) { header()->setSectionHidden(AlarmListModel::TimeColumn, false); for (QAction* act : actions) { if (act->data().toInt() == AlarmListModel::TimeColumn) { act->setEnabled(false); break; } } } else if (!timeShown) { header()->setSectionHidden(AlarmListModel::TimeToColumn, false); for (QAction* act : actions) { if (act->data().toInt() == AlarmListModel::TimeToColumn) { act->setEnabled(false); break; } } } } // vim: et sw=4: diff --git a/src/alarmtimewidget.cpp b/src/alarmtimewidget.cpp index 614a9ed1..f33d0f0b 100644 --- a/src/alarmtimewidget.cpp +++ b/src/alarmtimewidget.cpp @@ -1,666 +1,666 @@ /* * alarmtimewidget.cpp - alarm date/time entry widget * Program: kalarm * Copyright © 2001-2019 David Jarvie * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "alarmtimewidget.h" #include "buttongroup.h" #include "checkbox.h" #include "messagebox.h" #include "preferences.h" #include "pushbutton.h" #include "radiobutton.h" #include "synchtimer.h" #include "timeedit.h" #include "timespinbox.h" #include "timezonecombo.h" #include -#include +#include #include #include #include #include #include #include static const QTime time_23_59(23, 59); const int AlarmTimeWidget::maxDelayTime = 999*60 + 59; // < 1000 hours QString AlarmTimeWidget::i18n_TimeAfterPeriod() { return i18nc("@info", "Enter the length of time (in hours and minutes) after " "the current time to schedule the alarm."); } /****************************************************************************** * Construct a widget with a group box and title. */ AlarmTimeWidget::AlarmTimeWidget(const QString& groupBoxTitle, Mode mode, QWidget* parent) : QFrame(parent) { init(mode, groupBoxTitle); } /****************************************************************************** * Construct a widget without a group box or title. */ AlarmTimeWidget::AlarmTimeWidget(Mode mode, QWidget* parent) : QFrame(parent) { init(mode); } void AlarmTimeWidget::init(Mode mode, const QString& title) { static const QString recurText = i18nc("@info", "If a recurrence is configured, the start date/time will be adjusted " "to the first recurrence on or after the entered date/time."); static const QString tzText = i18nc("@info", "This uses KAlarm's default time zone, set in the Configuration dialog."); QWidget* topWidget; if (title.isEmpty()) topWidget = this; else { QBoxLayout* layout = new QVBoxLayout(this); layout->setContentsMargins(0, 0, 0, 0); layout->setSpacing(0); topWidget = new QGroupBox(title, this); layout->addWidget(topWidget); } mDeferring = mode & DEFER_TIME; mButtonGroup = new ButtonGroup(this); connect(mButtonGroup, &ButtonGroup::buttonSet, this, &AlarmTimeWidget::slotButtonSet); QVBoxLayout* topLayout = new QVBoxLayout(topWidget); topLayout->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); int dcm = title.isEmpty() ? 0 : style()->pixelMetric(QStyle::PM_DefaultChildMargin); topLayout->setContentsMargins(dcm, dcm, dcm, dcm); // At time radio button/label mAtTimeRadio = new RadioButton((mDeferring ? i18nc("@option:radio", "Defer to date/time:") : i18nc("@option:radio", "At date/time:")), topWidget); mAtTimeRadio->setFixedSize(mAtTimeRadio->sizeHint()); mAtTimeRadio->setWhatsThis(mDeferring ? i18nc("@info:whatsthis", "Reschedule the alarm to the specified date and time.") : i18nc("@info:whatsthis", "Specify the date, or date and time, to schedule the alarm.")); mButtonGroup->addButton(mAtTimeRadio); // Date edit box mDateEdit = new KDateComboBox(topWidget); mDateEdit->setOptions(KDateComboBox::EditDate | KDateComboBox::SelectDate | KDateComboBox::DatePicker); connect(mDateEdit, &KDateComboBox::dateEntered, this, &AlarmTimeWidget::dateTimeChanged); mDateEdit->setWhatsThis(xi18nc("@info:whatsthis", "Enter the date to schedule the alarm." "%1", (mDeferring ? tzText : recurText))); mAtTimeRadio->setFocusWidget(mDateEdit); // Time edit box and Any time checkbox QWidget* timeBox = new QWidget(topWidget); QHBoxLayout* timeBoxHLayout = new QHBoxLayout(timeBox); timeBoxHLayout->setContentsMargins(0, 0, 0, 0); timeBoxHLayout->setSpacing(2 * style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); mTimeEdit = new TimeEdit(timeBox); timeBoxHLayout->addWidget(mTimeEdit); mTimeEdit->setFixedSize(mTimeEdit->sizeHint()); connect(mTimeEdit, &TimeEdit::valueChanged, this, &AlarmTimeWidget::dateTimeChanged); mTimeEdit->setWhatsThis(xi18nc("@info:whatsthis", "Enter the time to schedule the alarm." "%1" "%2", (mDeferring ? tzText : recurText), TimeSpinBox::shiftWhatsThis())); mAnyTime = -1; // current status is uninitialised if (mode == DEFER_TIME) { mAnyTimeAllowed = false; mAnyTimeCheckBox = nullptr; } else { mAnyTimeAllowed = true; mAnyTimeCheckBox = new CheckBox(i18nc("@option:check", "Any time"), timeBox); timeBoxHLayout->addWidget(mAnyTimeCheckBox); mAnyTimeCheckBox->setFixedSize(mAnyTimeCheckBox->sizeHint()); connect(mAnyTimeCheckBox, &CheckBox::toggled, this, &AlarmTimeWidget::slotAnyTimeToggled); mAnyTimeCheckBox->setWhatsThis(i18nc("@info:whatsthis", "Check to specify only a date (without a time) for the alarm. The alarm will trigger at the first opportunity on the selected date.")); } // 'Time from now' radio button/label mAfterTimeRadio = new RadioButton((mDeferring ? i18nc("@option:radio", "Defer for time interval:") : i18nc("@option:radio", "Time from now:")), topWidget); mAfterTimeRadio->setFixedSize(mAfterTimeRadio->sizeHint()); mAfterTimeRadio->setWhatsThis(mDeferring ? i18nc("@info:whatsthis", "Reschedule the alarm for the specified time interval after now.") : i18nc("@info:whatsthis", "Schedule the alarm after the specified time interval from now.")); mButtonGroup->addButton(mAfterTimeRadio); // Delay time spin box mDelayTimeEdit = new TimeSpinBox(1, maxDelayTime, topWidget); mDelayTimeEdit->setValue(1439); mDelayTimeEdit->setFixedSize(mDelayTimeEdit->sizeHint()); connect(mDelayTimeEdit, static_cast(&TimeSpinBox::valueChanged), this, &AlarmTimeWidget::delayTimeChanged); mDelayTimeEdit->setWhatsThis(mDeferring ? xi18nc("@info:whatsthis", "%1%2", i18n_TimeAfterPeriod(), TimeSpinBox::shiftWhatsThis()) : xi18nc("@info:whatsthis", "%1%2%3", i18n_TimeAfterPeriod(), recurText, TimeSpinBox::shiftWhatsThis())); mAfterTimeRadio->setFocusWidget(mDelayTimeEdit); // Set up the layout, either narrow or wide QGridLayout* grid = new QGridLayout(); grid->setContentsMargins(0, 0, 0, 0); topLayout->addLayout(grid); if (mDeferring) { grid->addWidget(mAtTimeRadio, 0, 0); grid->addWidget(mDateEdit, 0, 1, Qt::AlignLeft); grid->addWidget(timeBox, 1, 1, Qt::AlignLeft); grid->setColumnStretch(2, 1); topLayout->addStretch(); QHBoxLayout* layout = new QHBoxLayout(); topLayout->addLayout(layout); layout->addWidget(mAfterTimeRadio); layout->addWidget(mDelayTimeEdit); layout->addStretch(); } else { grid->addWidget(mAtTimeRadio, 0, 0, Qt::AlignLeft); grid->addWidget(mDateEdit, 0, 1, Qt::AlignLeft); grid->addWidget(timeBox, 0, 2, Qt::AlignLeft); grid->setRowStretch(1, 1); grid->addWidget(mAfterTimeRadio, 2, 0, Qt::AlignLeft); grid->addWidget(mDelayTimeEdit, 2, 1, Qt::AlignLeft); // Time zone selection push button mTimeZoneButton = new PushButton(i18nc("@action:button", "Time Zone..."), topWidget); connect(mTimeZoneButton, &PushButton::clicked, this, &AlarmTimeWidget::showTimeZoneSelector); mTimeZoneButton->setWhatsThis(i18nc("@info:whatsthis", "Choose a time zone for this alarm which is different from the default time zone set in KAlarm's configuration dialog.")); grid->addWidget(mTimeZoneButton, 2, 2, 1, 2, Qt::AlignRight); grid->setColumnStretch(2, 1); topLayout->addStretch(); QHBoxLayout* layout = new QHBoxLayout(); topLayout->addLayout(layout); layout->setSpacing(2 * style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); // Time zone selector mTimeZoneBox = new QWidget(topWidget); // this is to control the QWhatsThis text display area QHBoxLayout* hlayout = new QHBoxLayout(mTimeZoneBox); hlayout->setContentsMargins(0, 0, 0, 0); QLabel* label = new QLabel(i18nc("@label:listbox", "Time zone:"), mTimeZoneBox); hlayout->addWidget(label); mTimeZone = new TimeZoneCombo(mTimeZoneBox); hlayout->addWidget(mTimeZone); mTimeZone->setMaxVisibleItems(15); connect(mTimeZone, static_cast(&TimeZoneCombo::activated), this, &AlarmTimeWidget::slotTimeZoneChanged); mTimeZoneBox->setWhatsThis(i18nc("@info:whatsthis", "Select the time zone to use for this alarm.")); label->setBuddy(mTimeZone); layout->addWidget(mTimeZoneBox); layout->addStretch(); // Initially show only the time zone button, not time zone selector mTimeZoneBox->hide(); } // Initialise the radio button statuses mAtTimeRadio->setChecked(true); slotButtonSet(mAtTimeRadio); // Timeout every minute to update alarm time fields. MinuteTimer::connect(this, SLOT(updateTimes())); } /****************************************************************************** * Set or clear read-only status for the controls */ void AlarmTimeWidget::setReadOnly(bool ro) { mAtTimeRadio->setReadOnly(ro); mDateEdit->setOptions(ro ? KDateComboBox::Options{} : KDateComboBox::EditDate | KDateComboBox::SelectDate | KDateComboBox::DatePicker); mTimeEdit->setReadOnly(ro); if (mAnyTimeCheckBox) mAnyTimeCheckBox->setReadOnly(ro); mAfterTimeRadio->setReadOnly(ro); if (!mDeferring) mTimeZone->setReadOnly(ro); mDelayTimeEdit->setReadOnly(ro); } /****************************************************************************** * Select the "Time from now" radio button. */ void AlarmTimeWidget::selectTimeFromNow(int minutes) { mAfterTimeRadio->setChecked(true); if (minutes > 0) mDelayTimeEdit->setValue(minutes); } /****************************************************************************** * Fetch the entered date/time. * If 'checkExpired' is true and the entered value <= current time, an error occurs. * If 'minsFromNow' is non-null, it is set to the number of minutes' delay selected, * or to zero if a date/time was entered. * In this case, if 'showErrorMessage' is true, output an error message. * 'errorWidget' if non-null, is set to point to the widget containing the error. * Reply = invalid date/time if error. */ KADateTime AlarmTimeWidget::getDateTime(int* minsFromNow, bool checkExpired, bool showErrorMessage, QWidget** errorWidget) const { if (minsFromNow) *minsFromNow = 0; if (errorWidget) *errorWidget = nullptr; KADateTime now = KADateTime::currentUtcDateTime(); now.setTime(QTime(now.time().hour(), now.time().minute(), 0)); if (!mAtTimeRadio->isChecked()) { if (!mDelayTimeEdit->isValid()) { if (showErrorMessage) KAMessageBox::sorry(const_cast(this), i18nc("@info", "Invalid time")); if (errorWidget) *errorWidget = mDelayTimeEdit; return KADateTime(); } int delayMins = mDelayTimeEdit->value(); if (minsFromNow) *minsFromNow = delayMins; return now.addSecs(delayMins * 60).toTimeSpec(mTimeSpec); } else { bool dateOnly = mAnyTimeAllowed && mAnyTimeCheckBox && mAnyTimeCheckBox->isChecked(); if (!mDateEdit->date().isValid() || !mTimeEdit->isValid()) { // The date and/or time is invalid if (!mDateEdit->date().isValid()) { if (showErrorMessage) KAMessageBox::sorry(const_cast(this), i18nc("@info", "Invalid date")); if (errorWidget) *errorWidget = mDateEdit; } else { if (showErrorMessage) KAMessageBox::sorry(const_cast(this), i18nc("@info", "Invalid time")); if (errorWidget) *errorWidget = mTimeEdit; } return KADateTime(); } KADateTime result; if (dateOnly) { result = KADateTime(mDateEdit->date(), mTimeSpec); if (checkExpired && result.date() < now.date()) { if (showErrorMessage) KAMessageBox::sorry(const_cast(this), i18nc("@info", "Alarm date has already expired")); if (errorWidget) *errorWidget = mDateEdit; return KADateTime(); } } else { result = KADateTime(mDateEdit->date(), mTimeEdit->time(), mTimeSpec); if (checkExpired && result <= now.addSecs(1)) { if (showErrorMessage) KAMessageBox::sorry(const_cast(this), i18nc("@info", "Alarm time has already expired")); if (errorWidget) *errorWidget = mTimeEdit; return KADateTime(); } } return result; } } /****************************************************************************** * Set the date/time. */ void AlarmTimeWidget::setDateTime(const DateTime& dt) { // Set the time zone first so that the call to dateTimeChanged() works correctly. if (mDeferring) mTimeSpec = dt.timeSpec().isValid() ? dt.timeSpec() : KADateTime::LocalZone; else { const QTimeZone tz = (dt.timeSpec() == KADateTime::LocalZone) ? QTimeZone() : dt.timeZone(); mTimeZone->setTimeZone(tz); slotTimeZoneChanged(); } if (dt.date().isValid()) { mTimeEdit->setValue(dt.effectiveTime()); mDateEdit->setDate(dt.date()); dateTimeChanged(); // update the delay time edit box } else { mTimeEdit->setValid(false); mDateEdit->setDate(QDate()); mDelayTimeEdit->setValid(false); } if (mAnyTimeCheckBox) { bool dateOnly = dt.isDateOnly(); if (dateOnly) mAnyTimeAllowed = true; mAnyTimeCheckBox->setChecked(dateOnly); setAnyTime(); } } /****************************************************************************** * Set the minimum date/time to track the current time. */ void AlarmTimeWidget::setMinDateTimeIsCurrent() { mMinDateTimeIsNow = true; mMinDateTime = KADateTime(); const KADateTime now = KADateTime::currentDateTime(mTimeSpec); mDateEdit->setMinimumDate(now.date()); setMaxMinTimeIf(now); } /****************************************************************************** * Set the minimum date/time, adjusting the entered date/time if necessary. * If 'dt' is invalid, any current minimum date/time is cleared. */ void AlarmTimeWidget::setMinDateTime(const KADateTime& dt) { mMinDateTimeIsNow = false; mMinDateTime = dt.toTimeSpec(mTimeSpec); mDateEdit->setMinimumDate(mMinDateTime.date()); setMaxMinTimeIf(KADateTime::currentDateTime(mTimeSpec)); } /****************************************************************************** * Set the maximum date/time, adjusting the entered date/time if necessary. * If 'dt' is invalid, any current maximum date/time is cleared. */ void AlarmTimeWidget::setMaxDateTime(const DateTime& dt) { mPastMax = false; if (dt.isValid() && dt.isDateOnly()) mMaxDateTime = dt.effectiveKDateTime().addSecs(24*3600 - 60).toTimeSpec(mTimeSpec); else mMaxDateTime = dt.kDateTime().toTimeSpec(mTimeSpec); mDateEdit->setMaximumDate(mMaxDateTime.date()); const KADateTime now = KADateTime::currentDateTime(mTimeSpec); setMaxMinTimeIf(now); setMaxDelayTime(now); } /****************************************************************************** * If the minimum and maximum date/times fall on the same date, set the minimum * and maximum times in the time edit box. */ void AlarmTimeWidget::setMaxMinTimeIf(const KADateTime& now) { int mint = 0; QTime maxt = time_23_59; mMinMaxTimeSet = false; if (mMaxDateTime.isValid()) { bool set = true; KADateTime minDT; if (mMinDateTimeIsNow) minDT = now.addSecs(60); else if (mMinDateTime.isValid()) minDT = mMinDateTime; else set = false; if (set && mMaxDateTime.date() == minDT.date()) { // The minimum and maximum times are on the same date, so // constrain the time value. mint = minDT.time().hour()*60 + minDT.time().minute(); maxt = mMaxDateTime.time(); mMinMaxTimeSet = true; } } mTimeEdit->setMinimum(mint); mTimeEdit->setMaximum(maxt); mTimeEdit->setWrapping(!mint && maxt == time_23_59); } /****************************************************************************** * Set the maximum value for the delay time edit box, depending on the maximum * value for the date/time. */ void AlarmTimeWidget::setMaxDelayTime(const KADateTime& now) { int maxVal = maxDelayTime; if (mMaxDateTime.isValid()) { if (now.date().daysTo(mMaxDateTime.date()) < 100) // avoid possible 32-bit overflow on secsTo() { KADateTime dt(now); dt.setTime(QTime(now.time().hour(), now.time().minute(), 0)); // round down to nearest minute maxVal = dt.secsTo(mMaxDateTime) / 60; if (maxVal > maxDelayTime) maxVal = maxDelayTime; } } mDelayTimeEdit->setMaximum(maxVal); } /****************************************************************************** * Set the status for whether a time is specified, or just a date. */ void AlarmTimeWidget::setAnyTime() { int old = mAnyTime; mAnyTime = (mAtTimeRadio->isChecked() && mAnyTimeAllowed && mAnyTimeCheckBox && mAnyTimeCheckBox->isChecked()) ? 1 : 0; if (mAnyTime != old) Q_EMIT dateOnlyToggled(mAnyTime); } /****************************************************************************** * Enable/disable the "date only" radio button. */ void AlarmTimeWidget::enableAnyTime(bool enable) { if (mAnyTimeCheckBox) { mAnyTimeAllowed = enable; bool at = mAtTimeRadio->isChecked(); mAnyTimeCheckBox->setEnabled(enable && at); if (at) mTimeEdit->setEnabled(!enable || !mAnyTimeCheckBox->isChecked()); setAnyTime(); } } /****************************************************************************** * Called every minute to update the alarm time data entry fields. * If the maximum date/time has been reached, a 'pastMax()' signal is emitted. */ void AlarmTimeWidget::updateTimes() { KADateTime now; if (mMinDateTimeIsNow) { // Make sure that the minimum date is updated when the day changes now = KADateTime::currentDateTime(mTimeSpec); mDateEdit->setMinimumDate(now.date()); } if (mMaxDateTime.isValid()) { if (!now.isValid()) now = KADateTime::currentDateTime(mTimeSpec); if (!mPastMax) { // Check whether the maximum date/time has now been reached if (now.date() >= mMaxDateTime.date()) { // The current date has reached or has passed the maximum date if (now.date() > mMaxDateTime.date() || (!mAnyTime && now.time() > mTimeEdit->maxTime())) { mPastMax = true; Q_EMIT pastMax(); } else if (mMinDateTimeIsNow && !mMinMaxTimeSet) { // The minimum date/time tracks the clock, so set the minimum // and maximum times setMaxMinTimeIf(now); } } } setMaxDelayTime(now); } if (mAtTimeRadio->isChecked()) dateTimeChanged(); else delayTimeChanged(mDelayTimeEdit->value()); } /****************************************************************************** * Called when the radio button states have been changed. * Updates the appropriate edit box. */ void AlarmTimeWidget::slotButtonSet(QAbstractButton*) { bool at = mAtTimeRadio->isChecked(); mDateEdit->setEnabled(at); mTimeEdit->setEnabled(at && (!mAnyTimeAllowed || !mAnyTimeCheckBox || !mAnyTimeCheckBox->isChecked())); if (mAnyTimeCheckBox) mAnyTimeCheckBox->setEnabled(at && mAnyTimeAllowed); // Ensure that the value of the delay edit box is > 0. const KADateTime att(mDateEdit->date(), mTimeEdit->time(), mTimeSpec); int minutes = (KADateTime::currentUtcDateTime().secsTo(att) + 59) / 60; if (minutes <= 0) mDelayTimeEdit->setValid(true); mDelayTimeEdit->setEnabled(!at); setAnyTime(); } /****************************************************************************** * Called after the mAnyTimeCheckBox checkbox has been toggled. */ void AlarmTimeWidget::slotAnyTimeToggled(bool on) { on = (on && mAnyTimeAllowed); mTimeEdit->setEnabled(!on && mAtTimeRadio->isChecked()); setAnyTime(); if (on) Q_EMIT changed(KADateTime(mDateEdit->date(), mTimeSpec)); else Q_EMIT changed(KADateTime(mDateEdit->date(), mTimeEdit->time(), mTimeSpec)); } /****************************************************************************** * Called after a new selection has been made in the time zone combo box. * Re-evaluates the time specification to use. */ void AlarmTimeWidget::slotTimeZoneChanged() { const QTimeZone tz = mTimeZone->timeZone(); mTimeSpec = tz.isValid() ? KADateTime::Spec(tz) : KADateTime::LocalZone; if (!mTimeZoneBox->isVisible() && mTimeSpec != Preferences::timeSpec()) { // The current time zone is not the default one, so // show the time zone selection controls showTimeZoneSelector(); } mMinDateTime = mMinDateTime.toTimeSpec(mTimeSpec); mMaxDateTime = mMaxDateTime.toTimeSpec(mTimeSpec); updateTimes(); } /****************************************************************************** * Called after the mTimeZoneButton button has been clicked. * Show the time zone selection controls, and hide the button. */ void AlarmTimeWidget::showTimeZoneSelector() { mTimeZoneButton->hide(); mTimeZoneBox->show(); } /****************************************************************************** * Show or hide the time zone button. */ void AlarmTimeWidget::showMoreOptions(bool more) { if (more) { if (!mTimeZoneBox->isVisible()) mTimeZoneButton->show(); } else mTimeZoneButton->hide(); } /****************************************************************************** * Called when the date or time edit box values have changed. * Updates the time delay edit box accordingly. */ void AlarmTimeWidget::dateTimeChanged() { const KADateTime dt(mDateEdit->date(), mTimeEdit->time(), mTimeSpec); int minutes = (KADateTime::currentUtcDateTime().secsTo(dt) + 59) / 60; bool blocked = mDelayTimeEdit->signalsBlocked(); mDelayTimeEdit->blockSignals(true); // prevent infinite recursion between here and delayTimeChanged() if (minutes <= 0 || minutes > mDelayTimeEdit->maximum()) mDelayTimeEdit->setValid(false); else mDelayTimeEdit->setValue(minutes); mDelayTimeEdit->blockSignals(blocked); if (mAnyTimeAllowed && mAnyTimeCheckBox && mAnyTimeCheckBox->isChecked()) Q_EMIT changed(KADateTime(dt.date(), mTimeSpec)); else Q_EMIT changed(dt); } /****************************************************************************** * Called when the delay time edit box value has changed. * Updates the Date and Time edit boxes accordingly. */ void AlarmTimeWidget::delayTimeChanged(int minutes) { if (mDelayTimeEdit->isValid()) { QDateTime dt = KADateTime::currentUtcDateTime().addSecs(minutes * 60).toTimeSpec(mTimeSpec).qDateTime(); bool blockedT = mTimeEdit->signalsBlocked(); bool blockedD = mDateEdit->signalsBlocked(); mTimeEdit->blockSignals(true); // prevent infinite recursion between here and dateTimeChanged() mDateEdit->blockSignals(true); mTimeEdit->setValue(dt.time()); mDateEdit->setDate(dt.date()); mTimeEdit->blockSignals(blockedT); mDateEdit->blockSignals(blockedD); Q_EMIT changed(KADateTime(dt.date(), dt.time(), mTimeSpec)); } } // vim: et sw=4: diff --git a/src/birthdaydlg.cpp b/src/birthdaydlg.cpp index 86f4741b..5a397e49 100644 --- a/src/birthdaydlg.cpp +++ b/src/birthdaydlg.cpp @@ -1,378 +1,378 @@ /* * birthdaydlg.cpp - dialog to pick birthdays from address book * Program: kalarm * Copyright © 2002-2012,2018 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 "birthdaydlg.h" #include "alarmcalendar.h" #include "birthdaymodel.h" #include "checkbox.h" #include "editdlgtypes.h" #include "fontcolourbutton.h" #include "kalarmapp.h" #include "latecancel.h" #include "preferences.h" #include "reminder.h" #include "repetitionbutton.h" #include "shellprocess.h" #include "soundpicker.h" #include "specialactions.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 using namespace KCal; BirthdayDlg::BirthdayDlg(QWidget* parent) : QDialog(parent) { setObjectName(QStringLiteral("BirthdayDlg")); // used by LikeBack setWindowTitle(i18nc("@title:window", "Import Birthdays From KAddressBook")); QVBoxLayout* topLayout = new QVBoxLayout(this); topLayout->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); // Prefix and suffix to the name in the alarm text // Get default prefix and suffix texts from config file KConfigGroup config(KSharedConfig::openConfig(), "General"); mPrefixText = config.readEntry("BirthdayPrefix", i18nc("@info", "Birthday: ")); mSuffixText = config.readEntry("BirthdaySuffix"); QGroupBox* textGroup = new QGroupBox(i18nc("@title:group", "Alarm Text"), this); topLayout->addWidget(textGroup); QGridLayout* grid = new QGridLayout(textGroup); int dcm = style()->pixelMetric(QStyle::PM_DefaultChildMargin); grid->setContentsMargins(dcm, dcm, dcm, dcm); grid->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); QLabel* label = new QLabel(i18nc("@label:textbox", "Prefix:"), textGroup); label->setFixedSize(label->sizeHint()); grid->addWidget(label, 0, 0); mPrefix = new BLineEdit(mPrefixText, textGroup); mPrefix->setMinimumSize(mPrefix->sizeHint()); label->setBuddy(mPrefix); connect(mPrefix, &BLineEdit::focusLost, this, &BirthdayDlg::slotTextLostFocus); mPrefix->setWhatsThis(i18nc("@info:whatsthis", "Enter text to appear before the person's name in the alarm message, " "including any necessary trailing spaces.")); grid->addWidget(mPrefix, 0, 1); label = new QLabel(i18nc("@label:textbox", "Suffix:"), textGroup); label->setFixedSize(label->sizeHint()); grid->addWidget(label, 1, 0); mSuffix = new BLineEdit(mSuffixText, textGroup); mSuffix->setMinimumSize(mSuffix->sizeHint()); label->setBuddy(mSuffix); connect(mSuffix, &BLineEdit::focusLost, this, &BirthdayDlg::slotTextLostFocus); mSuffix->setWhatsThis(i18nc("@info:whatsthis", "Enter text to appear after the person's name in the alarm message, " "including any necessary leading spaces.")); grid->addWidget(mSuffix, 1, 1); QGroupBox* group = new QGroupBox(i18nc("@title:group", "Select Birthdays"), this); topLayout->addWidget(group); QVBoxLayout* layout = new QVBoxLayout(group); layout->setContentsMargins(0, 0, 0, 0); // Start Akonadi server as we need it for the birthday model to access contacts information Akonadi::ControlGui::start(); BirthdayModel* model = BirthdayModel::instance(); connect(model, &BirthdayModel::dataChanged, this, &BirthdayDlg::resizeViewColumns); KDescendantsProxyModel* descendantsModel = new KDescendantsProxyModel(this); descendantsModel->setSourceModel(model); Akonadi::EntityMimeTypeFilterModel* mimeTypeFilter = new Akonadi::EntityMimeTypeFilterModel(this); mimeTypeFilter->setSourceModel(descendantsModel); mimeTypeFilter->addMimeTypeExclusionFilter(Akonadi::Collection::mimeType()); mimeTypeFilter->setHeaderGroup(Akonadi::EntityTreeModel::ItemListHeaders); mBirthdaySortModel = new BirthdaySortModel(this); mBirthdaySortModel->setSourceModel(mimeTypeFilter); mBirthdaySortModel->setSortCaseSensitivity(Qt::CaseInsensitive); mBirthdaySortModel->setPrefixSuffix(mPrefixText, mSuffixText); mListView = new QTreeView(group); mListView->setEditTriggers(QAbstractItemView::NoEditTriggers); mListView->setModel(mBirthdaySortModel); mListView->setRootIsDecorated(false); // don't show expander icons mListView->setSortingEnabled(true); mListView->sortByColumn(BirthdayModel::NameColumn, mListView->header()->sortIndicatorOrder()); mListView->setAllColumnsShowFocus(true); mListView->setSelectionMode(QAbstractItemView::ExtendedSelection); mListView->setSelectionBehavior(QAbstractItemView::SelectRows); mListView->setTextElideMode(Qt::ElideRight); mListView->header()->setSectionResizeMode(BirthdayModel::NameColumn, QHeaderView::Stretch); mListView->header()->setSectionResizeMode(BirthdayModel::DateColumn, QHeaderView::ResizeToContents); connect(mListView->selectionModel(), &QItemSelectionModel::selectionChanged, this, &BirthdayDlg::slotSelectionChanged); mListView->setWhatsThis(xi18nc("@info:whatsthis", "Select birthdays to set alarms for." "This list shows all birthdays in KAddressBook except those for which alarms already exist." "You can select multiple birthdays at one time by dragging the mouse over the list, " "or by clicking the mouse while pressing Ctrl or Shift.")); layout->addWidget(mListView); group = new QGroupBox(i18nc("@title:group", "Alarm Configuration"), this); topLayout->addWidget(group); QVBoxLayout* groupLayout = new QVBoxLayout(group); groupLayout->setContentsMargins(dcm, dcm, dcm, dcm); groupLayout->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); // Sound checkbox and file selector QHBoxLayout* hlayout = new QHBoxLayout(); hlayout->setContentsMargins(0, 0, 0, 0); groupLayout->addLayout(hlayout); mSoundPicker = new SoundPicker(group); mSoundPicker->setFixedSize(mSoundPicker->sizeHint()); hlayout->addWidget(mSoundPicker); hlayout->addSpacing(2 * style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); hlayout->addStretch(); // Font and colour choice button and sample text mFontColourButton = new FontColourButton(group); mFontColourButton->setMaximumHeight(mFontColourButton->sizeHint().height() * 3/2); hlayout->addWidget(mFontColourButton); connect(mFontColourButton, &FontColourButton::selected, this, &BirthdayDlg::setColours); // How much advance warning to give mReminder = new Reminder(i18nc("@info:whatsthis", "Check to display a reminder in advance of or after the birthday."), i18nc("@info:whatsthis", "Enter the number of days before or after each birthday to display a reminder. " "This is in addition to the alarm which is displayed on the birthday."), i18nc("@info:whatsthis", "Select whether the reminder should be triggered before or after the birthday."), false, false, group); mReminder->setFixedSize(mReminder->sizeHint()); mReminder->setMaximum(0, 364); mReminder->setMinutes(0, true); groupLayout->addWidget(mReminder, 0, Qt::AlignLeft); // Acknowledgement confirmation required - default = no confirmation hlayout = new QHBoxLayout(); hlayout->setContentsMargins(0, 0, 0, 0); hlayout->setSpacing(2 * style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); groupLayout->addLayout(hlayout); mConfirmAck = EditDisplayAlarmDlg::createConfirmAckCheckbox(group); mConfirmAck->setFixedSize(mConfirmAck->sizeHint()); hlayout->addWidget(mConfirmAck); hlayout->addSpacing(2 * style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); hlayout->addStretch(); if (ShellProcess::authorised()) // don't display if shell commands not allowed (e.g. kiosk mode) { // Special actions button mSpecialActionsButton = new SpecialActionsButton(false, group); mSpecialActionsButton->setFixedSize(mSpecialActionsButton->sizeHint()); hlayout->addWidget(mSpecialActionsButton); } // Late display checkbox - default = allow late display hlayout = new QHBoxLayout(); hlayout->setContentsMargins(0, 0, 0, 0); hlayout->setSpacing(2 * style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); groupLayout->addLayout(hlayout); mLateCancel = new LateCancelSelector(false, group); mLateCancel->setFixedSize(mLateCancel->sizeHint()); hlayout->addWidget(mLateCancel); hlayout->addStretch(); // Sub-repetition button mSubRepetition = new RepetitionButton(i18nc("@action:button", "Sub-Repetition"), false, group); mSubRepetition->setFixedSize(mSubRepetition->sizeHint()); mSubRepetition->set(Repetition(), true, 364*24*60); mSubRepetition->setWhatsThis(i18nc("@info:whatsthis", "Set up an additional alarm repetition")); hlayout->addWidget(mSubRepetition); // Set the values to their defaults setColours(Preferences::defaultFgColour(), Preferences::defaultBgColour()); mFontColourButton->setDefaultFont(); mFontColourButton->setBgColour(Preferences::defaultBgColour()); mFontColourButton->setFgColour(Preferences::defaultFgColour()); mLateCancel->setMinutes(Preferences::defaultLateCancel(), true, TimePeriod::Days); mConfirmAck->setChecked(Preferences::defaultConfirmAck()); mSoundPicker->set(Preferences::defaultSoundType(), Preferences::defaultSoundFile(), Preferences::defaultSoundVolume(), -1, 0, Preferences::defaultSoundRepeat()); if (mSpecialActionsButton) { KAEvent::ExtraActionOptions opts{}; 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); } mButtonBox = new QDialogButtonBox(this); mButtonBox->addButton(QDialogButtonBox::Ok); mButtonBox->addButton(QDialogButtonBox::Cancel); connect(mButtonBox, &QDialogButtonBox::accepted, this, &BirthdayDlg::slotOk); connect(mButtonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); topLayout->addWidget(mButtonBox); KActionCollection* actions = new KActionCollection(this); KStandardAction::selectAll(mListView, SLOT(selectAll()), actions); KStandardAction::deselect(mListView, SLOT(clearSelection()), actions); actions->addAssociatedWidget(mListView); const auto lstActions = actions->actions(); for (QAction* action : lstActions) action->setShortcutContext(Qt::WidgetWithChildrenShortcut); mButtonBox->button(QDialogButtonBox::Ok)->setEnabled(false); // only enable OK button when something is selected } /****************************************************************************** * Return a list of events for birthdays chosen. */ QVector BirthdayDlg::events() const { QVector list; const QModelIndexList indexes = mListView->selectionModel()->selectedRows(); int count = indexes.count(); if (!count) return list; const QDate today = KADateTime::currentLocalDate(); const KADateTime todayStart(today, KADateTime::LocalZone); int thisYear = today.year(); int reminder = mReminder->minutes(); for (int i = 0; i < count; ++i) { const QModelIndex nameIndex = indexes[i].model()->index(indexes[i].row(), 0); const QModelIndex birthdayIndex = indexes[i].model()->index(indexes[i].row(), 1); const QString name = nameIndex.data(Qt::DisplayRole).toString(); QDate date = birthdayIndex.data(BirthdayModel::DateRole).toDate(); date.setDate(thisYear, date.month(), date.day()); if (date <= today) date.setDate(thisYear + 1, date.month(), date.day()); KAEvent event(KADateTime(date, KADateTime::LocalZone), mPrefix->text() + name + mSuffix->text(), mFontColourButton->bgColour(), mFontColourButton->fgColour(), mFontColourButton->font(), KAEvent::MESSAGE, mLateCancel->minutes(), mFlags, true); float fadeVolume; int fadeSecs; float volume = mSoundPicker->volume(fadeVolume, fadeSecs); int repeatPause = mSoundPicker->repeatPause(); event.setAudioFile(mSoundPicker->file().toDisplayString(), volume, fadeVolume, fadeSecs, repeatPause); const QVector months(1, date.month()); event.setRecurAnnualByDate(1, months, 0, KARecurrence::defaultFeb29Type(), -1, QDate()); event.setRepetition(mSubRepetition->repetition()); event.setNextOccurrence(todayStart); if (reminder) event.setReminder(reminder, false); if (mSpecialActionsButton) event.setActions(mSpecialActionsButton->preAction(), mSpecialActionsButton->postAction(), mSpecialActionsButton->options()); event.endChanges(); list.append(event); } return list; } /****************************************************************************** * Called when the OK button is selected to import the selected birthdays. */ void BirthdayDlg::slotOk() { // Save prefix and suffix texts to use as future defaults KConfigGroup config(KSharedConfig::openConfig(), "General"); config.writeEntry("BirthdayPrefix", mPrefix->text()); config.writeEntry("BirthdaySuffix", mSuffix->text()); config.sync(); mFlags = KAEvent::ANY_TIME; if (mSoundPicker->sound() == Preferences::Sound_Beep) mFlags |= KAEvent::BEEP; if (mSoundPicker->repeatPause() >= 0) mFlags |= KAEvent::REPEAT_SOUND; if (mConfirmAck->isChecked()) mFlags |= KAEvent::CONFIRM_ACK; if (mFontColourButton->defaultFont()) mFlags |= KAEvent::DEFAULT_FONT; QDialog::accept(); } /****************************************************************************** * Called when the group of items selected changes. * Enable/disable the OK button depending on whether anything is selected. */ void BirthdayDlg::slotSelectionChanged() { mButtonBox->button(QDialogButtonBox::Ok)->setEnabled(mListView->selectionModel()->hasSelection()); } /****************************************************************************** * Called when the font/color button has been clicked. * Set the colors in the message text entry control. */ void BirthdayDlg::setColours(const QColor& fgColour, const QColor& bgColour) { QPalette pal = mPrefix->palette(); pal.setColor(mPrefix->backgroundRole(), bgColour); pal.setColor(mPrefix->foregroundRole(), fgColour); mPrefix->setPalette(pal); mSuffix->setPalette(pal); } /****************************************************************************** * Called when the data has changed in the birthday list. * Resize the date column. */ void BirthdayDlg::resizeViewColumns() { mListView->resizeColumnToContents(BirthdayModel::DateColumn); } /****************************************************************************** * Called when the prefix or suffix text has lost keyboard focus. * If the text has changed, re-evaluates the selection list according to the new * birthday alarm text format. */ void BirthdayDlg::slotTextLostFocus() { QString prefix = mPrefix->text(); QString suffix = mSuffix->text(); if (prefix != mPrefixText || suffix != mSuffixText) { // Text has changed - re-evaluate the selection list mPrefixText = prefix; mSuffixText = suffix; mBirthdaySortModel->setPrefixSuffix(mPrefixText, mSuffixText); } } // vim: et sw=4: diff --git a/src/editdlg.cpp b/src/editdlg.cpp index 5d941db2..1a9d4954 100644 --- a/src/editdlg.cpp +++ b/src/editdlg.cpp @@ -1,1476 +1,1476 @@ /* * editdlg.cpp - dialog to create or modify an alarm or alarm template * Program: kalarm * Copyright © 2001-2019 David Jarvie * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "editdlg.h" #include "editdlg_p.h" #include "editdlgtypes.h" #include "alarmcalendar.h" #include "alarmtimewidget.h" #include "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 "resources/resources.h" #include "kalarm_debug.h" #include #include -#include +#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace KCal; using namespace KAlarmCal; static const char EDIT_DIALOG_NAME[] = "EditDialog"; static const char TEMPLATE_DIALOG_NAME[] = "EditTemplateDialog"; static const char EDIT_MORE_GROUP[] = "ShowOpts"; static const char EDIT_MORE_KEY[] = "EditMore"; static const int maxDelayTime = 99*60 + 59; // < 100 hours inline QString recurText(const KAEvent& event) { QString r; if (event.repetition()) r = QStringLiteral("%1 / %2").arg(event.recurrenceText()).arg(event.repetitionText()); else r = event.recurrenceText(); return i18nc("@title:tab", "Recurrence - [%1]", r); } QList EditAlarmDlg::mWindowList; // Collect these widget labels together to ensure consistent wording and // translations across different modules. QString EditAlarmDlg::i18n_chk_ShowInKOrganizer() { return i18nc("@option:check", "Show in KOrganizer"); } EditAlarmDlg* EditAlarmDlg::create(bool Template, Type type, QWidget* parent, GetResourceType getResource) { qCDebug(KALARM_LOG) << "EditAlarmDlg::create"; switch (type) { case DISPLAY: return new EditDisplayAlarmDlg(Template, parent, getResource); case COMMAND: return new EditCommandAlarmDlg(Template, parent, getResource); case EMAIL: return new EditEmailAlarmDlg(Template, parent, getResource); case AUDIO: return new EditAudioAlarmDlg(Template, parent, getResource); default: break; } return nullptr; } EditAlarmDlg* EditAlarmDlg::create(bool Template, const KAEvent* event, bool newAlarm, QWidget* parent, GetResourceType getResource, bool readOnly) { switch (event->actionTypes()) { case KAEvent::ACT_COMMAND: return new EditCommandAlarmDlg(Template, event, newAlarm, parent, getResource, readOnly); case KAEvent::ACT_DISPLAY_COMMAND: case KAEvent::ACT_DISPLAY: return new EditDisplayAlarmDlg(Template, event, newAlarm, parent, getResource, readOnly); case KAEvent::ACT_EMAIL: return new EditEmailAlarmDlg(Template, event, newAlarm, parent, getResource, readOnly); case KAEvent::ACT_AUDIO: return new EditAudioAlarmDlg(Template, event, newAlarm, parent, getResource, readOnly); default: break; } return nullptr; } /****************************************************************************** * Constructor. * Parameters: * Template = true to edit/create an alarm template * = false to edit/create an alarm. * event != to initialise the dialog to show the specified event's data. */ EditAlarmDlg::EditAlarmDlg(bool Template, KAEvent::SubAction action, QWidget* parent, GetResourceType getResource) : QDialog(parent) , mAlarmType(action) , mTemplate(Template) , mNewAlarm(true) , mDesiredReadOnly(false) , mReadOnly(false) { init(nullptr, getResource); mWindowList.append(this); } EditAlarmDlg::EditAlarmDlg(bool Template, const KAEvent* event, bool newAlarm, QWidget* parent, GetResourceType getResource, bool readOnly) : QDialog(parent) , mAlarmType(event->actionSubType()) , mEventId(newAlarm ? QString() : event->id()) , mTemplate(Template) , mNewAlarm(newAlarm) , mDesiredReadOnly(readOnly) , mReadOnly(readOnly) { init(event, getResource); mWindowList.append(this); } void EditAlarmDlg::init(const KAEvent* event, GetResourceType getResource) { switch (getResource) { case RES_USE_EVENT_ID: if (event) { mResourceEventId = event->id(); mUseResourceEventId = true; break; } Q_FALLTHROUGH(); // fall through to RES_PROMPT case RES_PROMPT: mResourceEventId.clear(); mUseResourceEventId = true; break; case RES_IGNORE: default: mResourceEventId.clear(); mUseResourceEventId = false; break; } } void EditAlarmDlg::init(const KAEvent* event) { setObjectName(mTemplate ? QStringLiteral("TemplEditDlg") : QStringLiteral("EditDlg")); // used by LikeBack QString caption; if (mReadOnly) caption = mTemplate ? i18nc("@title:window", "Alarm Template [read-only]") : event->expired() ? i18nc("@title:window", "Archived Alarm [read-only]") : i18nc("@title:window", "Alarm [read-only]"); else caption = type_caption(); setWindowTitle(caption); // Create button box now so that types can work with it, but don't insert // it into layout just yet mButtonBox = new QDialogButtonBox(this); if (mReadOnly) { mButtonBox->addButton(QDialogButtonBox::Cancel); mTryButton = mButtonBox->addButton(i18nc("@action:button", "Try"), QDialogButtonBox::ActionRole); mMoreLessButton = mButtonBox->addButton(QDialogButtonBox::RestoreDefaults); } else if (mTemplate) { mButtonBox->addButton(QDialogButtonBox::Ok); mButtonBox->addButton(QDialogButtonBox::Cancel); mTryButton = mButtonBox->addButton(i18nc("@action:button", "Try"), QDialogButtonBox::ActionRole); mMoreLessButton = mButtonBox->addButton(QDialogButtonBox::RestoreDefaults); } else { mButtonBox->addButton(QDialogButtonBox::Ok); mButtonBox->addButton(QDialogButtonBox::Cancel); mTryButton = mButtonBox->addButton(i18nc("@action:button", "Try"), QDialogButtonBox::ActionRole); mLoadTemplateButton = mButtonBox->addButton(i18nc("@action:button", "Load Template..."), QDialogButtonBox::HelpRole); mMoreLessButton = mButtonBox->addButton(QDialogButtonBox::RestoreDefaults); } connect(mButtonBox, &QDialogButtonBox::clicked, this, &EditAlarmDlg::slotButtonClicked); if (mButtonBox->button(QDialogButtonBox::Ok)) mButtonBox->button(QDialogButtonBox::Ok)->setWhatsThis(i18nc("@info:whatsthis", "Schedule the alarm at the specified time.")); QVBoxLayout* mainLayout = new QVBoxLayout(this); if (mTemplate) { QFrame* frame = new QFrame; QHBoxLayout* box = new QHBoxLayout(); frame->setLayout(box); box->setContentsMargins(0, 0, 0, 0); box->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); QLabel* label = new QLabel(i18nc("@label:textbox", "Template name:")); label->setFixedSize(label->sizeHint()); box->addWidget(label); mTemplateName = new QLineEdit(); mTemplateName->setReadOnly(mReadOnly); connect(mTemplateName, &QLineEdit::textEdited, this, &EditAlarmDlg::contentsChanged); label->setBuddy(mTemplateName); box->addWidget(mTemplateName); frame->setWhatsThis(i18nc("@info:whatsthis", "Enter the name of the alarm template")); frame->setFixedHeight(box->sizeHint().height()); mainLayout->addWidget(frame); } mTabs = new QTabWidget(this); mainLayout->addWidget(mTabs); mTabScrollGroup = new StackedScrollGroup(this, mTabs); StackedScrollWidget* mainScroll = new StackedScrollWidget(mTabScrollGroup); mTabs->addTab(mainScroll, i18nc("@title:tab", "Alarm")); mMainPageIndex = 0; PageFrame* mainPage = new PageFrame(mainScroll); mainScroll->setWidget(mainPage); // mainPage becomes the child of mainScroll connect(mainPage, &PageFrame::shown, this, &EditAlarmDlg::slotShowMainPage); QVBoxLayout* topLayout = new QVBoxLayout(mainPage); int dcm = style()->pixelMetric(QStyle::PM_DefaultChildMargin); topLayout->setContentsMargins(dcm, dcm, dcm, dcm); topLayout->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); // Recurrence tab StackedScrollWidget* recurScroll = new StackedScrollWidget(mTabScrollGroup); mTabs->addTab(recurScroll, QString()); mRecurPageIndex = 1; QFrame* recurTab = new QFrame; QVBoxLayout* recurTabLayout = new QVBoxLayout(); recurTabLayout->setContentsMargins(dcm, dcm, dcm, dcm); recurTab->setLayout(recurTabLayout); recurScroll->setWidget(recurTab); // recurTab becomes the child of recurScroll mRecurrenceEdit = new RecurrenceEdit(mReadOnly); recurTabLayout->addWidget(mRecurrenceEdit); connect(mRecurrenceEdit, &RecurrenceEdit::shown, this, &EditAlarmDlg::slotShowRecurrenceEdit); connect(mRecurrenceEdit, &RecurrenceEdit::typeChanged, this, &EditAlarmDlg::slotRecurTypeChange); connect(mRecurrenceEdit, &RecurrenceEdit::frequencyChanged, this, &EditAlarmDlg::slotRecurFrequencyChange); connect(mRecurrenceEdit, &RecurrenceEdit::repeatNeedsInitialisation, this, &EditAlarmDlg::slotSetSubRepetition); connect(mRecurrenceEdit, &RecurrenceEdit::contentsChanged, this, &EditAlarmDlg::contentsChanged); // Controls specific to the alarm type QGroupBox* actionBox = new QGroupBox(i18nc("@title:group", "Action"), mainPage); topLayout->addWidget(actionBox, 1); QVBoxLayout* layout = new QVBoxLayout(actionBox); layout->setContentsMargins(dcm, dcm, dcm, dcm); layout->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); type_init(actionBox, layout); if (!mTemplate) { // Deferred date/time: visible only for a deferred recurring event. mDeferGroup = new QGroupBox(i18nc("@title:group", "Deferred Alarm"), mainPage); topLayout->addWidget(mDeferGroup); QHBoxLayout* hlayout = new QHBoxLayout(mDeferGroup); hlayout->setContentsMargins(dcm, dcm, dcm, dcm); hlayout->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); QLabel* label = new QLabel(i18nc("@label", "Deferred to:"), mDeferGroup); label->setFixedSize(label->sizeHint()); hlayout->addWidget(label); mDeferTimeLabel = new QLabel(mDeferGroup); hlayout->addWidget(mDeferTimeLabel); mDeferChangeButton = new QPushButton(i18nc("@action:button", "Change..."), mDeferGroup); mDeferChangeButton->setFixedSize(mDeferChangeButton->sizeHint()); connect(mDeferChangeButton, &QPushButton::clicked, this, &EditAlarmDlg::slotEditDeferral); mDeferChangeButton->setWhatsThis(i18nc("@info:whatsthis", "Change the alarm's deferred time, or cancel the deferral")); hlayout->addWidget(mDeferChangeButton); //?? mDeferGroup->addSpace(0); } QHBoxLayout* hlayout = new QHBoxLayout(); hlayout->setContentsMargins(0, 0, 0, 0); topLayout->addLayout(hlayout); // Date and time entry if (mTemplate) { QGroupBox* templateTimeBox = new QGroupBox(i18nc("@title:group", "Time"), mainPage); topLayout->addWidget(templateTimeBox); QGridLayout* grid = new QGridLayout(templateTimeBox); grid->setContentsMargins(dcm, dcm, dcm, dcm); grid->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); mTemplateTimeGroup = new ButtonGroup(templateTimeBox); connect(mTemplateTimeGroup, &ButtonGroup::buttonSet, this, &EditAlarmDlg::slotTemplateTimeType); connect(mTemplateTimeGroup, &ButtonGroup::buttonSet, this, &EditAlarmDlg::contentsChanged); mTemplateDefaultTime = new RadioButton(i18nc("@option:radio", "Default time"), templateTimeBox); mTemplateDefaultTime->setFixedSize(mTemplateDefaultTime->sizeHint()); mTemplateDefaultTime->setReadOnly(mReadOnly); mTemplateDefaultTime->setWhatsThis(i18nc("@info:whatsthis", "Do not specify a start time for alarms based on this template. " "The normal default start time will be used.")); mTemplateTimeGroup->addButton(mTemplateDefaultTime); grid->addWidget(mTemplateDefaultTime, 0, 0, Qt::AlignLeft); QWidget* box = new QWidget(templateTimeBox); QHBoxLayout* layout = new QHBoxLayout(box); layout->setContentsMargins(0, 0, 0, 0); layout->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); mTemplateUseTime = new RadioButton(i18nc("@option:radio", "Time:"), box); mTemplateUseTime->setFixedSize(mTemplateUseTime->sizeHint()); mTemplateUseTime->setReadOnly(mReadOnly); mTemplateUseTime->setWhatsThis(i18nc("@info:whatsthis", "Specify a start time for alarms based on this template.")); layout->addWidget(mTemplateUseTime); mTemplateTimeGroup->addButton(mTemplateUseTime); mTemplateTime = new TimeEdit(); mTemplateTime->setFixedSize(mTemplateTime->sizeHint()); mTemplateTime->setReadOnly(mReadOnly); mTemplateTime->setWhatsThis(xi18nc("@info:whatsthis", "Enter the start time for alarms based on this template.%1", TimeSpinBox::shiftWhatsThis())); connect(mTemplateTime, &TimeEdit::valueChanged, this, &EditAlarmDlg::contentsChanged); layout->addWidget(mTemplateTime); layout->addStretch(1); grid->addWidget(box, 0, 1, Qt::AlignLeft); mTemplateAnyTime = new RadioButton(i18nc("@option:radio", "Date only"), templateTimeBox); mTemplateAnyTime->setFixedSize(mTemplateAnyTime->sizeHint()); mTemplateAnyTime->setReadOnly(mReadOnly); mTemplateAnyTime->setWhatsThis(xi18nc("@info:whatsthis", "Set the Any time option for alarms based on this template.")); mTemplateTimeGroup->addButton(mTemplateAnyTime); grid->addWidget(mTemplateAnyTime, 1, 0, Qt::AlignLeft); box = new QWidget(templateTimeBox); layout = new QHBoxLayout(box); layout->setContentsMargins(0, 0, 0, 0); layout->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); mTemplateUseTimeAfter = new RadioButton(i18nc("@option:radio", "Time from now:"), box); mTemplateUseTimeAfter->setFixedSize(mTemplateUseTimeAfter->sizeHint()); mTemplateUseTimeAfter->setReadOnly(mReadOnly); mTemplateUseTimeAfter->setWhatsThis(i18nc("@info:whatsthis", "Set alarms based on this template to start after the specified time " "interval from when the alarm is created.")); layout->addWidget(mTemplateUseTimeAfter); mTemplateTimeGroup->addButton(mTemplateUseTimeAfter); mTemplateTimeAfter = new TimeSpinBox(1, maxDelayTime); mTemplateTimeAfter->setValue(1439); mTemplateTimeAfter->setFixedSize(mTemplateTimeAfter->sizeHint()); mTemplateTimeAfter->setReadOnly(mReadOnly); connect(mTemplateTimeAfter, static_cast(&TimeSpinBox::valueChanged), this, &EditAlarmDlg::contentsChanged); mTemplateTimeAfter->setWhatsThis(xi18nc("@info:whatsthis", "%1%2", AlarmTimeWidget::i18n_TimeAfterPeriod(), TimeSpinBox::shiftWhatsThis())); layout->addWidget(mTemplateTimeAfter); box->setFixedHeight(box->sizeHint().height()); grid->addWidget(box, 1, 1, Qt::AlignLeft); hlayout->addStretch(); } else { mTimeWidget = new AlarmTimeWidget(i18nc("@title:group", "Time"), AlarmTimeWidget::AT_TIME, mainPage); connect(mTimeWidget, &AlarmTimeWidget::dateOnlyToggled, this, &EditAlarmDlg::slotAnyTimeToggled); connect(mTimeWidget, &AlarmTimeWidget::changed, this, &EditAlarmDlg::contentsChanged); topLayout->addWidget(mTimeWidget); } // Optional controls depending on More/Fewer Options button mMoreOptions = new QFrame(mainPage); mMoreOptions->setFrameStyle(QFrame::NoFrame); topLayout->addWidget(mMoreOptions); QVBoxLayout* moreLayout = new QVBoxLayout(mMoreOptions); moreLayout->setContentsMargins(0, 0, 0, 0); moreLayout->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); // Reminder mReminder = createReminder(mMoreOptions); if (mReminder) { mReminder->setFixedSize(mReminder->sizeHint()); connect(mReminder, &Reminder::changed, this, &EditAlarmDlg::contentsChanged); moreLayout->addWidget(mReminder, 0, Qt::AlignLeft); if (mTimeWidget) connect(mTimeWidget, &AlarmTimeWidget::changed, mReminder, &Reminder::setDefaultUnits); } // Late cancel selector - default = allow late display mLateCancel = new LateCancelSelector(true, mMoreOptions); connect(mLateCancel, &LateCancelSelector::changed, this, &EditAlarmDlg::contentsChanged); moreLayout->addWidget(mLateCancel, 0, Qt::AlignLeft); PackedLayout* playout = new PackedLayout(Qt::AlignJustify); playout->setSpacing(2 * style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); moreLayout->addLayout(playout); // Acknowledgement confirmation required - default = no confirmation CheckBox* confirmAck = type_createConfirmAckCheckbox(mMoreOptions); if (confirmAck) { confirmAck->setFixedSize(confirmAck->sizeHint()); connect(confirmAck, &CheckBox::toggled, this, &EditAlarmDlg::contentsChanged); playout->addWidget(confirmAck); } if (theApp()->korganizerEnabled()) { // Show in KOrganizer checkbox mShowInKorganizer = new CheckBox(i18n_chk_ShowInKOrganizer(), mMoreOptions); mShowInKorganizer->setFixedSize(mShowInKorganizer->sizeHint()); connect(mShowInKorganizer, &CheckBox::toggled, this, &EditAlarmDlg::contentsChanged); mShowInKorganizer->setWhatsThis(i18nc("@info:whatsthis", "Check to copy the alarm into KOrganizer's calendar")); playout->addWidget(mShowInKorganizer); } mainLayout->addWidget(mButtonBox); // Hide optional controls KConfigGroup config(KSharedConfig::openConfig(), EDIT_MORE_GROUP); showOptions(config.readEntry(EDIT_MORE_KEY, false)); // Initialise the state of all controls according to the specified event, if any initValues(event); if (mTemplateName) mTemplateName->setFocus(); if (!mNewAlarm) { // Save the initial state of all controls so that we can later tell if they have changed saveState((event && (mTemplate || !event->isTemplate())) ? event : nullptr); contentsChanged(); // enable/disable OK button } // Note the current desktop so that the dialog can be shown on it. // If a main window is visible, the dialog will by KDE default always appear on its // desktop. If the user invokes the dialog via the system tray on a different desktop, // that can cause confusion. mDesktop = KWindowSystem::currentDesktop(); if (theApp()->windowFocusBroken()) { const QList children = findChildren(); for (QWidget* w : children) w->installEventFilter(this); } } EditAlarmDlg::~EditAlarmDlg() { delete mButtonBox; mButtonBox = nullptr; // prevent text edit contentsChanged() signal triggering a crash delete mSavedEvent; mWindowList.removeAll(this); } /****************************************************************************** * Return the number of instances. */ int EditAlarmDlg::instanceCount() { return mWindowList.count(); } /****************************************************************************** * Initialise the dialog controls from the specified event. */ void EditAlarmDlg::initValues(const KAEvent* event) { setReadOnly(mDesiredReadOnly); mChanged = false; mOnlyDeferred = false; mExpiredRecurrence = false; mLateCancel->showAutoClose(false); bool deferGroupVisible = false; if (event) { // Set the values to those for the specified event if (mTemplate) mTemplateName->setText(event->templateName()); bool recurs = event->recurs(); if ((recurs || event->repetition()) && !mTemplate && event->deferred()) { deferGroupVisible = true; mDeferDateTime = event->deferDateTime(); mDeferTimeLabel->setText(mDeferDateTime.formatLocale()); mDeferGroup->show(); } if (mTemplate) { // Editing a template int afterTime = event->isTemplate() ? event->templateAfterTime() : -1; bool noTime = !afterTime; bool useTime = !event->mainDateTime().isDateOnly(); RadioButton* button = noTime ? mTemplateDefaultTime : (afterTime > 0) ? mTemplateUseTimeAfter : useTime ? mTemplateUseTime : mTemplateAnyTime; button->setChecked(true); mTemplateTimeAfter->setValue(afterTime > 0 ? afterTime : 1); if (!noTime && useTime) mTemplateTime->setValue(event->mainDateTime().kDateTime().time()); else mTemplateTime->setValue(0); } else { if (event->isTemplate()) { // Initialising from an alarm template: use current date const KADateTime now = KADateTime::currentDateTime(Preferences::timeSpec()); int afterTime = event->templateAfterTime(); if (afterTime >= 0) { mTimeWidget->setDateTime(now.addSecs(afterTime * 60)); mTimeWidget->selectTimeFromNow(); } else { KADateTime dt = event->startDateTime().kDateTime(); dt.setTimeSpec(Preferences::timeSpec()); QDate d = now.date(); if (!dt.isDateOnly() && now.time() >= dt.time()) d = d.addDays(1); // alarm time has already passed, so use tomorrow dt.setDate(d); mTimeWidget->setDateTime(dt); } } else { mExpiredRecurrence = recurs && event->mainExpired(); mTimeWidget->setDateTime(recurs || event->category() == CalEvent::ARCHIVED ? event->startDateTime() : event->mainExpired() ? event->deferDateTime() : event->mainDateTime()); } } KAEvent::SubAction action = event->actionSubType(); AlarmText altext; if (event->commandScript()) altext.setScript(event->cleanText()); else altext.setText(event->cleanText()); setAction(action, altext); mLateCancel->setMinutes(event->lateCancel(), event->startDateTime().isDateOnly(), TimePeriod::HoursMinutes); if (mShowInKorganizer) mShowInKorganizer->setChecked(event->copyToKOrganizer()); type_initValues(event); mRecurrenceEdit->set(*event); // must be called after mTimeWidget is set up, to ensure correct date-only enabling mTabs->setTabText(mRecurPageIndex, recurText(*event)); } else { // Set the values to their defaults const KADateTime defaultTime = KADateTime::currentUtcDateTime().addSecs(60).toTimeSpec(Preferences::timeSpec()); if (mTemplate) { mTemplateDefaultTime->setChecked(true); mTemplateTime->setValue(0); mTemplateTimeAfter->setValue(1); } else mTimeWidget->setDateTime(defaultTime); mLateCancel->setMinutes((Preferences::defaultLateCancel() ? 1 : 0), false, TimePeriod::HoursMinutes); if (mShowInKorganizer) mShowInKorganizer->setChecked(Preferences::defaultCopyToKOrganizer()); type_initValues(nullptr); mRecurrenceEdit->setDefaults(defaultTime); // must be called after mTimeWidget is set up, to ensure correct date-only enabling slotRecurFrequencyChange(); // update the Recurrence text } if (mReminder && mTimeWidget) mReminder->setDefaultUnits(mTimeWidget->getDateTime(nullptr, false, false)); if (!deferGroupVisible && mDeferGroup) mDeferGroup->hide(); bool empty = AlarmCalendar::resources()->events(CalEvent::TEMPLATE).isEmpty(); if (mLoadTemplateButton) mLoadTemplateButton->setEnabled(!empty); } /****************************************************************************** * Initialise various values in the New Alarm dialogue. */ void EditAlarmDlg::setTime(const DateTime& start) { mTimeWidget->setDateTime(start); } void EditAlarmDlg::setRecurrence(const KARecurrence& recur, const KCalendarCore::Duration& subRepeatInterval, int subRepeatCount) { KAEvent event; event.setTime(mTimeWidget->getDateTime(nullptr, false, false)); event.setRecurrence(recur); event.setRepetition(Repetition(subRepeatInterval, subRepeatCount - 1)); mRecurrenceEdit->set(event); } void EditAlarmDlg::setRepeatAtLogin() { mRecurrenceEdit->setRepeatAtLogin(); } void EditAlarmDlg::setLateCancel(int minutes) { mLateCancel->setMinutes(minutes, mTimeWidget->getDateTime(nullptr, false, false).isDateOnly(), TimePeriod::HoursMinutes); } void EditAlarmDlg::setShowInKOrganizer(bool show) { mShowInKorganizer->setChecked(show); } /****************************************************************************** * Set the read-only status of all non-template controls. */ void EditAlarmDlg::setReadOnly(bool readOnly) { mReadOnly = readOnly; if (mTimeWidget) mTimeWidget->setReadOnly(readOnly); mLateCancel->setReadOnly(readOnly); if (mDeferChangeButton) { if (readOnly) mDeferChangeButton->hide(); else mDeferChangeButton->show(); } if (mShowInKorganizer) mShowInKorganizer->setReadOnly(readOnly); } /****************************************************************************** * Save the state of all controls. */ void EditAlarmDlg::saveState(const KAEvent* event) { delete mSavedEvent; mSavedEvent = nullptr; if (event) mSavedEvent = new KAEvent(*event); if (mTemplate) { mSavedTemplateName = mTemplateName->text(); mSavedTemplateTimeType = mTemplateTimeGroup->checkedButton(); mSavedTemplateTime = mTemplateTime->time(); mSavedTemplateAfterTime = mTemplateTimeAfter->value(); } checkText(mSavedTextFileCommandMessage, false); if (mTimeWidget) mSavedDateTime = mTimeWidget->getDateTime(nullptr, false, false); mSavedLateCancel = mLateCancel->minutes(); if (mShowInKorganizer) mSavedShowInKorganizer = mShowInKorganizer->isChecked(); mSavedRecurrenceType = mRecurrenceEdit->repeatType(); mSavedDeferTime = mDeferDateTime.kDateTime(); } /****************************************************************************** * Check whether any of the controls has changed state since the dialog was * first displayed. * Reply = true if any non-deferral controls have changed, or if it's a new event. * = false if no non-deferral controls have changed. In this case, * mOnlyDeferred indicates whether deferral controls may have changed. */ bool EditAlarmDlg::stateChanged() const { mChanged = true; mOnlyDeferred = false; if (!mSavedEvent) return true; QString textFileCommandMessage; checkText(textFileCommandMessage, false); if (mTemplate) { if (mSavedTemplateName != mTemplateName->text() || mSavedTemplateTimeType != mTemplateTimeGroup->checkedButton() || (mTemplateUseTime->isChecked() && mSavedTemplateTime != mTemplateTime->time()) || (mTemplateUseTimeAfter->isChecked() && mSavedTemplateAfterTime != mTemplateTimeAfter->value())) return true; } else { const KADateTime dt = mTimeWidget->getDateTime(nullptr, false, false); if (mSavedDateTime.timeSpec() != dt.timeSpec() || mSavedDateTime != dt) return true; } if (mSavedLateCancel != mLateCancel->minutes() || (mShowInKorganizer && mSavedShowInKorganizer != mShowInKorganizer->isChecked()) || textFileCommandMessage != mSavedTextFileCommandMessage || mSavedRecurrenceType != mRecurrenceEdit->repeatType()) return true; if (type_stateChanged()) return true; if (mRecurrenceEdit->stateChanged()) return true; if (mSavedEvent && mSavedEvent->deferred()) mOnlyDeferred = true; mChanged = false; return false; } /****************************************************************************** * Called whenever any of the controls changes state. * Enable or disable the OK button depending on whether any controls have a * different state from their initial state. */ void EditAlarmDlg::contentsChanged() { // Don't do anything if it's a new alarm or we're still initialising // (i.e. mSavedEvent null). if (mSavedEvent && mButtonBox && mButtonBox->button(QDialogButtonBox::Ok)) mButtonBox->button(QDialogButtonBox::Ok)->setEnabled(stateChanged() || mDeferDateTime.kDateTime() != mSavedDeferTime); } /****************************************************************************** * Get the currently entered dialog data. * The data is returned in the supplied KAEvent instance. * Reply = false if the only change has been to an existing deferral. */ bool EditAlarmDlg::getEvent(KAEvent& event, Resource& resource) { resource = mResource; if (mChanged) { // It's a new event, or the edit controls have changed setEvent(event, mAlarmMessage, false); return true; } // Only the deferral time may have changed event = *mSavedEvent; if (mOnlyDeferred) { // Just modify the original event, to avoid expired recurring events // being returned as rubbish. if (mDeferDateTime.isValid()) event.defer(mDeferDateTime, event.reminderDeferral(), false); else event.cancelDefer(); } return false; } /****************************************************************************** * Extract the data in the dialog and set up a KAEvent from it. * If 'trial' is true, the event is set up for a simple one-off test, ignoring * recurrence, reminder, template etc. data. */ void EditAlarmDlg::setEvent(KAEvent& event, const QString& text, bool trial) { KADateTime dt; if (!trial) { if (!mTemplate) dt = mAlarmDateTime.effectiveKDateTime(); else if (mTemplateUseTime->isChecked()) dt = KADateTime(QDate(2000,1,1), mTemplateTime->time()); } int lateCancel = (trial || !mLateCancel->isEnabled()) ? 0 : mLateCancel->minutes(); type_setEvent(event, dt, text, lateCancel, trial); if (!trial) { if (mRecurrenceEdit->repeatType() != RecurrenceEdit::NO_RECUR) { mRecurrenceEdit->updateEvent(event, !mTemplate); const KADateTime now = KADateTime::currentDateTime(mAlarmDateTime.timeSpec()); bool dateOnly = mAlarmDateTime.isDateOnly(); if ((dateOnly && mAlarmDateTime.date() < now.date()) || (!dateOnly && mAlarmDateTime.kDateTime() < now)) { // A timed recurrence has an entered start date which has // already expired, so we must adjust the next repetition. event.setNextOccurrence(now); } mAlarmDateTime = event.startDateTime(); if (mDeferDateTime.isValid() && mDeferDateTime < mAlarmDateTime) { bool deferral = true; bool deferReminder = false; int reminder = mReminder ? mReminder->minutes() : 0; if (reminder) { DateTime remindTime = mAlarmDateTime.addMins(-reminder); if (mDeferDateTime >= remindTime) { if (remindTime > KADateTime::currentUtcDateTime()) deferral = false; // ignore deferral if it's after next reminder else if (mDeferDateTime > remindTime) deferReminder = true; // it's the reminder which is being deferred } } if (deferral) event.defer(mDeferDateTime, deferReminder, false); } } if (mTemplate) { int afterTime = mTemplateDefaultTime->isChecked() ? 0 : mTemplateUseTimeAfter->isChecked() ? mTemplateTimeAfter->value() : -1; event.setTemplate(mTemplateName->text(), afterTime); } } } /****************************************************************************** * Get the currently specified alarm flag bits. */ KAEvent::Flags EditAlarmDlg::getAlarmFlags() const { KAEvent::Flags flags{}; if (mShowInKorganizer && mShowInKorganizer->isEnabled() && mShowInKorganizer->isChecked()) flags |= KAEvent::COPY_KORGANIZER; if (mRecurrenceEdit->repeatType() == RecurrenceEdit::AT_LOGIN) flags |= KAEvent::REPEAT_AT_LOGIN; if (mTemplate ? mTemplateAnyTime->isChecked() : mAlarmDateTime.isDateOnly()) flags |= KAEvent::ANY_TIME; return flags; } /****************************************************************************** * Called when the dialog is displayed. * The first time through, sets the size to the same as the last time it was * displayed. */ void EditAlarmDlg::showEvent(QShowEvent* se) { QDialog::showEvent(se); if (!mDeferGroupHeight) { if (mDeferGroup) mDeferGroupHeight = mDeferGroup->height() + style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing); QSize s; if (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; Resource res; getEvent(event, res); // this may adjust mAlarmDateTime const KADateTime now = KADateTime::currentDateTime(mAlarmDateTime.timeSpec()); bool dateOnly = mAlarmDateTime.isDateOnly(); if ((dateOnly && mAlarmDateTime.date() < now.date()) || (!dateOnly && mAlarmDateTime.kDateTime() < now)) { // A timed recurrence has an entered start date which // has already expired, so we must adjust it. if (event.nextOccurrence(now, mAlarmDateTime, KAEvent::ALLOW_FOR_REPETITION) == KAEvent::NO_OCCURRENCE) { KAMessageBox::sorry(this, i18nc("@info", "Recurrence has already expired")); return false; } if (event.workTimeOnly() && !event.nextTrigger(KAEvent::DISPLAY_TRIGGER).isValid()) { if (KAMessageBox::warningContinueCancel(this, i18nc("@info", "The alarm will never occur during working hours")) != KMessageBox::Continue) return false; } } } QString errmsg; QWidget* errWidget = mRecurrenceEdit->checkData(mAlarmDateTime.effectiveKDateTime(), errmsg); if (errWidget) { mTabs->setCurrentIndex(mRecurPageIndex); errWidget->setFocus(); KAMessageBox::sorry(this, errmsg); return false; } } if (recurType != RecurrenceEdit::NO_RECUR) { KAEvent recurEvent; int longestRecurMinutes = -1; int reminder = mReminder ? mReminder->minutes() : 0; if (reminder && !mReminder->isOnceOnly()) { mRecurrenceEdit->updateEvent(recurEvent, false); longestRecurMinutes = recurEvent.longestRecurrenceInterval().asSeconds() / 60; if (longestRecurMinutes && reminder >= longestRecurMinutes) { mTabs->setCurrentIndex(mMainPageIndex); mReminder->setFocusOnCount(); KAMessageBox::sorry(this, xi18nc("@info", "Reminder period must be less than the recurrence interval, unless %1 is checked.", Reminder::i18n_chk_FirstRecurrenceOnly())); return false; } } if (mRecurrenceEdit->subRepetition()) { if (longestRecurMinutes < 0) { mRecurrenceEdit->updateEvent(recurEvent, false); longestRecurMinutes = recurEvent.longestRecurrenceInterval().asSeconds() / 60; } if (longestRecurMinutes > 0 && recurEvent.repetition().intervalMinutes() * recurEvent.repetition().count() >= longestRecurMinutes - reminder) { KAMessageBox::sorry(this, i18nc("@info", "The duration of a repetition within the recurrence must be less than the recurrence interval minus any reminder period")); mRecurrenceEdit->activateSubRepetition(); // display the alarm repetition dialog again return false; } if (!recurEvent.repetition().isDaily() && ((mTemplate && mTemplateAnyTime->isChecked()) || (!mTemplate && mAlarmDateTime.isDateOnly()))) { KAMessageBox::sorry(this, i18nc("@info", "For a repetition within the recurrence, its period must be in units of days or weeks for a date-only alarm")); mRecurrenceEdit->activateSubRepetition(); // display the alarm repetition dialog again return false; } } } if (!checkText(mAlarmMessage)) return false; mResource = Resource(); // mUseResourceEventId = false indicates that the caller already knows // which resource to use. if (mUseResourceEventId) { if (!mResourceEventId.isEmpty()) { mResource = Resources::resourceForEvent(mResourceEventId); if (mResource.isValid()) { CalEvent::Type type = mTemplate ? CalEvent::TEMPLATE : CalEvent::ACTIVE; if (!(mResource.alarmTypes() & type)) mResource = Resource(); // event may have expired while dialog was open } } bool cancelled = false; CalEvent::Type type = mTemplate ? CalEvent::TEMPLATE : CalEvent::ACTIVE; if (!mResource.isWritable(type)) mResource = Resources::destination(type, this, false, &cancelled); if (!mResource.isValid()) { if (!cancelled) KAMessageBox::sorry(this, i18nc("@info", "You must select a calendar to save the alarm in")); return false; } } return true; } /****************************************************************************** * Called when the Try button is clicked. * Display/execute the alarm immediately for the user to check its configuration. */ void EditAlarmDlg::slotTry() { QString text; if (checkText(text)) { if (!type_validate(true)) return; KAEvent event; setEvent(event, text, true); if (!mNewAlarm && !stateChanged()) { // It's an existing alarm which hasn't been changed yet: // enable KALARM_UID environment variable to be set. event.setEventId(mEventId); } type_aboutToTry(); void* result = theApp()->execAlarm(event, event.firstAlarm(), false, false); type_executedTry(text, result); } } /****************************************************************************** * Called when the Load Template button is clicked. * Prompt to select a template and initialise the dialog with its contents. */ void EditAlarmDlg::slotHelp() { KAEvent::Actions type; switch (mAlarmType) { case KAEvent::FILE: case KAEvent::MESSAGE: type = KAEvent::ACT_DISPLAY; break; case KAEvent::COMMAND: type = KAEvent::ACT_COMMAND; break; case KAEvent::EMAIL: type = KAEvent::ACT_EMAIL; break; case KAEvent::AUDIO: type = KAEvent::ACT_AUDIO; break; default: return; } // Use AutoQPointer to guard against crash on application exit while // the dialogue is still open. It prevents double deletion (both on // deletion of EditAlarmDlg, and on return from this function). AutoQPointer dlg = new TemplatePickDlg(type, this); if (dlg->exec() == QDialog::Accepted) { KAEvent event = dlg->selectedTemplate(); initValues(&event); } } /****************************************************************************** * Called when the More Options or Fewer Options buttons are clicked. * Show/hide the optional options and swap the More/Less buttons, and save the * new setting as the default from now on. */ void EditAlarmDlg::slotDefault() { showOptions(!mShowingMore); KConfigGroup config(KSharedConfig::openConfig(), EDIT_MORE_GROUP); config.writeEntry(EDIT_MORE_KEY, mShowingMore); } /****************************************************************************** * Show/hide the optional options and swap the More/Less buttons. */ void EditAlarmDlg::showOptions(bool more) { qCDebug(KALARM_LOG) << "EditAlarmDlg::showOptions:" << (more ? "More" : "Less"); if (more) { mMoreOptions->show(); mMoreLessButton->setText(i18nc("@action:Button", "Fewer Options <<")); } else { mMoreOptions->hide(); mMoreLessButton->setText(i18nc("@action:button", "More Options >>")); } if (mTimeWidget) mTimeWidget->showMoreOptions(more); type_showOptions(more); mRecurrenceEdit->showMoreOptions(more); mShowingMore = more; QTimer::singleShot(0, this, &EditAlarmDlg::slotResize); } /****************************************************************************** * Called when the Change deferral button is clicked. */ void EditAlarmDlg::slotEditDeferral() { if (!mTimeWidget) return; bool limit = true; const Repetition repetition = mRecurrenceEdit->subRepetition(); DateTime start = mSavedEvent->recurs() ? (mExpiredRecurrence ? DateTime() : mSavedEvent->mainDateTime()) : mTimeWidget->getDateTime(nullptr, !repetition, !mExpiredRecurrence); if (!start.isValid()) { if (!mExpiredRecurrence) return; limit = false; } const KADateTime now = KADateTime::currentUtcDateTime(); if (limit) { if (repetition && start < now) { // Sub-repetition - find the time of the next one int repeatNum = repetition.isDaily() ? (start.daysTo(now) + repetition.intervalDays() - 1) / repetition.intervalDays() : (start.secsTo(now) + repetition.intervalSeconds() - 1) / repetition.intervalSeconds(); if (repeatNum > repetition.count()) { mTimeWidget->getDateTime(); // output the appropriate error message return; } start = KADateTime(repetition.duration(repeatNum).end(start.qDateTime())); } } bool deferred = mDeferDateTime.isValid(); // Use AutoQPointer to guard against crash on application exit while // the dialogue is still open. It prevents double deletion (both on // deletion of EditAlarmDlg, and on return from this function). AutoQPointer deferDlg = new DeferAlarmDlg((deferred ? mDeferDateTime : DateTime(now.addSecs(60).toTimeSpec(start.timeSpec()))), start.isDateOnly(), deferred, this); deferDlg->setObjectName(QStringLiteral("EditDeferDlg")); // used by LikeBack if (limit) { // Don't allow deferral past the next recurrence int reminder = mReminder ? mReminder->minutes() : 0; if (reminder) { DateTime remindTime = start.addMins(-reminder); if (KADateTime::currentUtcDateTime() < remindTime) start = remindTime; } deferDlg->setLimit(start.addSecs(-60)); } if (deferDlg->exec() == QDialog::Accepted) { mDeferDateTime = deferDlg->getDateTime(); mDeferTimeLabel->setText(mDeferDateTime.isValid() ? mDeferDateTime.formatLocale() : QString()); contentsChanged(); } } /****************************************************************************** * Called when the main page is shown. * Sets the focus widget to the first edit field. */ void EditAlarmDlg::slotShowMainPage() { if (!mMainPageShown) { if (mTemplateName) mTemplateName->setFocus(); mMainPageShown = true; } else { // Set scroll position to top, since it otherwise jumps randomly StackedScrollWidget* main = static_cast(mTabs->widget(0)); main->verticalScrollBar()->setValue(0); } if (mTimeWidget) { if (!mReadOnly && mRecurPageShown && mRecurrenceEdit->repeatType() == RecurrenceEdit::AT_LOGIN) mTimeWidget->setDateTime(mRecurrenceEdit->endDateTime()); if (mReadOnly || mRecurrenceEdit->isTimedRepeatType()) mTimeWidget->setMinDateTime(); // don't set a minimum date/time else mTimeWidget->setMinDateTimeIsCurrent(); // set the minimum date/time to track the clock } } /****************************************************************************** * Called when the recurrence edit page is shown. * The recurrence defaults are set to correspond to the start date. * The first time, for a new alarm, the recurrence end date is set according to * the alarm start time. */ void EditAlarmDlg::slotShowRecurrenceEdit() { mRecurPageIndex = mTabs->currentIndex(); if (!mReadOnly && !mTemplate) { mAlarmDateTime = mTimeWidget->getDateTime(nullptr, false, false); const KADateTime now = KADateTime::currentDateTime(mAlarmDateTime.timeSpec()); bool expired = (mAlarmDateTime.effectiveKDateTime() < now); if (mRecurSetDefaultEndDate) { mRecurrenceEdit->setDefaultEndDate(expired ? now.date() : mAlarmDateTime.date()); mRecurSetDefaultEndDate = false; } mRecurrenceEdit->setStartDate(mAlarmDateTime.date(), now.date()); if (mRecurrenceEdit->repeatType() == RecurrenceEdit::AT_LOGIN) mRecurrenceEdit->setEndDateTime(expired ? now : mAlarmDateTime.kDateTime()); } mRecurPageShown = true; } /****************************************************************************** * Called when the recurrence type selection changes. * Enables/disables date-only alarms as appropriate. * Enables/disables controls depending on at-login setting. */ void EditAlarmDlg::slotRecurTypeChange(int repeatType) { bool atLogin = (mRecurrenceEdit->repeatType() == RecurrenceEdit::AT_LOGIN); if (!mTemplate) { bool recurs = (mRecurrenceEdit->repeatType() != RecurrenceEdit::NO_RECUR); if (mDeferGroup) mDeferGroup->setEnabled(recurs); mTimeWidget->enableAnyTime(!recurs || repeatType != RecurrenceEdit::SUBDAILY); if (atLogin) { mAlarmDateTime = mTimeWidget->getDateTime(nullptr, false, false); mRecurrenceEdit->setEndDateTime(mAlarmDateTime.kDateTime()); } if (mReminder) mReminder->enableOnceOnly(recurs && !atLogin); } if (mReminder) mReminder->setAfterOnly(atLogin); mLateCancel->setEnabled(!atLogin); if (mShowInKorganizer) mShowInKorganizer->setEnabled(!atLogin); slotRecurFrequencyChange(); } /****************************************************************************** * Called when the recurrence frequency selection changes, or the sub- * repetition interval changes. * Updates the recurrence frequency text. */ void EditAlarmDlg::slotRecurFrequencyChange() { slotSetSubRepetition(); KAEvent event; mRecurrenceEdit->updateEvent(event, false); mTabs->setTabText(mRecurPageIndex, recurText(event)); } /****************************************************************************** * Called when the Repetition within Recurrence button has been pressed to * display the sub-repetition dialog. * Alarm repetition has the following restrictions: * 1) Not allowed for a repeat-at-login alarm * 2) For a date-only alarm, the repeat interval must be a whole number of days. * 3) The overall repeat duration must be less than the recurrence interval. */ void EditAlarmDlg::slotSetSubRepetition() { bool dateOnly = mTemplate ? mTemplateAnyTime->isChecked() : mTimeWidget->anyTime(); mRecurrenceEdit->setSubRepetition((mReminder ? mReminder->minutes() : 0), dateOnly); } /****************************************************************************** * Called when one of the template time radio buttons is clicked, * to enable or disable the template time entry spin boxes. */ void EditAlarmDlg::slotTemplateTimeType(QAbstractButton*) { mTemplateTime->setEnabled(mTemplateUseTime->isChecked()); mTemplateTimeAfter->setEnabled(mTemplateUseTimeAfter->isChecked()); } /****************************************************************************** * Called when the "Any time" checkbox is toggled in the date/time widget. * Sets the advance reminder and late cancel units to days if any time is checked. */ void EditAlarmDlg::slotAnyTimeToggled(bool anyTime) { if (mReminder && mReminder->isReminder()) mReminder->setDateOnly(anyTime); mLateCancel->setDateOnly(anyTime); } bool EditAlarmDlg::dateOnly() const { return mTimeWidget ? mTimeWidget->anyTime() : mTemplateAnyTime->isChecked(); } bool EditAlarmDlg::isTimedRecurrence() const { return mRecurrenceEdit->isTimedRepeatType(); } void EditAlarmDlg::showMainPage() { mTabs->setCurrentIndex(mMainPageIndex); } #include "moc_editdlg.cpp" #include "moc_editdlg_p.cpp" // vim: et sw=4: diff --git a/src/editdlg_p.h b/src/editdlg_p.h index ca77c2de..fb02e7ec 100644 --- a/src/editdlg_p.h +++ b/src/editdlg_p.h @@ -1,86 +1,86 @@ /* * editdlg_p.h - private classes for editdlg.cpp * Program: kalarm * Copyright © 2003-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. */ #ifndef EDITDLG_P_H #define EDITDLG_P_H -#include +#include #include class QDragEnterEvent; class QShowEvent; class CheckBox; class LineEdit; class PageFrame : public QFrame { Q_OBJECT public: explicit PageFrame(QWidget* parent = nullptr) : QFrame(parent) { } protected: void showEvent(QShowEvent*) override { emit shown(); } Q_SIGNALS: void shown(); }; class TextEdit : public KTextEdit { Q_OBJECT public: explicit TextEdit(QWidget* parent); QSize sizeHint() const override { return minimumSizeHint(); } QSize minimumSizeHint() const override { return minimumSize(); } protected: void dragEnterEvent(QDragEnterEvent*) override; }; class CommandEdit : public QWidget { Q_OBJECT public: explicit CommandEdit(QWidget* parent); bool isScript() const; void setScript(bool); QString text() const; QString text(EditAlarmDlg*, bool showErrorMessage) const; void setText(const AlarmText&); void setReadOnly(bool); QSize minimumSizeHint() const override; QSize sizeHint() const override { return minimumSizeHint(); } Q_SIGNALS: void scriptToggled(bool); void changed(); // emitted when any changes occur private Q_SLOTS: void slotCmdScriptToggled(bool); private: CheckBox* mTypeScript; // entering a script LineEdit* mCommandEdit; // command line edit box TextEdit* mScriptEdit; // script edit box }; #endif // EDITDLG_P_H // vim: et sw=4: diff --git a/src/editdlgtypes.cpp b/src/editdlgtypes.cpp index 4a535eb2..dcf9c0d0 100644 --- a/src/editdlgtypes.cpp +++ b/src/editdlgtypes.cpp @@ -1,1872 +1,1872 @@ /* * editdlgtypes.cpp - dialogs to create or edit alarm or alarm template types * Program: kalarm * Copyright © 2001-2019 David Jarvie * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "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 "kalarm_debug.h" #include #include #include #include using namespace KCalendarCore; #include -#include +#include #include #include #include #include #include #include #include #include #include #include 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) { } bool pickFile(QString& file) override // called when browse button is pressed to select a log file { return KAlarm::browseFile(file, 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) { 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) { 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->setContentsMargins(0, 0, 0, 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->setContentsMargins(0, 0, 0, 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->setContentsMargins(0, 0, 0, 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->setContentsMargins(0, 0, 0, 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) { mAkonadiItemId = -1; lateCancel()->showAutoClose(true); if (event) { if (mAlarmType == KAEvent::MESSAGE && event->akonadiItemId() && AlarmText::checkIfEmail(event->cleanText())) mAkonadiItemId = event->akonadiItemId(); 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); mAkonadiItemId = alarmText.isEmail() ? alarmText.akonadiItemId() : -1; 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 = KAEvent(dt, text, mFontColourButton->bgColour(), mFontColourButton->fgColour(), mFontColourButton->font(), type, lateCancel, getAlarmFlags()); if (type == KAEvent::MESSAGE) { if (AlarmText::checkIfEmail(text)) event.setAkonadiItemId(mAkonadiItemId); } 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; if (KAlarm::browseFile(file, 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) << "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) << "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); int dcm = style()->pixelMetric(QStyle::PM_DefaultChildMargin); vlayout->setContentsMargins(dcm, dcm, dcm, dcm); 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->setContentsMargins(0, 0, 0, 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->setContentsMargins(0, 0, 0, 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 = KAEvent(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) { 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) { 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->setContentsMargins(0, 0, 0, 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->setContentsMargins(0, 0, 0, 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 KCalendarCore::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 = KAEvent(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 file; if (KAlarm::browseFile(file, i18nc("@title:window", "Choose File to Attach"), mAttachDefaultDir, QString(), QString(), true, this)) { if (!file.isEmpty()) { mEmailAttachList->addItem(file); 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) { 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) { 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->setContentsMargins(0, 0, 0, 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 = KAEvent(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->setContentsMargins(0, 0, 0, 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/find.cpp b/src/find.cpp index 48f0695b..8776e7c5 100644 --- a/src/find.cpp +++ b/src/find.cpp @@ -1,441 +1,441 @@ /* * find.cpp - search facility * Program: kalarm * Copyright © 2005-2019 David Jarvie * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "find.h" #include "alarmlistview.h" #include "eventlistview.h" #include "messagebox.h" #include "preferences.h" #include "resources/eventmodel.h" #include "kalarm_debug.h" #include -#include -#include -#include +#include +#include +#include #include #include #include #include #include #include #include #include #include using namespace KAlarmCal; // KAlarm-specific options for Find dialog enum { FIND_LIVE = KFind::MinimumUserOption, FIND_ARCHIVED = KFind::MinimumUserOption << 1, FIND_MESSAGE = KFind::MinimumUserOption << 2, FIND_FILE = KFind::MinimumUserOption << 3, FIND_COMMAND = KFind::MinimumUserOption << 4, FIND_EMAIL = KFind::MinimumUserOption << 5, FIND_AUDIO = KFind::MinimumUserOption << 6 }; static long FIND_KALARM_OPTIONS = FIND_LIVE | FIND_ARCHIVED | FIND_MESSAGE | FIND_FILE | FIND_COMMAND | FIND_EMAIL | FIND_AUDIO; Find::Find(EventListView* parent) : QObject(parent) , mListView(parent) , mDialog(nullptr) { connect(mListView->selectionModel(), &QItemSelectionModel::currentChanged, this, &Find::slotSelectionChanged); } Find::~Find() { delete mDialog; // automatically set to null delete mFind; mFind = nullptr; } void Find::slotSelectionChanged() { if (mDialog) mDialog->setHasCursor(mListView->selectionModel()->currentIndex().isValid()); } /****************************************************************************** * Display the Find dialog. */ void Find::display() { if (!mOptions) { // Set defaults the first time the Find dialog is activated mOptions = FIND_LIVE | FIND_ARCHIVED | FIND_MESSAGE | FIND_FILE | FIND_COMMAND | FIND_EMAIL | FIND_AUDIO; } bool noArchived = !Preferences::archivedKeepDays(); bool showArchived = qobject_cast(mListView) && (static_cast(mListView->model())->eventTypeFilter() & CalEvent::ARCHIVED); if (noArchived || !showArchived) // these settings could change between activations mOptions &= ~FIND_ARCHIVED; if (mDialog) KWindowSystem::activateWindow(mDialog->winId()); else { mDialog = new KFindDialog(mListView, mOptions, mHistory, (mListView->selectionModel()->selectedRows().count() > 1)); mDialog->setModal(false); mDialog->setObjectName(QStringLiteral("FindDlg")); mDialog->setHasSelection(false); QWidget* kalarmWidgets = mDialog->findExtension(); // Alarm types QVBoxLayout* layout = new QVBoxLayout(kalarmWidgets); layout->setContentsMargins(0, 0, 0, 0); layout->setSpacing(QApplication::style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); QGroupBox* group = new QGroupBox(i18nc("@title:group", "Alarm Type"), kalarmWidgets); layout->addWidget(group); QGridLayout* grid = new QGridLayout(group); int dcm = QApplication::style()->pixelMetric(QStyle::PM_DefaultChildMargin); grid->setContentsMargins(dcm, dcm, dcm, dcm); grid->setSpacing(QApplication::style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); grid->setColumnStretch(1, 1); // Live & archived alarm selection mLive = new QCheckBox(i18nc("@option:check Alarm type", "Active"), group); mLive->setFixedSize(mLive->sizeHint()); mLive->setWhatsThis(i18nc("@info:whatsthis", "Check to include active alarms in the search.")); grid->addWidget(mLive, 1, 0, Qt::AlignLeft); mArchived = new QCheckBox(i18nc("@option:check Alarm type", "Archived"), group); mArchived->setFixedSize(mArchived->sizeHint()); mArchived->setWhatsThis(i18nc("@info:whatsthis", "Check to include archived alarms in the search. " "This option is only available if archived alarms are currently being displayed.")); grid->addWidget(mArchived, 1, 2, Qt::AlignLeft); mActiveArchivedSep = new KSeparator(Qt::Horizontal, kalarmWidgets); grid->addWidget(mActiveArchivedSep, 2, 0, 1, 3); // Alarm actions mMessageType = new QCheckBox(i18nc("@option:check Alarm action = text display", "Text"), group); mMessageType->setFixedSize(mMessageType->sizeHint()); mMessageType->setWhatsThis(i18nc("@info:whatsthis", "Check to include text message alarms in the search.")); grid->addWidget(mMessageType, 3, 0); mFileType = new QCheckBox(i18nc("@option:check Alarm action = file display", "File"), group); mFileType->setFixedSize(mFileType->sizeHint()); mFileType->setWhatsThis(i18nc("@info:whatsthis", "Check to include file alarms in the search.")); grid->addWidget(mFileType, 3, 2); mCommandType = new QCheckBox(i18nc("@option:check Alarm action", "Command"), group); mCommandType->setFixedSize(mCommandType->sizeHint()); mCommandType->setWhatsThis(i18nc("@info:whatsthis", "Check to include command alarms in the search.")); grid->addWidget(mCommandType, 4, 0); mEmailType = new QCheckBox(i18nc("@option:check Alarm action", "Email"), group); mEmailType->setFixedSize(mEmailType->sizeHint()); mEmailType->setWhatsThis(i18nc("@info:whatsthis", "Check to include email alarms in the search.")); grid->addWidget(mEmailType, 4, 2); mAudioType = new QCheckBox(i18nc("@option:check Alarm action", "Audio"), group); mAudioType->setFixedSize(mAudioType->sizeHint()); mAudioType->setWhatsThis(i18nc("@info:whatsthis", "Check to include audio alarms in the search.")); grid->addWidget(mAudioType, 5, 0); // Set defaults mLive->setChecked(mOptions & FIND_LIVE); mArchived->setChecked(mOptions & FIND_ARCHIVED); mMessageType->setChecked(mOptions & FIND_MESSAGE); mFileType->setChecked(mOptions & FIND_FILE); mCommandType->setChecked(mOptions & FIND_COMMAND); mEmailType->setChecked(mOptions & FIND_EMAIL); mAudioType->setChecked(mOptions & FIND_AUDIO); connect(mDialog.data(), &KFindDialog::okClicked, this, &Find::slotFind); } // Only display active/archived options if archived alarms are being kept if (noArchived) { mLive->hide(); mArchived->hide(); mActiveArchivedSep->hide(); } else { mLive->show(); mArchived->show(); mActiveArchivedSep->show(); } // Disable options where no displayed alarms match them bool live = false; bool archived = false; bool text = false; bool file = false; bool command = false; bool email = false; bool audio = false; const int rowCount = mListView->model()->rowCount(); for (int row = 0; row < rowCount; ++row) { const KAEvent viewEvent = mListView->event(row); const KAEvent* event = &viewEvent; if (event->expired()) archived = true; else live = true; switch (event->actionTypes()) { case KAEvent::ACT_EMAIL: email = true; break; case KAEvent::ACT_AUDIO: audio = true; break; case KAEvent::ACT_COMMAND: command = true; break; case KAEvent::ACT_DISPLAY: if (event->actionSubType() == KAEvent::FILE) { file = true; break; } // fall through to ACT_DISPLAY_COMMAND Q_FALLTHROUGH(); case KAEvent::ACT_DISPLAY_COMMAND: default: text = true; break; } } mLive->setEnabled(live); mArchived->setEnabled(archived); mMessageType->setEnabled(text); mFileType->setEnabled(file); mCommandType->setEnabled(command); mEmailType->setEnabled(email); mAudioType->setEnabled(audio); mDialog->setHasCursor(mListView->selectionModel()->currentIndex().isValid()); mDialog->show(); } /****************************************************************************** * Called when the user requests a search by clicking the dialog OK button. */ void Find::slotFind() { if (!mDialog) return; mHistory = mDialog->findHistory(); // save search history so that it can be displayed again mOptions = mDialog->options() & ~FIND_KALARM_OPTIONS; if ((mOptions & KFind::RegularExpression) && !QRegExp(mDialog->pattern()).isValid()) return; mOptions |= (mLive->isEnabled() && mLive->isChecked() ? FIND_LIVE : 0) | (mArchived->isEnabled() && mArchived->isChecked() ? FIND_ARCHIVED : 0) | (mMessageType->isEnabled() && mMessageType->isChecked() ? FIND_MESSAGE : 0) | (mFileType->isEnabled() && mFileType->isChecked() ? FIND_FILE : 0) | (mCommandType->isEnabled() && mCommandType->isChecked() ? FIND_COMMAND : 0) | (mEmailType->isEnabled() && mEmailType->isChecked() ? FIND_EMAIL : 0) | (mAudioType->isEnabled() && mAudioType->isChecked() ? FIND_AUDIO : 0); if (!(mOptions & (FIND_LIVE | FIND_ARCHIVED)) || !(mOptions & (FIND_MESSAGE | FIND_FILE | FIND_COMMAND | FIND_EMAIL | FIND_AUDIO))) { KAMessageBox::sorry(mDialog, i18nc("@info", "No alarm types are selected to search")); return; } // Supply KFind with only those options which relate to the text within alarms const long options = mOptions & (KFind::WholeWordsOnly | KFind::CaseSensitive | KFind::RegularExpression); const bool newFind = !mFind; const bool newPattern = (mDialog->pattern() != mLastPattern); mLastPattern = mDialog->pattern(); if (mFind) { mFind->resetCounts(); mFind->setPattern(mLastPattern); mFind->setOptions(options); } else { mFind = new KFind(mLastPattern, options, mListView, mDialog); connect(mFind, &KFind::destroyed, this, &Find::slotKFindDestroyed); mFind->closeFindNextDialog(); // prevent 'Find Next' dialog appearing } // Set the starting point for the search mStartID.clear(); mNoCurrentItem = newPattern; bool checkEnd = false; if (newPattern) { mFound = false; if (mOptions & KFind::FromCursor) { const QModelIndex index = mListView->selectionModel()->currentIndex(); if (index.isValid()) { mStartID = mListView->event(index).id(); mNoCurrentItem = false; checkEnd = true; } } } // Execute the search findNext(true, checkEnd, false); if (mFind && newFind) Q_EMIT active(true); } /****************************************************************************** * Perform the search. * If 'fromCurrent' is true, the search starts with the current search item; * otherwise, it starts from the next item. */ void Find::findNext(bool forward, bool checkEnd, bool fromCurrent) { QModelIndex index; if (!mNoCurrentItem) index = mListView->selectionModel()->currentIndex(); if (!fromCurrent) index = nextItem(index, forward); // Search successive alarms until a match is found or the end is reached bool found = false; bool last = false; for ( ; index.isValid() && !last; index = nextItem(index, forward)) { const KAEvent viewEvent = mListView->event(index); const KAEvent* event = &viewEvent; if (!fromCurrent && !mStartID.isNull() && mStartID == event->id()) last = true; // we've wrapped round and reached the starting alarm again fromCurrent = false; const bool live = !event->expired(); if ((live && !(mOptions & FIND_LIVE)) || (!live && !(mOptions & FIND_ARCHIVED))) continue; // we're not searching this type of alarm switch (event->actionTypes()) { case KAEvent::ACT_EMAIL: if (!(mOptions & FIND_EMAIL)) break; mFind->setData(event->emailAddresses(QStringLiteral(", "))); found = (mFind->find() == KFind::Match); if (found) break; mFind->setData(event->emailSubject()); found = (mFind->find() == KFind::Match); if (found) break; mFind->setData(event->emailAttachments().join(QLatin1String(", "))); found = (mFind->find() == KFind::Match); if (found) break; mFind->setData(event->cleanText()); found = (mFind->find() == KFind::Match); break; case KAEvent::ACT_AUDIO: if (!(mOptions & FIND_AUDIO)) break; mFind->setData(event->audioFile()); found = (mFind->find() == KFind::Match); break; case KAEvent::ACT_COMMAND: if (!(mOptions & FIND_COMMAND)) break; mFind->setData(event->cleanText()); found = (mFind->find() == KFind::Match); break; case KAEvent::ACT_DISPLAY: if (event->actionSubType() == KAEvent::FILE) { if (!(mOptions & FIND_FILE)) break; mFind->setData(event->cleanText()); found = (mFind->find() == KFind::Match); break; } // fall through to ACT_DISPLAY_COMMAND Q_FALLTHROUGH(); case KAEvent::ACT_DISPLAY_COMMAND: if (!(mOptions & FIND_MESSAGE)) break; mFind->setData(event->cleanText()); found = (mFind->find() == KFind::Match); break; default: break; } if (found) break; } // Process the search result mNoCurrentItem = !index.isValid(); if (found) { // A matching alarm was found - highlight it and make it current mFound = true; QItemSelectionModel* sel = mListView->selectionModel(); sel->select(index, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows); sel->setCurrentIndex(index, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows); mListView->scrollTo(index); } else { // No match was found if (mFound || checkEnd) { const QString msg = forward ? xi18nc("@info", "End of alarm list reached.Continue from the beginning?") : xi18nc("@info", "Beginning of alarm list reached.Continue from the end?"); if (KAMessageBox::questionYesNo(mListView, msg, QString(), KStandardGuiItem::cont(), KStandardGuiItem::cancel()) == KMessageBox::Yes) { mNoCurrentItem = true; findNext(forward, false, false); return; } } else mFind->displayFinalDialog(); // display "no match was found" mNoCurrentItem = false; // restart from the currently highlighted alarm if Find Next etc selected } } /****************************************************************************** * Get the next alarm item to search. */ QModelIndex Find::nextItem(const QModelIndex& index, bool forward) const { if (mOptions & KFind::FindBackwards) forward = !forward; if (!index.isValid()) { QAbstractItemModel* model = mListView->model(); if (forward) return model->index(0, 0); else return model->index(model->rowCount() - 1, 0); } if (forward) return mListView->indexBelow(index); else return mListView->indexAbove(index); } // vim: et sw=4: diff --git a/src/fontcolour.cpp b/src/fontcolour.cpp index bcf68a83..f6d6eae5 100644 --- a/src/fontcolour.cpp +++ b/src/fontcolour.cpp @@ -1,227 +1,227 @@ /* * fontcolour.cpp - font and colour chooser widget * Program: kalarm * Copyright © 2001-2019 David Jarvie * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "fontcolour.h" #include "kalarmapp.h" #include "preferences.h" #include "colourbutton.h" #include "checkbox.h" -#include +#include #include #include #include #include #include #include FontColourChooser::FontColourChooser(QWidget* parent, const QStringList& fontList, const QString& frameLabel, bool fg, bool defaultFont, int visibleListSize) : QWidget(parent) { QVBoxLayout* topLayout = new QVBoxLayout(this); topLayout->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); QWidget* page = this; if (!frameLabel.isNull()) { page = new QGroupBox(frameLabel, this); topLayout->addWidget(page); topLayout = new QVBoxLayout(page); int dcm = style()->pixelMetric(QStyle::PM_DefaultChildMargin); topLayout->setContentsMargins(dcm, dcm, dcm, dcm); topLayout->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); } QHBoxLayout* hlayout = new QHBoxLayout(); hlayout->setContentsMargins(0, 0, 0, 0); topLayout->addLayout(hlayout); QVBoxLayout* colourLayout = new QVBoxLayout(); colourLayout->setContentsMargins(0, 0, 0, 0); hlayout->addLayout(colourLayout); if (fg) { QWidget* box = new QWidget(page); // to group widgets for QWhatsThis text colourLayout->addWidget(box); QHBoxLayout* boxHLayout = new QHBoxLayout(box); boxHLayout->setContentsMargins(0, 0, 0, 0); boxHLayout->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing) / 2); QLabel* label = new QLabel(i18nc("@label:listbox", "Foreground color:"), box); boxHLayout->addWidget(label); boxHLayout->setStretchFactor(new QWidget(box), 0); mFgColourButton = new ColourButton(box); boxHLayout->addWidget(mFgColourButton); connect(mFgColourButton, &ColourButton::changed, this, &FontColourChooser::setSampleColour); label->setBuddy(mFgColourButton); box->setWhatsThis(i18nc("@info:whatsthis", "Select the alarm message foreground color")); } QWidget* box = new QWidget(page); // to group widgets for QWhatsThis text colourLayout->addWidget(box); QHBoxLayout* boxHLayout = new QHBoxLayout(box); boxHLayout->setContentsMargins(0, 0, 0, 0); boxHLayout->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing) / 2); QLabel* label = new QLabel(i18nc("@label:listbox", "Background color:"), box); boxHLayout->addWidget(label); boxHLayout->setStretchFactor(new QWidget(box), 0); mBgColourButton = new ColourButton(box); boxHLayout->addWidget(mBgColourButton); connect(mBgColourButton, &ColourButton::changed, this, &FontColourChooser::setSampleColour); label->setBuddy(mBgColourButton); box->setWhatsThis(i18nc("@info:whatsthis", "Select the alarm message background color")); hlayout->addStretch(); if (defaultFont) { QHBoxLayout* layout = new QHBoxLayout(); layout->setContentsMargins(0, 0, 0, 0); topLayout->addLayout(layout); mDefaultFont = new CheckBox(i18nc("@option:check", "Use default font"), page); mDefaultFont->setMinimumSize(mDefaultFont->sizeHint()); connect(mDefaultFont, &CheckBox::toggled, this, &FontColourChooser::slotDefaultFontToggled); mDefaultFont->setWhatsThis(i18nc("@info:whatsthis", "Check to use the default font current at the time the alarm is displayed.")); layout->addWidget(mDefaultFont); layout->addWidget(new QWidget(page)); // left adjust the widget } mFontChooser = new KFontChooser(page, KFontChooser::DisplayFrame, fontList, visibleListSize); mFontChooser->installEventFilter(this); // for read-only mode QList kids = mFontChooser->findChildren(); for (int i = 0, end = kids.count(); i < end; ++i) kids[i]->installEventFilter(this); topLayout->addWidget(mFontChooser); slotDefaultFontToggled(false); } void FontColourChooser::setDefaultFont() { if (mDefaultFont) mDefaultFont->setChecked(true); } void FontColourChooser::setFont(const QFont& font, bool onlyFixed) { if (mDefaultFont) mDefaultFont->setChecked(false); mFontChooser->setFont(font, onlyFixed); } bool FontColourChooser::defaultFont() const { return mDefaultFont ? mDefaultFont->isChecked() : false; } QFont FontColourChooser::font() const { return (mDefaultFont && mDefaultFont->isChecked()) ? QFont() : mFontChooser->font(); } void FontColourChooser::setBgColour(const QColor& colour) { mBgColourButton->setColor(colour); mFontChooser->setBackgroundColor(colour); } void FontColourChooser::setSampleColour() { QColor bg = mBgColourButton->color(); mFontChooser->setBackgroundColor(bg); QColor fg = fgColour(); mFontChooser->setColor(fg); } QColor FontColourChooser::bgColour() const { return mBgColourButton->color(); } QColor FontColourChooser::fgColour() const { if (mFgColourButton) return mFgColourButton->color(); else { QColor bg = mBgColourButton->color(); QPalette pal(bg, bg); return pal.color(QPalette::Active, QPalette::Text); } } QString FontColourChooser::sampleText() const { return mFontChooser->sampleText(); } void FontColourChooser::setSampleText(const QString& text) { mFontChooser->setSampleText(text); } void FontColourChooser::setFgColour(const QColor& colour) { if (mFgColourButton) { mFgColourButton->setColor(colour); mFontChooser->setColor(colour); } } void FontColourChooser::setReadOnly(bool ro) { if (ro != mReadOnly) { mReadOnly = ro; if (mFgColourButton) mFgColourButton->setReadOnly(ro); mBgColourButton->setReadOnly(ro); mDefaultFont->setReadOnly(ro); } } bool FontColourChooser::eventFilter(QObject*, QEvent* e) { if (mReadOnly) { switch (e->type()) { case QEvent::MouseMove: case QEvent::MouseButtonPress: case QEvent::MouseButtonRelease: case QEvent::MouseButtonDblClick: case QEvent::KeyPress: case QEvent::KeyRelease: return true; // prevent the event being handled default: break; } } return false; } void FontColourChooser::slotDefaultFontToggled(bool on) { mFontChooser->setEnabled(!on); } // vim: et sw=4: diff --git a/src/functions.cpp b/src/functions.cpp index 4b493b9a..8b6933a8 100644 --- a/src/functions.cpp +++ b/src/functions.cpp @@ -1,1883 +1,1883 @@ /* * functions.cpp - miscellaneous functions * Program: kalarm * Copyright © 2001-2019 David Jarvie * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "functions.h" #include "functions_p.h" #include "collectionsearch.h" #include "alarmcalendar.h" #include "alarmtime.h" #include "autoqpointer.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 "resources/resources.h" #include "resources/eventmodel.h" #include "config-kalarm.h" #include "kalarm_debug.h" #include #include #include #include #include #include #include using namespace KCalendarCore; #include #include #include -#include +#include #include -#include -#include +#include +#include #include #include -#include -#include +#include +#include #include #include -#include +#include #include #include #include #include #include #include #include #include #include #include namespace { bool refreshAlarmsQueued = false; struct UpdateStatusData { KAlarm::UpdateResult status; // status code and KOrganizer error message if any int warnErr; int warnKOrg; explicit UpdateStatusData(KAlarm::UpdateStatus s = KAlarm::UPDATE_OK) : status(s), warnErr(0), warnKOrg(0) {} // Set an error status and increment to number of errors to warn about void setError(KAlarm::UpdateStatus st, int errorCount = -1) { status.set(st); if (errorCount < 0) ++warnErr; else warnErr = errorCount; } // Update the error status with a KOrganizer related status void korgUpdate(const KAlarm::UpdateResult& result) { if (result.status != KAlarm::UPDATE_OK) { ++warnKOrg; if (result.status > status.status) status = result; } } }; const QLatin1String KMAIL_DBUS_SERVICE("org.kde.kmail"); //const QLatin1String KMAIL_DBUS_IFACE("org.kde.kmail.kmail"); //const QLatin1String KMAIL_DBUS_WINDOW_PATH("/kmail/kmail_mainwindow_1"); const QLatin1String KORG_DBUS_SERVICE("org.kde.korganizer"); const QLatin1String KORG_DBUS_IFACE("org.kde.korganizer.Korganizer"); // D-Bus object path of KOrganizer's notification interface #define KORG_DBUS_PATH "/Korganizer" #define KORG_DBUS_LOAD_PATH "/korganizer_PimApplication" //const QLatin1String KORG_DBUS_WINDOW_PATH("/korganizer/MainWindow_1"); const QLatin1String KORG_MIME_TYPE("application/x-vnd.akonadi.calendar.event"); const QLatin1String KORGANIZER_UID("korg-"); const QLatin1String ALARM_OPTS_FILE("alarmopts"); const char* DONT_SHOW_ERRORS_GROUP = "DontShowErrors"; void editNewTemplate(EditAlarmDlg::Type, const KAEvent* preset, QWidget* parent); void displayUpdateError(QWidget* parent, KAlarm::UpdateError, const UpdateStatusData&, bool showKOrgError = true); KAlarm::UpdateResult sendToKOrganizer(const KAEvent&); KAlarm::UpdateResult deleteFromKOrganizer(const QString& eventID); KAlarm::UpdateResult runKOrganizer(); QString uidKOrganizer(const QString& eventID); } namespace KAlarm { Private* Private::mInstance = nullptr; /****************************************************************************** * Display a main window with the specified event selected. */ MainWindow* displayMainWindowSelected(const QString& eventId) { MainWindow* win = MainWindow::firstWindow(); if (!win) { if (theApp()->checkCalendar()) // ensure calendar is open { win = MainWindow::create(); win->show(); } } else { // There is already a main window, so make it the active window #pragma message("Don't hide unless necessary, since it moves the window") win->hide(); // in case it's on a different desktop win->setWindowState(win->windowState() & ~Qt::WindowMinimized); win->show(); win->raise(); win->activateWindow(); } if (win) win->selectEvent(eventId); return win; } /****************************************************************************** * Create an "Alarms Enabled/Enable Alarms" action. */ KToggleAction* createAlarmEnableAction(QObject* parent) { KToggleAction* action = new KToggleAction(i18nc("@action", "Enable &Alarms"), parent); action->setChecked(theApp()->alarmsEnabled()); QObject::connect(action, &QAction::toggled, theApp(), &KAlarmApp::setAlarmsEnabled); // The following line ensures that all instances are kept in the same state QObject::connect(theApp(), &KAlarmApp::alarmEnabledToggled, action, &QAction::setChecked); return action; } /****************************************************************************** * Create a "Stop Play" action. */ QAction* createStopPlayAction(QObject* parent) { QAction* action = new QAction(QIcon::fromTheme(QStringLiteral("media-playback-stop")), i18nc("@action", "Stop Play"), parent); action->setEnabled(MessageWin::isAudioPlaying()); QObject::connect(action, &QAction::triggered, theApp(), &KAlarmApp::stopAudio); // The following line ensures that all instances are kept in the same state QObject::connect(theApp(), &KAlarmApp::audioPlaying, action, &QAction::setEnabled); return action; } /****************************************************************************** * Create a "Spread Windows" action. */ KToggleAction* createSpreadWindowsAction(QObject* parent) { KToggleAction* action = new KToggleAction(i18nc("@action", "Spread Windows"), parent); QObject::connect(action, &QAction::triggered, theApp(), &KAlarmApp::spreadWindows); // The following line ensures that all instances are kept in the same state QObject::connect(theApp(), &KAlarmApp::spreadWindowsToggled, action, &QAction::setChecked); return action; } /****************************************************************************** * Add a new active (non-archived) alarm. * Save it in the calendar file and add it to every main window instance. * Parameters: msgParent = parent widget for any calendar selection prompt or * error message. * event - is updated with the actual event ID. */ UpdateResult addEvent(KAEvent& event, Resource* resource, QWidget* msgParent, int options, bool showKOrgErr) { qCDebug(KALARM_LOG) << "KAlarm::addEvent:" << event.id(); bool cancelled = false; UpdateStatusData status; if (!theApp()->checkCalendar()) // ensure calendar is open status.status = UPDATE_FAILED; else { // Save the event details in the calendar file, and get the new event ID AlarmCalendar* cal = AlarmCalendar::resources(); // Note that AlarmCalendar::addEvent() updates 'event'. if (!cal->addEvent(event, msgParent, (options & USE_EVENT_ID), resource, (options & NO_RESOURCE_PROMPT), &cancelled)) { status.status = UPDATE_FAILED; } else { if (!cal->save()) status.status = SAVE_FAILED; } if (status.status == UPDATE_OK) { if ((options & ALLOW_KORG_UPDATE) && event.copyToKOrganizer()) { UpdateResult st = sendToKOrganizer(event); // tell KOrganizer to show the event status.korgUpdate(st); } } } if (status.status != UPDATE_OK && !cancelled && msgParent) displayUpdateError(msgParent, ERR_ADD, status, showKOrgErr); return status.status; } /****************************************************************************** * Add a list of new active (non-archived) alarms. * Save them in the calendar file and add them to every main window instance. * The events are updated with their actual event IDs. */ UpdateResult addEvents(QVector& events, QWidget* msgParent, bool allowKOrgUpdate, bool showKOrgErr) { qCDebug(KALARM_LOG) << "KAlarm::addEvents:" << events.count(); if (events.isEmpty()) return UpdateResult(UPDATE_OK); UpdateStatusData status; if (!theApp()->checkCalendar()) // ensure calendar is open status.status = UPDATE_FAILED; else { Resource resource = Resources::destination(CalEvent::ACTIVE, msgParent); if (!resource.isValid()) { qCDebug(KALARM_LOG) << "KAlarm::addEvents: No calendar"; status.status = UPDATE_FAILED; } else { AlarmCalendar* cal = AlarmCalendar::resources(); for (int i = 0, end = events.count(); i < end; ++i) { // Save the event details in the calendar file, and get the new event ID KAEvent& event = events[i]; if (!cal->addEvent(event, msgParent, false, &resource)) { status.setError(UPDATE_ERROR); continue; } if (allowKOrgUpdate && event.copyToKOrganizer()) { UpdateResult st = sendToKOrganizer(event); // tell KOrganizer to show the event status.korgUpdate(st); } } if (status.warnErr == events.count()) status.status = UPDATE_FAILED; else if (!cal->save()) status.setError(SAVE_FAILED, events.count()); // everything failed } } if (status.status != UPDATE_OK && msgParent) displayUpdateError(msgParent, ERR_ADD, status, showKOrgErr); return status.status; } /****************************************************************************** * Save the event in the archived calendar and adjust every main window instance. * The event's ID is changed to an archived ID if necessary. */ bool addArchivedEvent(KAEvent& event, Resource* resourceptr) { qCDebug(KALARM_LOG) << "KAlarm::addArchivedEvent:" << event.id(); bool archiving = (event.category() == CalEvent::ACTIVE); if (archiving && !Preferences::archivedKeepDays()) return false; // expired alarms aren't being kept AlarmCalendar* cal = AlarmCalendar::resources(); KAEvent newevent(event); KAEvent* const newev = &newevent; if (archiving) { newev->setCategory(CalEvent::ARCHIVED); // this changes the event ID newev->setCreatedDateTime(KADateTime::currentUtcDateTime()); // time stamp to control purging } // Note that archived resources are automatically saved after changes are made if (!cal->addEvent(newevent, nullptr, false, resourceptr)) return false; event = *newev; // update event ID etc. return true; } /****************************************************************************** * Add a new template. * Save it in the calendar file and add it to every template list view. * 'event' is updated with the actual event ID. * Parameters: promptParent = parent widget for any calendar selection prompt. */ UpdateResult addTemplate(KAEvent& event, Resource* resourceptr, QWidget* msgParent) { qCDebug(KALARM_LOG) << "KAlarm::addTemplate:" << event.id(); UpdateStatusData status; // Add the template to the calendar file AlarmCalendar* cal = AlarmCalendar::resources(); KAEvent newev(event); if (!cal->addEvent(newev, msgParent, false, resourceptr)) status.status = UPDATE_FAILED; else { event = newev; // update event ID etc. if (!cal->save()) status.status = SAVE_FAILED; else { return UpdateResult(UPDATE_OK); } } if (msgParent) displayUpdateError(msgParent, ERR_TEMPLATE, status); return status.status; } /****************************************************************************** * Modify an active (non-archived) alarm in the calendar file and in every main * window instance. * The new event must have a different event ID from the old one. */ UpdateResult modifyEvent(KAEvent& oldEvent, KAEvent& newEvent, QWidget* msgParent, bool showKOrgErr) { qCDebug(KALARM_LOG) << "KAlarm::modifyEvent:" << oldEvent.id(); UpdateStatusData status; if (!newEvent.isValid()) { deleteEvent(oldEvent, true); status.status = UPDATE_FAILED; } else { EventId oldId(oldEvent); if (oldEvent.copyToKOrganizer()) { // Tell KOrganizer to delete its old event. // But ignore errors, because the user could have manually // deleted it since KAlarm asked KOrganizer to set it up. deleteFromKOrganizer(oldId.eventId()); } // Update the event in the calendar file, and get the new event ID AlarmCalendar* cal = AlarmCalendar::resources(); if (!cal->modifyEvent(oldId, newEvent)) status.status = UPDATE_FAILED; else { if (!cal->save()) status.status = SAVE_FAILED; if (status.status == UPDATE_OK) { if (newEvent.copyToKOrganizer()) { UpdateResult st = sendToKOrganizer(newEvent); // tell KOrganizer to show the new event status.korgUpdate(st); } // Remove "Don't show error messages again" for the old alarm setDontShowErrors(oldId); } } } if (status.status != UPDATE_OK && msgParent) displayUpdateError(msgParent, ERR_MODIFY, status, showKOrgErr); return status.status; } /****************************************************************************** * Update an active (non-archived) alarm from the calendar file and from every * main window instance. * The new event will have the same event ID as the old one. * The event is not updated in KOrganizer, since this function is called when an * existing alarm is rescheduled (due to recurrence or deferral). */ UpdateResult updateEvent(KAEvent& event, QWidget* msgParent, bool archiveOnDelete) { qCDebug(KALARM_LOG) << "KAlarm::updateEvent:" << event.id(); if (!event.isValid()) deleteEvent(event, archiveOnDelete); else { // Update the event in the calendar file. AlarmCalendar* cal = AlarmCalendar::resources(); cal->updateEvent(event); if (!cal->save()) { if (msgParent) displayUpdateError(msgParent, ERR_ADD, UpdateStatusData(SAVE_FAILED)); return UpdateResult(SAVE_FAILED); } } return UpdateResult(UPDATE_OK); } /****************************************************************************** * Update a template in the calendar file and in every template list view. * If 'selectionView' is non-null, the selection highlight is moved to the * updated event in that listView instance. */ UpdateResult updateTemplate(KAEvent& event, QWidget* msgParent) { AlarmCalendar* cal = AlarmCalendar::resources(); const KAEvent* newEvent = cal->updateEvent(event); UpdateStatus status = UPDATE_OK; if (!newEvent) status = UPDATE_FAILED; else if (!cal->save()) status = SAVE_FAILED; if (status != UPDATE_OK) { if (msgParent) displayUpdateError(msgParent, ERR_TEMPLATE, UpdateStatusData(SAVE_FAILED)); return UpdateResult(status); } return UpdateResult(UPDATE_OK); } /****************************************************************************** * Delete alarms from the calendar file and from every main window instance. * If the events are archived, the events' IDs are changed to archived IDs if necessary. */ UpdateResult deleteEvent(KAEvent& event, bool archive, QWidget* msgParent, bool showKOrgErr) { QVector events(1, event); return deleteEvents(events, archive, msgParent, showKOrgErr); } UpdateResult deleteEvents(QVector& events, bool archive, QWidget* msgParent, bool showKOrgErr) { qCDebug(KALARM_LOG) << "KAlarm::deleteEvents:" << events.count(); if (events.isEmpty()) return UpdateResult(UPDATE_OK); UpdateStatusData status; AlarmCalendar* cal = AlarmCalendar::resources(); bool deleteWakeFromSuspendAlarm = false; const QString wakeFromSuspendId = checkRtcWakeConfig().value(0); for (int i = 0, end = events.count(); i < end; ++i) { // Save the event details in the calendar file, and get the new event ID KAEvent* event = &events[i]; const QString id = event->id(); // Delete the event from the calendar file if (event->category() != CalEvent::ARCHIVED) { if (event->copyToKOrganizer()) { // The event was shown in KOrganizer, so tell KOrganizer to // delete it. But ignore errors, because the user could have // manually deleted it from KOrganizer since it was set up. UpdateResult st = deleteFromKOrganizer(id); status.korgUpdate(st); } if (archive && event->toBeArchived()) { KAEvent ev(*event); addArchivedEvent(ev); // this changes the event ID to an archived ID } } if (!cal->deleteEvent(*event, false)) // don't save calendar after deleting status.setError(UPDATE_ERROR); if (id == wakeFromSuspendId) deleteWakeFromSuspendAlarm = true; // Remove "Don't show error messages again" for this alarm setDontShowErrors(EventId(*event)); } if (status.warnErr == events.count()) status.status = UPDATE_FAILED; else if (!cal->save()) // save the calendars now status.setError(SAVE_FAILED, events.count()); if (status.status != UPDATE_OK && msgParent) displayUpdateError(msgParent, ERR_DELETE, status, showKOrgErr); // Remove any wake-from-suspend scheduled for a deleted alarm if (deleteWakeFromSuspendAlarm && !wakeFromSuspendId.isEmpty()) cancelRtcWake(msgParent, wakeFromSuspendId); return status.status; } /****************************************************************************** * Delete templates from the calendar file and from every template list view. */ UpdateResult deleteTemplates(const KAEvent::List& events, QWidget* msgParent) { int count = events.count(); qCDebug(KALARM_LOG) << "KAlarm::deleteTemplates:" << count; if (!count) return UpdateResult(UPDATE_OK); UpdateStatusData status; AlarmCalendar* cal = AlarmCalendar::resources(); for (const KAEvent* event : events) { // Update the window lists // Delete the template from the calendar file AlarmCalendar* cal = AlarmCalendar::resources(); if (!cal->deleteEvent(*event, false)) // don't save calendar after deleting status.setError(UPDATE_ERROR); } if (status.warnErr == count) status.status = UPDATE_FAILED; else if (!cal->save()) // save the calendars now status.setError(SAVE_FAILED, count); if (status.status != UPDATE_OK && msgParent) displayUpdateError(msgParent, ERR_TEMPLATE, status); return status.status; } /****************************************************************************** * Delete an alarm from the display calendar. */ void deleteDisplayEvent(const QString& eventID) { qCDebug(KALARM_LOG) << "KAlarm::deleteDisplayEvent:" << eventID; AlarmCalendar* cal = AlarmCalendar::displayCalendarOpen(); if (cal) cal->deleteDisplayEvent(eventID, true); // save calendar after deleting } /****************************************************************************** * Undelete archived alarms, and update every main window instance. * The archive bit is set to ensure that they get re-archived if deleted again. * Parameters: * calendar - the active alarms calendar to restore the alarms into, or null * to use the default way of determining the active alarm calendar. * ineligibleIDs - will be filled in with the IDs of any ineligible events. */ UpdateResult reactivateEvent(KAEvent& event, Resource* resourceptr, QWidget* msgParent, bool showKOrgErr) { QVector ids; QVector events(1, event); return reactivateEvents(events, ids, resourceptr, msgParent, showKOrgErr); } UpdateResult reactivateEvents(QVector& events, QVector& ineligibleIDs, Resource* resourceptr, QWidget* msgParent, bool showKOrgErr) { qCDebug(KALARM_LOG) << "KAlarm::reactivateEvents:" << events.count(); ineligibleIDs.clear(); if (events.isEmpty()) return UpdateResult(UPDATE_OK); UpdateStatusData status; Resource resource; if (resourceptr) resource = *resourceptr; if (!resource.isValid()) resource = Resources::destination(CalEvent::ACTIVE, msgParent); if (!resource.isValid()) { qCDebug(KALARM_LOG) << "KAlarm::reactivateEvents: No calendar"; status.setError(UPDATE_FAILED, events.count()); } else { int count = 0; AlarmCalendar* cal = AlarmCalendar::resources(); const KADateTime now = KADateTime::currentUtcDateTime(); for (int i = 0, end = events.count(); i < end; ++i) { // Delete the event from the archived resource KAEvent* event = &events[i]; if (event->category() != CalEvent::ARCHIVED || !event->occursAfter(now, true)) { ineligibleIDs += EventId(*event); continue; } ++count; KAEvent newevent(*event); KAEvent* const newev = &newevent; newev->setCategory(CalEvent::ACTIVE); // this changes the event ID if (newev->recurs() || newev->repetition()) newev->setNextOccurrence(now); // skip any recurrences in the past newev->setArchive(); // ensure that it gets re-archived if it is deleted // Save the event details in the calendar file. // This converts the event ID. if (!cal->addEvent(newevent, msgParent, true, &resource)) { status.setError(UPDATE_ERROR); continue; } if (newev->copyToKOrganizer()) { UpdateResult st = sendToKOrganizer(*newev); // tell KOrganizer to show the event status.korgUpdate(st); } if (cal->event(EventId(*event)) // no error if event doesn't exist in archived resource && !cal->deleteEvent(*event, false)) // don't save calendar after deleting status.setError(UPDATE_ERROR); events[i] = newevent; } if (status.warnErr == count) status.status = UPDATE_FAILED; // Save the calendars, even if all events failed, since more than one calendar was updated if (!cal->save() && status.status != UPDATE_FAILED) status.setError(SAVE_FAILED, count); } if (status.status != UPDATE_OK && msgParent) displayUpdateError(msgParent, ERR_REACTIVATE, status, showKOrgErr); return status.status; } /****************************************************************************** * Enable or disable alarms in the calendar file and in every main window instance. * The new events will have the same event IDs as the old ones. */ UpdateResult enableEvents(QVector& events, bool enable, QWidget* msgParent) { qCDebug(KALARM_LOG) << "KAlarm::enableEvents:" << events.count(); if (events.isEmpty()) return UpdateResult(UPDATE_OK); UpdateStatusData status; AlarmCalendar* cal = AlarmCalendar::resources(); bool deleteWakeFromSuspendAlarm = false; const QString wakeFromSuspendId = checkRtcWakeConfig().value(0); for (int i = 0, end = events.count(); i < end; ++i) { KAEvent* event = &events[i]; if (event->category() == CalEvent::ACTIVE && enable != event->enabled()) { event->setEnabled(enable); if (!enable && event->id() == wakeFromSuspendId) deleteWakeFromSuspendAlarm = true; // Update the event in the calendar file const KAEvent* newev = cal->updateEvent(event); if (!newev) qCCritical(KALARM_LOG) << "KAlarm::enableEvents: Error updating event in calendar:" << event->id(); else { cal->disabledChanged(newev); // If we're disabling a display alarm, close any message window if (!enable && (event->actionTypes() & KAEvent::ACT_DISPLAY)) { MessageWin* win = MessageWin::findEvent(EventId(*event)); delete win; } } } } if (!cal->save()) status.setError(SAVE_FAILED, events.count()); if (status.status != UPDATE_OK && msgParent) displayUpdateError(msgParent, ERR_ADD, status); // Remove any wake-from-suspend scheduled for a disabled alarm if (deleteWakeFromSuspendAlarm && !wakeFromSuspendId.isEmpty()) cancelRtcWake(msgParent, wakeFromSuspendId); return status.status; } /****************************************************************************** * This method must only be called from the main KAlarm queue processing loop, * to prevent asynchronous calendar operations interfering with one another. * * Purge all archived events from the default archived alarm resource whose end * time is longer ago than 'purgeDays'. All events are deleted if 'purgeDays' is * zero. */ void purgeArchive(int purgeDays) { if (purgeDays < 0) return; qCDebug(KALARM_LOG) << "KAlarm::purgeArchive:" << purgeDays; const QDate cutoff = KADateTime::currentLocalDate().addDays(-purgeDays); const Resource resource = Resources::getStandard(CalEvent::ARCHIVED); if (!resource.isValid()) return; KAEvent::List events = AlarmCalendar::resources()->events(resource); for (int i = 0; i < events.count(); ) { if (purgeDays && events.at(i)->createdDateTime().date() >= cutoff) events.remove(i); else ++i; } if (!events.isEmpty()) AlarmCalendar::resources()->purgeEvents(events); // delete the events and save the calendar } /****************************************************************************** * Display an error message about an error when saving an event. * If 'model' is non-null, the AlarmListModel* which it points to is used; if * that is null, it is created. */ QVector getSortedActiveEvents(QObject* parent, AlarmListModel** model) { AlarmListModel* mdl = nullptr; if (!model) model = &mdl; if (!*model) { *model = AlarmListModel::create(parent); (*model)->setEventTypeFilter(CalEvent::ACTIVE); (*model)->sort(AlarmListModel::TimeColumn); } QVector result; for (int i = 0, count = (*model)->rowCount(); i < count; ++i) { const KAEvent event = (*model)->event(i); if (event.enabled() && !event.expired()) result += event; } return result; } /****************************************************************************** * Display an error message corresponding to a specified alarm update error code. */ void displayKOrgUpdateError(QWidget* parent, UpdateError code, const UpdateResult& korgError, int nAlarms) { QString errmsg; switch (code) { case ERR_ADD: case ERR_REACTIVATE: errmsg = (nAlarms > 1) ? i18nc("@info", "Unable to show alarms in KOrganizer") : i18nc("@info", "Unable to show alarm in KOrganizer"); break; case ERR_MODIFY: errmsg = i18nc("@info", "Unable to update alarm in KOrganizer"); break; case ERR_DELETE: errmsg = (nAlarms > 1) ? i18nc("@info", "Unable to delete alarms from KOrganizer") : i18nc("@info", "Unable to delete alarm from KOrganizer"); break; case ERR_TEMPLATE: return; } bool showDetail = !korgError.message.isEmpty(); QString msg; switch (korgError.status) { case UPDATE_KORG_ERRINIT: msg = xi18nc("@info", "%1(Could not start KOrganizer)", errmsg); break; case UPDATE_KORG_ERRSTART: msg = xi18nc("@info", "%1(KOrganizer not fully started)", errmsg); break; case UPDATE_KORG_ERR: msg = xi18nc("@info", "%1(Error communicating with KOrganizer)", errmsg); break; default: msg = errmsg; showDetail = false; break; } if (showDetail) KAMessageBox::detailedError(parent, msg, korgError.message); else KAMessageBox::error(parent, msg); } /****************************************************************************** * Execute a New Alarm dialog for the specified alarm type. */ void editNewAlarm(EditAlarmDlg::Type type, QWidget* parent) { execNewAlarmDlg(EditAlarmDlg::create(false, type, parent)); } /****************************************************************************** * Execute a New Alarm dialog for the specified alarm type. */ void editNewAlarm(KAEvent::SubAction action, QWidget* parent, const AlarmText* text) { bool setAction = false; EditAlarmDlg::Type type; switch (action) { case KAEvent::MESSAGE: case KAEvent::FILE: type = EditAlarmDlg::DISPLAY; setAction = true; break; case KAEvent::COMMAND: type = EditAlarmDlg::COMMAND; break; case KAEvent::EMAIL: type = EditAlarmDlg::EMAIL; break; case KAEvent::AUDIO: type = EditAlarmDlg::AUDIO; break; default: return; } EditAlarmDlg* editDlg = EditAlarmDlg::create(false, type, parent); if (setAction || text) editDlg->setAction(action, *text); execNewAlarmDlg(editDlg); } /****************************************************************************** * Execute a New Alarm dialog, optionally either presetting it to the supplied * event, or setting the action and text. */ void editNewAlarm(const KAEvent* preset, QWidget* parent) { execNewAlarmDlg(EditAlarmDlg::create(false, preset, true, parent)); } /****************************************************************************** * Common code for editNewAlarm() variants. */ void execNewAlarmDlg(EditAlarmDlg* editDlg) { // Create a PrivateNewAlarmDlg parented by editDlg. // It will be deleted when editDlg is closed. new PrivateNewAlarmDlg(editDlg); editDlg->show(); editDlg->raise(); editDlg->activateWindow(); } PrivateNewAlarmDlg::PrivateNewAlarmDlg(EditAlarmDlg* dlg) : QObject(dlg) { connect(dlg, &QDialog::accepted, this, &PrivateNewAlarmDlg::okClicked); connect(dlg, &QDialog::rejected, this, &PrivateNewAlarmDlg::cancelClicked); } /****************************************************************************** * Called when the dialogue is accepted (e.g. by clicking the OK button). * Creates the event specified in the instance's dialogue. */ void PrivateNewAlarmDlg::okClicked() { accept(static_cast(parent())); } /****************************************************************************** * Creates the event specified in a given dialogue. */ void PrivateNewAlarmDlg::accept(EditAlarmDlg* editDlg) { KAEvent event; Resource resource; editDlg->getEvent(event, resource); // Add the alarm to the displayed lists and to the calendar file const UpdateResult status = addEvent(event, &resource, editDlg); switch (status.status) { case UPDATE_FAILED: return; case UPDATE_KORG_ERR: case UPDATE_KORG_ERRINIT: case UPDATE_KORG_ERRSTART: case UPDATE_KORG_FUNCERR: displayKOrgUpdateError(editDlg, ERR_ADD, status); break; default: break; } Undo::saveAdd(event, resource); outputAlarmWarnings(editDlg, &event); editDlg->deleteLater(); } /****************************************************************************** * Called when the dialogue is rejected (e.g. by clicking the Cancel button). */ void PrivateNewAlarmDlg::cancelClicked() { static_cast(parent())->deleteLater(); } /****************************************************************************** * Display the alarm edit dialog to edit a new alarm, preset with a template. */ bool editNewAlarm(const QString& templateName, QWidget* parent) { if (!templateName.isEmpty()) { KAEvent* templateEvent = AlarmCalendar::resources()->templateEvent(templateName); if (templateEvent->isValid()) { editNewAlarm(templateEvent, parent); return true; } qCWarning(KALARM_LOG) << "KAlarm::editNewAlarm:" << templateName << ": template not found"; } return false; } /****************************************************************************** * Create a new template. */ void editNewTemplate(EditAlarmDlg::Type type, QWidget* parent) { ::editNewTemplate(type, nullptr, parent); } /****************************************************************************** * Create a new template, based on an existing event or template. */ void editNewTemplate(const KAEvent* preset, QWidget* parent) { ::editNewTemplate(EditAlarmDlg::Type(0), preset, parent); } /****************************************************************************** * 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 == QLatin1String("KDE")) return Desktop::Kde; if (desktop == QLatin1String("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 resource ID, * [1] = event ID, * [2] = trigger time (int64 seconds since epoch). * = empty list if none or expired. */ QStringList checkRtcWakeConfig(bool checkEventExists) { KConfigGroup config(KSharedConfig::openConfig(), "General"); const QStringList params = config.readEntry("RtcWake", QStringList()); #if KALARMCAL_VERSION >= QT_VERSION_CHECK(5,12,1) if (params.count() == 3 && params[2].toLongLong() > KADateTime::currentUtcDateTime().toSecsSinceEpoch()) #else if (params.count() == 3 && params[2].toUInt() > KADateTime::currentUtcDateTime().toTime_t()) #endif { if (checkEventExists && !AlarmCalendar::getEvent(EventId(params[0].toLongLong(), params[1]))) return QStringList(); return params; // config entry is valid } if (!params.isEmpty()) { config.deleteEntry("RtcWake"); // delete the expired config entry config.sync(); } return QStringList(); } /****************************************************************************** * Delete any wake-on-suspend alarm from the config. */ void deleteRtcWakeConfig() { KConfigGroup config(KSharedConfig::openConfig(), "General"); config.deleteEntry("RtcWake"); config.sync(); } /****************************************************************************** * Delete any wake-on-suspend alarm, optionally only for a specified event. */ void cancelRtcWake(QWidget* msgParent, const QString& eventId) { const QStringList wakeup = checkRtcWakeConfig(); if (!wakeup.isEmpty() && (eventId.isEmpty() || wakeup[0] == eventId)) { Private::instance()->mMsgParent = msgParent ? msgParent : MainWindow::mainMainWindow(); QTimer::singleShot(0, Private::instance(), &Private::cancelRtcWake); } } /****************************************************************************** * Delete any wake-on-suspend alarm. */ void Private::cancelRtcWake() { // setRtcWakeTime will only work with a parent window specified setRtcWakeTime(0, mMsgParent); deleteRtcWakeConfig(); KAMessageBox::information(mMsgParent, i18nc("info", "The scheduled Wake from Suspend has been cancelled.")); } /****************************************************************************** * Set the wakeup time for the system. * Set 'triggerTime' to zero to cancel the wakeup. * Reply = true if successful. */ bool setRtcWakeTime(unsigned triggerTime, QWidget* parent) { QVariantMap args; args[QStringLiteral("time")] = triggerTime; KAuth::Action action(QStringLiteral("org.kde.kalarm.rtcwake.settimer")); action.setHelperId(QStringLiteral("org.kde.kalarm.rtcwake")); action.setParentWidget(parent); action.setArguments(args); KAuth::ExecuteJob* job = action.execute(); if (!job->exec()) { QString errmsg = job->errorString(); qCDebug(KALARM_LOG) << "KAlarm::setRtcWakeTime: Error code=" << job->error() << errmsg; if (errmsg.isEmpty()) { int errcode = job->error(); switch (errcode) { case KAuth::ActionReply::AuthorizationDeniedError: case KAuth::ActionReply::UserCancelledError: qCDebug(KALARM_LOG) << "KAlarm::setRtcWakeTime: Authorization error:" << errcode; return false; // the user should already know about this default: break; } errmsg = i18nc("@info", "Error obtaining authorization (%1)", errcode); } KAMessageBox::information(parent, errmsg); return false; } return true; } } // namespace KAlarm namespace { /****************************************************************************** * Create a new template. * 'preset' is non-null to base it on an existing event or template; otherwise, * the alarm type is set to 'type'. */ void editNewTemplate(EditAlarmDlg::Type type, const KAEvent* preset, QWidget* parent) { if (Resources::enabledResources(CalEvent::TEMPLATE, true).isEmpty()) { KAMessageBox::sorry(parent, i18nc("@info", "You must enable a template calendar to save the template in")); return; } // Use AutoQPointer to guard against crash on application exit while // the dialogue is still open. It prevents double deletion (both on // deletion of parent, and on return from this function). AutoQPointer editDlg; if (preset) editDlg = EditAlarmDlg::create(true, preset, true, parent); else editDlg = EditAlarmDlg::create(true, type, parent); if (editDlg->exec() == QDialog::Accepted) { KAEvent event; Resource resource; editDlg->getEvent(event, resource); // Add the template to the displayed lists and to the calendar file KAlarm::addTemplate(event, &resource, editDlg); Undo::saveAdd(event, resource); } } } // namespace namespace KAlarm { /****************************************************************************** * Open the Edit Alarm dialog to edit the specified alarm. * If the alarm is read-only or archived, the dialog is opened read-only. */ void editAlarm(KAEvent* event, QWidget* parent) { if (event->expired() || AlarmCalendar::resources()->eventReadOnly(event->id())) { viewAlarm(event, parent); return; } const EventId id(*event); // Use AutoQPointer to guard against crash on application exit while // the dialogue is still open. It prevents double deletion (both on // deletion of parent, and on return from this function). AutoQPointer editDlg = EditAlarmDlg::create(false, event, false, parent, EditAlarmDlg::RES_USE_EVENT_ID); if (editDlg->exec() == QDialog::Accepted) { if (!AlarmCalendar::resources()->event(id)) { // Event has been deleted while the user was editing the alarm, // so treat it as a new alarm. PrivateNewAlarmDlg().accept(editDlg); return; } KAEvent newEvent; Resource resource; bool changeDeferral = !editDlg->getEvent(newEvent, resource); // Update the event in the displays and in the calendar file const Undo::Event undo(*event, resource); if (changeDeferral) { // The only change has been to an existing deferral if (updateEvent(newEvent, editDlg, true) != UPDATE_OK) // keep the same event ID return; // failed to save event } else { const UpdateResult status = modifyEvent(*event, newEvent, editDlg); if (status.status != UPDATE_OK && status.status <= UPDATE_KORG_ERR) displayKOrgUpdateError(editDlg, ERR_MODIFY, status); } Undo::saveEdit(undo, newEvent); outputAlarmWarnings(editDlg, &newEvent); } } /****************************************************************************** * Display the alarm edit dialog to edit the alarm with the specified ID. * An error occurs if the alarm is not found, if there is more than one alarm * with the same ID, or if it is read-only or expired. */ bool editAlarmById(const EventId& id, QWidget* parent) { const QString eventID(id.eventId()); KAEvent* event = AlarmCalendar::resources()->event(id, true); if (!event) { if (id.collectionId() != -1) qCWarning(KALARM_LOG) << "KAlarm::editAlarmById: Event ID not found, or duplicated:" << eventID; else qCWarning(KALARM_LOG) << "KAlarm::editAlarmById: Event ID not found:" << eventID; return false; } if (AlarmCalendar::resources()->eventReadOnly(event->id())) { qCCritical(KALARM_LOG) << "KAlarm::editAlarmById:" << eventID << ": read-only"; return false; } switch (event->category()) { case CalEvent::ACTIVE: case CalEvent::TEMPLATE: break; default: qCCritical(KALARM_LOG) << "KAlarm::editAlarmById:" << eventID << ": event not active or template"; return false; } editAlarm(event, parent); return true; } /****************************************************************************** * Open the Edit Alarm dialog to edit the specified template. * If the template is read-only, the dialog is opened read-only. */ void editTemplate(KAEvent* event, QWidget* parent) { if (AlarmCalendar::resources()->eventReadOnly(event->id())) { // The template is read-only, so make the dialogue read-only. // Use AutoQPointer to guard against crash on application exit while // the dialogue is still open. It prevents double deletion (both on // deletion of parent, and on return from this function). AutoQPointer editDlg = EditAlarmDlg::create(true, event, false, parent, EditAlarmDlg::RES_PROMPT, true); editDlg->exec(); return; } // Use AutoQPointer to guard against crash on application exit while // the dialogue is still open. It prevents double deletion (both on // deletion of parent, and on return from this function). AutoQPointer editDlg = EditAlarmDlg::create(true, event, false, parent, EditAlarmDlg::RES_USE_EVENT_ID); if (editDlg->exec() == QDialog::Accepted) { KAEvent newEvent; Resource resource; editDlg->getEvent(newEvent, resource); const QString id = event->id(); newEvent.setEventId(id); newEvent.setResourceId(event->resourceId()); // Update the event in the displays and in the calendar file const Undo::Event undo(*event, resource); updateTemplate(newEvent, editDlg); Undo::saveEdit(undo, newEvent); } } /****************************************************************************** * Open the Edit Alarm dialog to view the specified alarm (read-only). */ void viewAlarm(const KAEvent* event, QWidget* parent) { // Use AutoQPointer to guard against crash on application exit while // the dialogue is still open. It prevents double deletion (both on // deletion of parent, and on return from this function). AutoQPointer editDlg = EditAlarmDlg::create(false, event, false, parent, EditAlarmDlg::RES_PROMPT, true); editDlg->exec(); } /****************************************************************************** * Called when OK is clicked in the alarm edit dialog invoked by the Edit button * in an alarm message window. * Updates the alarm calendar and closes the dialog. */ void updateEditedAlarm(EditAlarmDlg* editDlg, KAEvent& event, Resource& resource) { qCDebug(KALARM_LOG) << "KAlarm::updateEditedAlarm"; KAEvent newEvent; Resource res; editDlg->getEvent(newEvent, res); // Update the displayed lists and the calendar file UpdateResult status; if (AlarmCalendar::resources()->event(EventId(event))) { // The old alarm hasn't expired yet, so replace it const Undo::Event undo(event, resource); status = modifyEvent(event, newEvent, editDlg); Undo::saveEdit(undo, newEvent); } else { // The old event has expired, so simply create a new one status = addEvent(newEvent, &resource, editDlg); Undo::saveAdd(newEvent, resource); } if (status.status != UPDATE_OK && status.status <= UPDATE_KORG_ERR) displayKOrgUpdateError(editDlg, ERR_MODIFY, status); outputAlarmWarnings(editDlg, &newEvent); editDlg->close(); } /****************************************************************************** * Returns a list of all alarm templates. * If shell commands are disabled, command alarm templates are omitted. */ KAEvent::List templateList() { KAEvent::List templates; const bool includeCmdAlarms = ShellProcess::authorised(); const KAEvent::List events = AlarmCalendar::resources()->events(CalEvent::TEMPLATE); for (KAEvent* event : events) { if (includeCmdAlarms || !(event->actionTypes() & KAEvent::ACT_COMMAND)) templates.append(event); } return templates; } /****************************************************************************** * To be called after an alarm has been edited. * Prompt the user to re-enable alarms if they are currently disabled, and if * it's an email alarm, warn if no 'From' email address is configured. */ void outputAlarmWarnings(QWidget* parent, const KAEvent* event) { if (event && event->actionTypes() == KAEvent::ACT_EMAIL && Preferences::emailAddress().isEmpty()) KAMessageBox::information(parent, xi18nc("@info Please set the 'From' email address...", "%1Please set it in the Configuration dialog.", KAMail::i18n_NeedFromEmailAddress())); if (!theApp()->alarmsEnabled()) { if (KAMessageBox::warningYesNo(parent, xi18nc("@info", "Alarms are currently disabled.Do you want to enable alarms now?"), QString(), KGuiItem(i18nc("@action:button", "Enable")), KGuiItem(i18nc("@action:button", "Keep Disabled")), QStringLiteral("EditEnableAlarms")) == KMessageBox::Yes) theApp()->setAlarmsEnabled(true); } } /****************************************************************************** * Reload the calendar. */ void refreshAlarms() { qCDebug(KALARM_LOG) << "KAlarm::refreshAlarms"; if (!refreshAlarmsQueued) { refreshAlarmsQueued = true; theApp()->processQueue(); } } /****************************************************************************** * This method must only be called from the main KAlarm queue processing loop, * to prevent asynchronous calendar operations interfering with one another. * * If refreshAlarms() has been called, reload the calendars. */ void refreshAlarmsIfQueued() { if (refreshAlarmsQueued) { qCDebug(KALARM_LOG) << "KAlarm::refreshAlarmsIfQueued"; AlarmCalendar::resources()->reload(); // Close any message windows for alarms which are now disabled const KAEvent::List events = AlarmCalendar::resources()->events(CalEvent::ACTIVE); for (KAEvent* event : events) { if (!event->enabled() && (event->actionTypes() & KAEvent::ACT_DISPLAY)) { MessageWin* win = MessageWin::findEvent(EventId(*event)); delete win; } } MainWindow::refresh(); refreshAlarmsQueued = false; } } /****************************************************************************** * Start KMail if it isn't already running, optionally minimised. * Reply = reason for failure to run KMail * = null string if success. */ QString runKMail() { const QDBusReply reply = QDBusConnection::sessionBus().interface()->isServiceRegistered(KMAIL_DBUS_SERVICE); if (!reply.isValid() || !reply.value()) { // Program is not already running, so start it const QDBusReply startReply = QDBusConnection::sessionBus().interface()->startService(KMAIL_DBUS_SERVICE); if (!startReply.isValid()) { const QString errmsg = startReply.error().message(); qCCritical(KALARM_LOG) << "Couldn't start KMail (" << errmsg << ")"; return xi18nc("@info", "Unable to start KMail(%1)", errmsg); } } return QString(); } /****************************************************************************** * The "Don't show again" option for error messages is personal to the user on a * particular computer. For example, he may want to inhibit error messages only * on his laptop. So the status is not stored in the alarm calendar, but in the * user's local KAlarm data directory. ******************************************************************************/ /****************************************************************************** * Return the Don't-show-again error message tags set for a specified alarm ID. */ QStringList dontShowErrors(const EventId& eventId) { if (eventId.isEmpty()) return QStringList(); KConfig config(QStandardPaths::writableLocation(QStandardPaths::DataLocation) + QLatin1Char('/') + ALARM_OPTS_FILE); KConfigGroup group(&config, DONT_SHOW_ERRORS_GROUP); const QString id = QStringLiteral("%1:%2").arg(eventId.collectionId()).arg(eventId.eventId()); return group.readEntry(id, QStringList()); } /****************************************************************************** * Check whether the specified Don't-show-again error message tag is set for an * alarm ID. */ bool dontShowErrors(const EventId& eventId, const QString& tag) { if (tag.isEmpty()) return false; const QStringList tags = dontShowErrors(eventId); return tags.indexOf(tag) >= 0; } /****************************************************************************** * Reset the Don't-show-again error message tags for an alarm ID. * If 'tags' is empty, the config entry is deleted. */ void setDontShowErrors(const EventId& eventId, const QStringList& tags) { if (eventId.isEmpty()) return; KConfig config(QStandardPaths::writableLocation(QStandardPaths::DataLocation) + QLatin1Char('/') + ALARM_OPTS_FILE); KConfigGroup group(&config, DONT_SHOW_ERRORS_GROUP); const QString id = QStringLiteral("%1:%2").arg(eventId.collectionId()).arg(eventId.eventId()); if (tags.isEmpty()) group.deleteEntry(id); else group.writeEntry(id, tags); group.sync(); } /****************************************************************************** * Set the specified Don't-show-again error message tag for an alarm ID. * Existing tags are unaffected. */ void setDontShowErrors(const EventId& eventId, const QString& tag) { if (eventId.isEmpty() || tag.isEmpty()) return; KConfig config(QStandardPaths::writableLocation(QStandardPaths::DataLocation) + QLatin1Char('/') + ALARM_OPTS_FILE); KConfigGroup group(&config, DONT_SHOW_ERRORS_GROUP); const QString id = QStringLiteral("%1:%2").arg(eventId.collectionId()).arg(eventId.eventId()); QStringList tags = group.readEntry(id, QStringList()); if (tags.indexOf(tag) < 0) { tags += tag; group.writeEntry(id, tags); group.sync(); } } /****************************************************************************** * Read the size for the specified window from the config file, for the * current screen resolution. * Reply = true if size set in the config file, in which case 'result' is set * = false if no size is set, in which case 'result' is unchanged. */ bool readConfigWindowSize(const char* window, QSize& result, int* splitterWidth) { KConfigGroup config(KSharedConfig::openConfig(), window); const QWidget* desktop = QApplication::desktop(); const QSize s = QSize(config.readEntry(QStringLiteral("Width %1").arg(desktop->width()), (int)0), config.readEntry(QStringLiteral("Height %1").arg(desktop->height()), (int)0)); if (s.isEmpty()) return false; result = s; if (splitterWidth) *splitterWidth = config.readEntry(QStringLiteral("Splitter %1").arg(desktop->width()), -1); return true; } /****************************************************************************** * Write the size for the specified window to the config file, for the * current screen resolution. */ void writeConfigWindowSize(const char* window, const QSize& size, int splitterWidth) { KConfigGroup config(KSharedConfig::openConfig(), window); const QWidget* desktop = QApplication::desktop(); config.writeEntry(QStringLiteral("Width %1").arg(desktop->width()), size.width()); config.writeEntry(QStringLiteral("Height %1").arg(desktop->height()), size.height()); if (splitterWidth >= 0) config.writeEntry(QStringLiteral("Splitter %1").arg(desktop->width()), splitterWidth); config.sync(); } /****************************************************************************** * Check from its mime type whether a file appears to be a text or image file. * If a text file, its type is distinguished. * Reply = file type. */ FileType fileType(const QMimeType& mimetype) { if (mimetype.inherits(QStringLiteral("text/html"))) return TextFormatted; if (mimetype.inherits(QStringLiteral("application/x-executable"))) return TextApplication; if (mimetype.inherits(QStringLiteral("text/plain"))) return TextPlain; if (mimetype.name().startsWith(QLatin1String("image/"))) return Image; return Unknown; } /****************************************************************************** * Check that a file exists and is a plain readable file. * Updates 'filename' and 'url' even if an error occurs, since 'filename' may * be needed subsequently by showFileErrMessage(). * 'filename' is in user input format and may be a local file path or URL. */ FileErr checkFileExists(QString& filename, QUrl& url) { // Convert any relative file path to absolute // (using home directory as the default). // This also supports absolute paths and absolute urls. FileErr err = FileErr_None; url = QUrl::fromUserInput(filename, QDir::homePath(), QUrl::AssumeLocalFile); if (filename.isEmpty()) { url = QUrl(); err = FileErr_Blank; // blank file name } else if (!url.isValid()) err = FileErr_Nonexistent; else if (url.isLocalFile()) { // It's a local file filename = url.toLocalFile(); QFileInfo info(filename); if (info.isDir()) err = FileErr_Directory; else if (!info.exists()) err = FileErr_Nonexistent; else if (!info.isReadable()) err = FileErr_Unreadable; } else { filename = url.toDisplayString(); auto statJob = KIO::stat(url, KIO::StatJob::SourceSide, 2); KJobWidgets::setWindow(statJob, MainWindow::mainMainWindow()); if (!statJob->exec()) err = FileErr_Nonexistent; else { KFileItem fi(statJob->statResult(), url); if (fi.isDir()) err = FileErr_Directory; else if (!fi.isReadable()) err = FileErr_Unreadable; } } return err; } /****************************************************************************** * Display an error message appropriate to 'err'. * Display a Continue/Cancel error message if 'errmsgParent' non-null. * Reply = true to continue, false to cancel. */ bool showFileErrMessage(const QString& filename, FileErr err, FileErr blankError, QWidget* errmsgParent) { if (err != FileErr_None) { // If file is a local file, remove "file://" from name QString file = filename; const QRegExp f(QStringLiteral("^file:/+")); if (f.indexIn(file) >= 0) file = file.mid(f.matchedLength() - 1); QString errmsg; switch (err) { case FileErr_Blank: if (blankError == FileErr_BlankDisplay) errmsg = i18nc("@info", "Please select a file to display"); else if (blankError == FileErr_BlankPlay) errmsg = i18nc("@info", "Please select a file to play"); else qFatal("showFileErrMessage: Program error"); KAMessageBox::sorry(errmsgParent, errmsg); return false; case FileErr_Directory: KAMessageBox::sorry(errmsgParent, xi18nc("@info", "%1 is a folder", file)); return false; case FileErr_Nonexistent: errmsg = xi18nc("@info", "%1 not found", file); break; case FileErr_Unreadable: errmsg = xi18nc("@info", "%1 is not readable", file); break; case FileErr_NotTextImage: errmsg = xi18nc("@info", "%1 appears not to be a text or image file", file); break; default: break; } if (KAMessageBox::warningContinueCancel(errmsgParent, errmsg) == KMessageBox::Cancel) return false; } return true; } /****************************************************************************** * If a url string is a local file, strip off the 'file:/' prefix. */ QString pathOrUrl(const QString& url) { static const QRegExp localfile(QStringLiteral("^file:/+")); return (localfile.indexIn(url) >= 0) ? url.mid(localfile.matchedLength() - 1) : url; } /****************************************************************************** * Display a modal dialog to choose an existing file, initially highlighting * any specified file. * @param file Updated with the file which was selected, or empty if no file * was selected. * @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 = true if 'file' value can be used. * = false if the dialogue was deleted while visible (indicating that * the parent widget was probably also deleted). */ bool browseFile(QString& file, const QString& caption, QString& defaultDir, const QString& initialFile, const QString& filter, bool existing, QWidget* parent) { file.clear(); const QString initialDir = !initialFile.isEmpty() ? QString(initialFile).remove(QRegExp(QLatin1String("/[^/]*$"))) : !defaultDir.isEmpty() ? defaultDir : QDir::homePath(); // Use AutoQPointer to guard against crash on application exit while // the dialogue is still open. It prevents double deletion (both on // deletion of parent, and on return from this function). AutoQPointer fileDlg = new QFileDialog(parent, caption, initialDir, filter); fileDlg->setAcceptMode(existing ? QFileDialog::AcceptOpen : QFileDialog::AcceptSave); fileDlg->setFileMode(existing ? QFileDialog::ExistingFile : QFileDialog::AnyFile); if (!initialFile.isEmpty()) fileDlg->selectFile(initialFile); if (fileDlg->exec() != QDialog::Accepted) return static_cast(fileDlg); // return false if dialog was deleted const QList urls = fileDlg->selectedUrls(); if (urls.isEmpty()) return true; const QUrl& url = urls[0]; defaultDir = url.isLocalFile() ? KIO::upUrl(url).toLocalFile() : url.adjusted(QUrl::RemoveFilename).path(); bool localOnly = true; file = localOnly ? url.toDisplayString(QUrl::PreferLocalFile) : url.toDisplayString(); return true; } /****************************************************************************** * 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) { const QString msg = whole ? xi18n("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) : xi18n("Some or all of the alarms in calendar %1 are in an old KAlarm format, " "and will be read-only unless you choose to update them to the current format.", calendarName); return xi18nc("@info", "%1" "Do not update the calendar if it is also used with an older version of KAlarm " "(e.g. on another computer). If you do so, the calendar may become unusable there." "Do you wish to update the calendar?", msg); } #ifndef NDEBUG /****************************************************************************** * Set up KAlarm test conditions based on environment variables. * KALARM_TIME: specifies current system time (format [[[yyyy-]mm-]dd-]hh:mm [TZ]). */ void setTestModeConditions() { const QByteArray newTime = qgetenv("KALARM_TIME"); if (!newTime.isEmpty()) { KADateTime dt; if (AlarmTime::convertTimeString(newTime, dt, KADateTime::realCurrentLocalDateTime(), true)) setSimulatedSystemTime(dt); } } /****************************************************************************** * Set the simulated system time. */ void setSimulatedSystemTime(const KADateTime& dt) { KADateTime::setSimulatedSystemTime(dt); qCDebug(KALARM_LOG) << "New time =" << qPrintable(KADateTime::currentLocalDateTime().toString(QStringLiteral("%Y-%m-%d %H:%M %:Z"))); } #endif } // namespace KAlarm namespace { /****************************************************************************** * Display an error message about an error when saving an event. */ void displayUpdateError(QWidget* parent, KAlarm::UpdateError code, const UpdateStatusData& status, bool showKOrgError) { QString errmsg; if (status.status.status > KAlarm::UPDATE_KORG_ERR) { switch (code) { case KAlarm::ERR_ADD: case KAlarm::ERR_MODIFY: errmsg = (status.warnErr > 1) ? i18nc("@info", "Error saving alarms") : i18nc("@info", "Error saving alarm"); break; case KAlarm::ERR_DELETE: errmsg = (status.warnErr > 1) ? i18nc("@info", "Error deleting alarms") : i18nc("@info", "Error deleting alarm"); break; case KAlarm::ERR_REACTIVATE: errmsg = (status.warnErr > 1) ? i18nc("@info", "Error saving reactivated alarms") : i18nc("@info", "Error saving reactivated alarm"); break; case KAlarm::ERR_TEMPLATE: errmsg = (status.warnErr > 1) ? i18nc("@info", "Error saving alarm templates") : i18nc("@info", "Error saving alarm template"); break; } KAMessageBox::error(parent, errmsg); } else if (showKOrgError) displayKOrgUpdateError(parent, code, status.status, status.warnKOrg); } /****************************************************************************** * Tell KOrganizer to put an alarm in its calendar. * It will be held by KOrganizer as a simple event, without alarms - KAlarm * is still responsible for alarming. */ KAlarm::UpdateResult sendToKOrganizer(const KAEvent& event) { Event::Ptr kcalEvent(new KCalendarCore::Event); event.updateKCalEvent(kcalEvent, KAEvent::UID_IGNORE); // Change the event ID to avoid duplicating the same unique ID as the original event const QString uid = uidKOrganizer(event.id()); kcalEvent->setUid(uid); kcalEvent->clearAlarms(); QString userEmail; switch (event.actionTypes()) { case KAEvent::ACT_DISPLAY: case KAEvent::ACT_COMMAND: case KAEvent::ACT_DISPLAY_COMMAND: kcalEvent->setSummary(event.cleanText()); userEmail = Preferences::emailAddress(); break; case KAEvent::ACT_EMAIL: { const QString from = event.emailFromId() ? Identities::identityManager()->identityForUoid(event.emailFromId()).fullEmailAddr() : Preferences::emailAddress(); AlarmText atext; atext.setEmail(event.emailAddresses(QStringLiteral(", ")), from, QString(), QString(), event.emailSubject(), QString()); kcalEvent->setSummary(atext.displayText()); userEmail = from; break; } case KAEvent::ACT_AUDIO: kcalEvent->setSummary(event.audioFile()); break; default: break; } const Person person(QString(), userEmail); kcalEvent->setOrganizer(person); kcalEvent->setDuration(Duration(Preferences::kOrgEventDuration() * 60, Duration::Seconds)); // Translate the event into string format ICalFormat format; format.setTimeZone(Preferences::timeSpecAsZone()); const QString iCal = format.toICalString(kcalEvent); // Send the event to KOrganizer KAlarm::UpdateResult status = runKOrganizer(); // start KOrganizer if it isn't already running, and create its D-Bus interface if (status != KAlarm::UPDATE_OK) return status; QDBusInterface korgInterface(KORG_DBUS_SERVICE, QStringLiteral(KORG_DBUS_PATH), KORG_DBUS_IFACE); const QList args{iCal}; QDBusReply reply = korgInterface.callWithArgumentList(QDBus::Block, QStringLiteral("addIncidence"), args); if (!reply.isValid()) { if (reply.error().type() == QDBusError::UnknownObject) { status = KAlarm::UPDATE_KORG_ERRSTART; qCCritical(KALARM_LOG) << "KAlarm::sendToKOrganizer: addIncidence() D-Bus error: still starting"; } else { status.set(KAlarm::UPDATE_KORG_ERR, reply.error().message()); qCCritical(KALARM_LOG) << "KAlarm::sendToKOrganizer: addIncidence(" << uid << ") D-Bus call failed:" << status.message; } } else if (!reply.value()) { status = KAlarm::UPDATE_KORG_FUNCERR; qCDebug(KALARM_LOG) << "KAlarm::sendToKOrganizer: addIncidence(" << uid << ") D-Bus call returned false"; } else qCDebug(KALARM_LOG) << "KAlarm::sendToKOrganizer:" << uid << ": success"; return status; } /****************************************************************************** * Tell KOrganizer to delete an event from its calendar. */ KAlarm::UpdateResult deleteFromKOrganizer(const QString& eventID) { const QString newID = uidKOrganizer(eventID); new CollectionSearch(KORG_MIME_TYPE, QString(), newID, true); // this auto-deletes when complete // Ignore errors return KAlarm::UpdateResult(KAlarm::UPDATE_OK); } /****************************************************************************** * Start KOrganizer if not already running, and create its D-Bus interface. */ KAlarm::UpdateResult runKOrganizer() { KAlarm::UpdateResult status; // If Kontact is running, there is a load() method which needs to be called to // load KOrganizer into Kontact. But if KOrganizer is running independently, // the load() method doesn't exist. This call starts korganizer if needed, too. QDBusInterface iface(KORG_DBUS_SERVICE, QStringLiteral(KORG_DBUS_LOAD_PATH), QStringLiteral("org.kde.PIMUniqueApplication")); QDBusReply reply = iface.call(QStringLiteral("load")); if ((!reply.isValid() || !reply.value()) && iface.lastError().type() != QDBusError::UnknownMethod) { status.set(KAlarm::UPDATE_KORG_ERR, iface.lastError().message()); qCWarning(KALARM_LOG) << "Loading KOrganizer failed:" << status.message; return status; } return status; } /****************************************************************************** * Insert a KOrganizer string after the hyphen in the supplied event ID. */ QString uidKOrganizer(const QString& id) { if (id.startsWith(KORGANIZER_UID)) return id; QString result = id; return result.insert(0, KORGANIZER_UID); } } // namespace /****************************************************************************** * Case insensitive comparison for use by qSort(). */ bool caseInsensitiveLessThan(const QString& s1, const QString& s2) { return s1.toLower() < s2.toLower(); } // vim: et sw=4: diff --git a/src/functions.h b/src/functions.h index d983bb78..9c305f44 100644 --- a/src/functions.h +++ b/src/functions.h @@ -1,203 +1,203 @@ /* * functions.h - miscellaneous functions * Program: kalarm * Copyright © 2007-2019 David Jarvie * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef FUNCTIONS_H #define FUNCTIONS_H /** @file functions.h - miscellaneous functions */ #include "editdlg.h" #include "eventid.h" #include -#include +#include #include #include #include #include #include using namespace KAlarmCal; namespace KCal { class Event; } class QWidget; class QAction; class QAction; class KToggleAction; class Resource; class MainWindow; class AlarmListModel; namespace KAlarm { /** Return codes from fileType() */ enum FileType { Unknown, TextPlain, TextFormatted, TextApplication, Image }; /** Return codes from calendar update functions. * The codes are ordered by severity, so... * DO NOT CHANGE THE ORDER OF THESE VALUES! */ enum UpdateStatus { UPDATE_OK, // update succeeded UPDATE_KORG_FUNCERR, // update succeeded, but KOrganizer reported an error updating UPDATE_KORG_ERRSTART, // update succeeded, but KOrganizer update failed (KOrganizer not fully started) UPDATE_KORG_ERRINIT, // update succeeded, but KOrganizer update failed (KOrganizer not started) UPDATE_KORG_ERR, // update succeeded, but KOrganizer update failed UPDATE_ERROR, // update failed partially UPDATE_FAILED, // update failed completely SAVE_FAILED // calendar was updated in memory, but save failed }; /** Error codes supplied as parameter to displayUpdateError() */ enum UpdateError { ERR_ADD, ERR_MODIFY, ERR_DELETE, ERR_REACTIVATE, ERR_TEMPLATE }; /** Result of calendar update. */ struct UpdateResult { UpdateStatus status; // status code QString message; // error message if any UpdateResult() : status(UPDATE_OK) {} explicit UpdateResult(UpdateStatus s, const QString& m = QString()) : status(s), message(m) {} UpdateResult& operator=(UpdateStatus s) { status = s; message.clear(); return *this; } bool operator==(UpdateStatus s) const { return status == s; } bool operator!=(UpdateStatus s) const { return status != s; } void set(UpdateStatus s) { operator=(s); } void set(UpdateStatus s, const QString& m) { status = s; message = m; } }; /** Desktop identity, obtained from XDG_CURRENT_DESKTOP. */ enum class Desktop { Kde, //!< KDE (KDE 4 and Plasma both identify as "KDE") Unity, //!< Unity Other }; /** Display a main window with the specified event selected */ MainWindow* displayMainWindowSelected(const QString& eventId); bool readConfigWindowSize(const char* window, QSize&, int* splitterWidth = nullptr); void writeConfigWindowSize(const char* window, const QSize&, int splitterWidth = -1); /** Check from its mime type whether a file appears to be a text or image file. * If a text file, its type is distinguished. */ FileType fileType(const QMimeType& mimetype); /** Check that a file exists and is a plain readable file, optionally a text/image file. * Display a Continue/Cancel error message if 'errmsgParent' non-null. */ enum FileErr { FileErr_None = 0, FileErr_Blank, // generic blank error FileErr_Nonexistent, FileErr_Directory, FileErr_Unreadable, FileErr_NotTextImage, FileErr_BlankDisplay, // blank error to use for file to display FileErr_BlankPlay // blank error to use for file to play }; FileErr checkFileExists(QString& filename, QUrl&); bool showFileErrMessage(const QString& filename, FileErr, FileErr blankError, QWidget* errmsgParent); /** If a url string is a local file, strip off the 'file:/' prefix. */ QString pathOrUrl(const QString& url); bool browseFile(QString& file, const QString& caption, QString& defaultDir, const QString& initialFile = QString(), const QString& filter = QString(), bool existing = false, QWidget* parent = nullptr); bool editNewAlarm(const QString& templateName, QWidget* parent = nullptr); void editNewAlarm(EditAlarmDlg::Type, QWidget* parent = nullptr); void editNewAlarm(KAEvent::SubAction, QWidget* parent = nullptr, const AlarmText* = nullptr); void editNewAlarm(const KAEvent* preset, QWidget* parent = nullptr); void editAlarm(KAEvent*, QWidget* parent = nullptr); bool editAlarmById(const EventId& eventID, QWidget* parent = nullptr); void updateEditedAlarm(EditAlarmDlg*, KAEvent&, Resource&); void viewAlarm(const KAEvent*, QWidget* parent = nullptr); void editNewTemplate(EditAlarmDlg::Type, QWidget* parent = nullptr); void editNewTemplate(const KAEvent* preset, QWidget* parent = nullptr); void editTemplate(KAEvent*, QWidget* parent = nullptr); void execNewAlarmDlg(EditAlarmDlg*); /** Create a "New From Template" QAction */ KToggleAction* createAlarmEnableAction(QObject* parent); QAction* createStopPlayAction(QObject* parent); KToggleAction* createSpreadWindowsAction(QObject* parent); /** Returns a list of all alarm templates. * If shell commands are disabled, command alarm templates are omitted. */ KAEvent::List templateList(); void outputAlarmWarnings(QWidget* parent, const KAEvent* = nullptr); void refreshAlarms(); void refreshAlarmsIfQueued(); // must only be called from KAlarmApp::processQueue() QString runKMail(); QStringList dontShowErrors(const EventId&); bool dontShowErrors(const EventId&, const QString& tag); void setDontShowErrors(const EventId&, const QStringList& tags = QStringList()); void setDontShowErrors(const EventId&, const QString& tag); void setDontShowErrors(const QString& eventId, const QString& tag); enum // 'options' parameter values for addEvent(). May be OR'ed together. { USE_EVENT_ID = 0x01, // use event ID if it's provided NO_RESOURCE_PROMPT = 0x02, // don't prompt for resource ALLOW_KORG_UPDATE = 0x04 // allow change to be sent to KOrganizer }; UpdateResult addEvent(KAEvent&, Resource* = nullptr, QWidget* msgParent = nullptr, int options = ALLOW_KORG_UPDATE, bool showKOrgErr = true); UpdateResult addEvents(QVector&, QWidget* msgParent = nullptr, bool allowKOrgUpdate = true, bool showKOrgErr = true); bool addArchivedEvent(KAEvent&, Resource* = nullptr); UpdateResult addTemplate(KAEvent&, Resource* = nullptr, QWidget* msgParent = nullptr); UpdateResult modifyEvent(KAEvent& oldEvent, KAEvent& newEvent, QWidget* msgParent = nullptr, bool showKOrgErr = true); UpdateResult updateEvent(KAEvent&, QWidget* msgParent = nullptr, bool archiveOnDelete = true); UpdateResult updateTemplate(KAEvent&, QWidget* msgParent = nullptr); UpdateResult deleteEvent(KAEvent&, bool archive = true, QWidget* msgParent = nullptr, bool showKOrgErr = true); UpdateResult deleteEvents(QVector&, bool archive = true, QWidget* msgParent = nullptr, bool showKOrgErr = true); UpdateResult deleteTemplates(const KAEvent::List& events, QWidget* msgParent = nullptr); inline UpdateResult deleteTemplate(KAEvent& event, QWidget* msgParent = nullptr) { KAEvent::List e; e += &event; return deleteTemplates(e, msgParent); } void deleteDisplayEvent(const QString& eventID); UpdateResult reactivateEvent(KAEvent&, Resource* = nullptr, QWidget* msgParent = nullptr, bool showKOrgErr = true); UpdateResult reactivateEvents(QVector&, QVector& ineligibleIDs, Resource* = nullptr, QWidget* msgParent = nullptr, bool showKOrgErr = true); UpdateResult enableEvents(QVector&, bool enable, QWidget* msgParent = nullptr); QVector getSortedActiveEvents(QObject* parent, AlarmListModel** model = nullptr); void purgeArchive(int purgeDays); // must only be called from KAlarmApp::processQueue() void displayKOrgUpdateError(QWidget* parent, UpdateError, const UpdateResult& korgError, int nAlarms = 0); Desktop currentDesktopIdentity(); QString currentDesktopIdentityName(); QStringList checkRtcWakeConfig(bool checkEventExists = false); void deleteRtcWakeConfig(); void cancelRtcWake(QWidget* msgParent, const QString& eventId = QString()); bool setRtcWakeTime(unsigned triggerTime, QWidget* parent); /** Return a prompt string to ask the user whether to convert the calendar to the * current format. * @param calendarName The calendar name * @param calendarVersion The calendar version * @param whole If true, the whole calendar needs to be converted; else * only some alarms may need to be converted. */ QString conversionPrompt(const QString& calendarName, const QString& calendarVersion, bool whole); #ifndef NDEBUG void setTestModeConditions(); void setSimulatedSystemTime(const KADateTime&); #endif } // namespace KAlarm bool caseInsensitiveLessThan(const QString& s1, const QString& s2); #endif // FUNCTIONS_H // vim: et sw=4: diff --git a/src/kalarmapp.cpp b/src/kalarmapp.cpp index 8467c802..4220a888 100644 --- a/src/kalarmapp.cpp +++ b/src/kalarmapp.cpp @@ -1,2511 +1,2511 @@ /* * kalarmapp.cpp - the KAlarm application object * Program: kalarm * Copyright © 2001-2019 David Jarvie * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kalarmapp.h" #include "alarmcalendar.h" #include "alarmtime.h" #include "commandoptions.h" #include "dbushandler.h" #include "editdlgtypes.h" #include "functions.h" #include "kamail.h" #include "mainwindow.h" #include "messagebox.h" #include "messagewin.h" #include "kalarmmigrateapplication.h" #include "preferences.h" #include "prefdlg.h" #include "shellprocess.h" #include "startdaytimer.h" #include "traywindow.h" #include "resources/resources.h" #include "resources/eventmodel.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 #include #include #include #include static const int AKONADI_TIMEOUT = 30; // timeout (seconds) for Akonadi collections to be populated /****************************************************************************** * Find the maximum number of seconds late which a late-cancel alarm is allowed * to be. This is calculated as the late cancel interval, plus a few seconds * leeway to cater for any timing irregularities. */ static inline int maxLateness(int lateCancel) { static const int LATENESS_LEEWAY = 5; int lc = (lateCancel >= 1) ? (lateCancel - 1)*60 : 0; return LATENESS_LEEWAY + lc; } KAlarmApp* KAlarmApp::mInstance = nullptr; int KAlarmApp::mActiveCount = 0; int KAlarmApp::mFatalError = 0; QString KAlarmApp::mFatalMessage; /****************************************************************************** * Construct the application. */ KAlarmApp::KAlarmApp(int& argc, char** argv) : QApplication(argc, argv) , mDBusHandler(new DBusHandler()) { qCDebug(KALARM_LOG) << "KAlarmApp:"; KAlarmMigrateApplication migrate; migrate.migrate(); #ifndef NDEBUG KAlarm::setTestModeConditions(); #endif setQuitOnLastWindowClosed(false); Preferences::self(); // read KAlarm configuration if (!Preferences::noAutoStart()) { // Strip out any "OnlyShowIn=KDE" list from kalarm.autostart.desktop Preferences::setNoAutoStart(false); // Enable kalarm.autostart.desktop to start KAlarm Preferences::setAutoStart(true); Preferences::self()->save(); } Preferences::connect(SIGNAL(startOfDayChanged(QTime)), this, SLOT(changeStartOfDay())); Preferences::connect(SIGNAL(workTimeChanged(QTime,QTime,QBitArray)), this, SLOT(slotWorkTimeChanged(QTime,QTime,QBitArray))); Preferences::connect(SIGNAL(holidaysChanged(KHolidays::HolidayRegion)), this, SLOT(slotHolidaysChanged(KHolidays::HolidayRegion))); Preferences::connect(SIGNAL(feb29TypeChanged(Feb29Type)), this, SLOT(slotFeb29TypeChanged(Feb29Type))); Preferences::connect(SIGNAL(showInSystemTrayChanged(bool)), this, SLOT(slotShowInSystemTrayChanged())); Preferences::connect(SIGNAL(archivedKeepDaysChanged(int)), this, SLOT(setArchivePurgeDays())); Preferences::connect(SIGNAL(messageFontChanged(QFont)), this, SLOT(slotMessageFontChanged(QFont))); slotFeb29TypeChanged(Preferences::defaultFeb29Type()); KAEvent::setStartOfDay(Preferences::startOfDay()); KAEvent::setWorkTime(Preferences::workDays(), Preferences::workDayStart(), Preferences::workDayEnd()); KAEvent::setHolidays(Preferences::holidays()); KAEvent::setDefaultFont(Preferences::messageFont()); if (initialise()) // initialise calendars and alarm timer { connect(Resources::instance(), &Resources::resourceAdded, this, &KAlarmApp::slotResourceAdded); connect(Resources::instance(), &Resources::resourcePopulated, this, &KAlarmApp::slotResourcePopulated); connect(Resources::instance(), &Resources::resourcePopulated, this, &KAlarmApp::purgeNewArchivedDefault); connect(Resources::instance(), &Resources::resourcesCreated, this, &KAlarmApp::checkWritableCalendar); connect(AkonadiModel::instance(), &AkonadiModel::migrationCompleted, this, &KAlarmApp::checkWritableCalendar); KConfigGroup config(KSharedConfig::openConfig(), "General"); mNoSystemTray = config.readEntry("NoSystemTray", false); mOldShowInSystemTray = wantShowInSystemTray(); DateTime::setStartOfDay(Preferences::startOfDay()); mPrefsArchivedColour = Preferences::archivedColour(); } // Check if KOrganizer is installed const QString korg = QStringLiteral("korganizer"); mKOrganizerEnabled = !QStandardPaths::findExecutable(korg).isEmpty(); if (!mKOrganizerEnabled) { qCDebug(KALARM_LOG) << "KAlarmApp: KOrganizer options disabled (KOrganizer not found)"; } // Check if the window manager can't handle keyboard focus transfer between windows mWindowFocusBroken = (KAlarm::currentDesktopIdentity() == KAlarm::Desktop::Unity); if (mWindowFocusBroken) { qCDebug(KALARM_LOG) << "KAlarmApp: Window keyboard focus broken"; } } /****************************************************************************** */ KAlarmApp::~KAlarmApp() { while (!mCommandProcesses.isEmpty()) { ProcData* pd = mCommandProcesses.at(0); mCommandProcesses.pop_front(); delete pd; } AlarmCalendar::terminateCalendars(); } /****************************************************************************** * Return the one and only KAlarmApp instance. * If it doesn't already exist, it is created first. */ KAlarmApp* KAlarmApp::create(int& argc, char** argv) { if (!mInstance) { mInstance = new KAlarmApp(argc, argv); if (mFatalError) mInstance->quitFatal(); } return mInstance; } /****************************************************************************** * (Re)initialise things which are tidied up/closed by quitIf(). * Reinitialisation can be necessary if session restoration finds nothing to * restore and starts quitting the application, but KAlarm then starts up again * before the application has exited. * Reply = true if calendars were initialised successfully, * false if they were already initialised, or if initialisation failed. */ bool KAlarmApp::initialise() { if (!mAlarmTimer) { mAlarmTimer = new QTimer(this); mAlarmTimer->setSingleShot(true); connect(mAlarmTimer, &QTimer::timeout, this, &KAlarmApp::checkNextDueAlarm); } if (!AlarmCalendar::resources()) { qCDebug(KALARM_LOG) << "KAlarmApp::initialise: initialising calendars"; if (AlarmCalendar::initialiseCalendars()) { connect(AlarmCalendar::resources(), &AlarmCalendar::earliestAlarmChanged, this, &KAlarmApp::checkNextDueAlarm); connect(AlarmCalendar::resources(), &AlarmCalendar::atLoginEventAdded, this, &KAlarmApp::atLoginEventAdded); return true; } } return false; } /****************************************************************************** * Restore the saved session if required. */ bool KAlarmApp::restoreSession() { if (!isSessionRestored()) return false; if (mFatalError) { quitFatal(); return false; } // Process is being restored by session management. qCDebug(KALARM_LOG) << "KAlarmApp::restoreSession: Restoring"; ++mActiveCount; // Create the session config object now. // This is necessary since if initCheck() below causes calendars to be updated, // the session config created after that points to an invalid file, resulting // in no windows being restored followed by a later crash. KConfigGui::sessionConfig(); // When KAlarm is session restored, automatically set start-at-login to true. Preferences::self()->load(); Preferences::setAutoStart(true); Preferences::setNoAutoStart(false); Preferences::setAskAutoStart(true); // cancel any start-at-login prompt suppression Preferences::self()->save(); if (!initCheck(true)) // open the calendar file (needed for main windows), don't process queue yet { --mActiveCount; quitIf(1, true); // error opening the main calendar - quit return false; } MainWindow* trayParent = nullptr; for (int i = 1; KMainWindow::canBeRestored(i); ++i) { const QString type = KMainWindow::classNameOfToplevel(i); if (type == QLatin1String("MainWindow")) { MainWindow* win = MainWindow::create(true); win->restore(i, false); if (win->isHiddenTrayParent()) trayParent = win; else win->show(); } else if (type == QLatin1String("MessageWin")) { MessageWin* win = new MessageWin; win->restore(i, false); if (win->isValid()) { if (Resources::allCreated()) win->show(); } else delete win; } } // Try to display the system tray icon if it is configured to be shown if (trayParent || wantShowInSystemTray()) { if (!MainWindow::count()) qCWarning(KALARM_LOG) << "KAlarmApp::restoreSession: no main window to be restored!?"; else { displayTrayIcon(true, trayParent); // Occasionally for no obvious reason, the main main window is // shown when it should be hidden, so hide it just to be sure. if (trayParent) trayParent->hide(); } } --mActiveCount; if (quitIf(0)) // quit if no windows are open return false; // quitIf() can sometimes return, despite calling exit() startProcessQueue(); // start processing the execution queue return true; } /****************************************************************************** * Called for a unique QApplication when a new instance of the application is * started. * Reply: exit code (>= 0), or -1 to continue execution. * If exit code >= 0, 'outputText' holds text to output before terminating. */ void KAlarmApp::activateByDBus(const QStringList& args, const QString& workingDirectory) { activateInstance(args, workingDirectory, nullptr); } /****************************************************************************** * Called to start a new instance of the application. * Reply: exit code (>= 0), or -1 to continue execution. * If exit code >= 0, 'outputText' holds text to output before terminating. */ int KAlarmApp::activateInstance(const QStringList& args, const QString& workingDirectory, QString* outputText) { Q_UNUSED(workingDirectory) qCDebug(KALARM_LOG) << "KAlarmApp::activateInstance"; if (outputText) outputText->clear(); if (mFatalError) { quitFatal(); return 1; } // The D-Bus call to activate a subsequent instance of KAlarm may not supply // any arguments, but we need one. if (!args.isEmpty() && mActivateArg0.isEmpty()) mActivateArg0 = args[0]; QStringList fixedArgs(args); if (args.isEmpty() && !mActivateArg0.isEmpty()) fixedArgs << mActivateArg0; // Parse and interpret command line arguments. QCommandLineParser parser; KAboutData::applicationData().setupCommandLine(&parser); parser.setApplicationDescription(QApplication::applicationDisplayName()); CommandOptions* options = new CommandOptions; const QStringList newArgs = options->setOptions(&parser, fixedArgs); options->parse(); KAboutData::applicationData().processCommandLine(&parser); ++mActiveCount; int exitCode = 0; // default = success static bool firstInstance = true; bool dontRedisplay = false; CommandOptions::Command command = CommandOptions::NONE; const bool processOptions = (!firstInstance || !isSessionRestored()); if (processOptions) { options->process(); #ifndef NDEBUG if (options->simulationTime().isValid()) KAlarm::setSimulatedSystemTime(options->simulationTime()); #endif command = options->command(); if (options->disableAll()) setAlarmsEnabled(false); // disable alarm monitoring // Handle options which exit with a terminal message, before // making the application a unique application, since a // unique application won't output to the terminal if another // instance is already running. switch (command) { case CommandOptions::CMD_ERROR: if (outputText) { *outputText = options->outputText(); delete options; return 1; } mReadOnly = true; // don't need write access to calendars exitCode = 1; break; case CommandOptions::EXIT: if (outputText) { *outputText = options->outputText(); delete options; return 0; } exitCode = -1; break; default: break; } } // Make this a unique application. KDBusService* s = new KDBusService(KDBusService::Unique, this); connect(this, &KAlarmApp::aboutToQuit, s, &KDBusService::deleteLater); connect(s, &KDBusService::activateRequested, this, &KAlarmApp::activateByDBus); if (processOptions) { switch (command) { case CommandOptions::TRIGGER_EVENT: case CommandOptions::CANCEL_EVENT: { // Display or delete the event with the specified event ID const EventFunc function = (command == CommandOptions::TRIGGER_EVENT) ? EVENT_TRIGGER : EVENT_CANCEL; // Open the calendar, don't start processing execution queue yet, // and wait for the calendar resources to be populated. if (!initCheck(true, true, options->eventId().collectionId())) exitCode = 1; else { startProcessQueue(); // start processing the execution queue dontRedisplay = true; if (!handleEvent(options->eventId(), function, true)) { CommandOptions::printError(xi18nc("@info:shell", "%1: Event %2 not found, or not unique", QStringLiteral("--") + options->commandName(), options->eventId().eventId())); exitCode = 1; } } break; } case CommandOptions::LIST: // Output a list of scheduled alarms to stdout. // Open the calendar, don't start processing execution queue yet, // and wait for all calendar resources to be populated. mReadOnly = true; // don't need write access to calendars mAlarmsEnabled = false; // prevent alarms being processed if (!initCheck(true, true)) exitCode = 1; else { dontRedisplay = true; const QStringList alarms = scheduledAlarmList(); for (const QString& alarm : alarms) std::cout << alarm.toUtf8().constData() << std::endl; } break; case CommandOptions::EDIT: // Edit a specified existing alarm. // Open the calendar and wait for the calendar resources to be populated. if (!initCheck(false, true, options->eventId().collectionId())) exitCode = 1; else if (!KAlarm::editAlarmById(options->eventId())) { CommandOptions::printError(xi18nc("@info:shell", "%1: Event %2 not found, or not editable", QStringLiteral("--") + options->commandName(), options->eventId().eventId())); exitCode = 1; } break; case CommandOptions::EDIT_NEW: { // Edit a new alarm, and optionally preset selected values if (!initCheck()) exitCode = 1; else { EditAlarmDlg* editDlg = EditAlarmDlg::create(false, options->editType(), MainWindow::mainMainWindow()); if (options->alarmTime().isValid()) editDlg->setTime(options->alarmTime()); if (options->recurrence()) editDlg->setRecurrence(*options->recurrence(), options->subRepeatInterval(), options->subRepeatCount()); else if (options->flags() & KAEvent::REPEAT_AT_LOGIN) editDlg->setRepeatAtLogin(); editDlg->setAction(options->editAction(), AlarmText(options->text())); if (options->lateCancel()) editDlg->setLateCancel(options->lateCancel()); if (options->flags() & KAEvent::COPY_KORGANIZER) editDlg->setShowInKOrganizer(true); switch (options->editType()) { case EditAlarmDlg::DISPLAY: { // EditAlarmDlg::create() always returns EditDisplayAlarmDlg for type = DISPLAY EditDisplayAlarmDlg* dlg = qobject_cast(editDlg); if (options->fgColour().isValid()) dlg->setFgColour(options->fgColour()); if (options->bgColour().isValid()) dlg->setBgColour(options->bgColour()); if (!options->audioFile().isEmpty() || options->flags() & (KAEvent::BEEP | KAEvent::SPEAK)) { const KAEvent::Flags flags = options->flags(); const Preferences::SoundType type = (flags & KAEvent::BEEP) ? Preferences::Sound_Beep : (flags & KAEvent::SPEAK) ? Preferences::Sound_Speak : Preferences::Sound_File; dlg->setAudio(type, options->audioFile(), options->audioVolume(), (flags & KAEvent::REPEAT_SOUND ? 0 : -1)); } if (options->reminderMinutes()) dlg->setReminder(options->reminderMinutes(), (options->flags() & KAEvent::REMINDER_ONCE)); if (options->flags() & KAEvent::CONFIRM_ACK) dlg->setConfirmAck(true); if (options->flags() & KAEvent::AUTO_CLOSE) dlg->setAutoClose(true); break; } case EditAlarmDlg::COMMAND: break; case EditAlarmDlg::EMAIL: { // EditAlarmDlg::create() always returns EditEmailAlarmDlg for type = EMAIL EditEmailAlarmDlg* dlg = qobject_cast(editDlg); if (options->fromID() || !options->addressees().isEmpty() || !options->subject().isEmpty() || !options->attachments().isEmpty()) dlg->setEmailFields(options->fromID(), options->addressees(), options->subject(), options->attachments()); if (options->flags() & KAEvent::EMAIL_BCC) dlg->setBcc(true); break; } case EditAlarmDlg::AUDIO: { // EditAlarmDlg::create() always returns EditAudioAlarmDlg for type = AUDIO EditAudioAlarmDlg* dlg = qobject_cast(editDlg); if (!options->audioFile().isEmpty() || options->audioVolume() >= 0) dlg->setAudio(options->audioFile(), options->audioVolume()); break; } case EditAlarmDlg::NO_TYPE: break; } // Execute the edit dialogue. Note that if no other instance of KAlarm is // running, this new instance will not exit after the dialogue is closed. // This is deliberate, since exiting would mean that KAlarm wouldn't // trigger the new alarm. KAlarm::execNewAlarmDlg(editDlg); } break; } case CommandOptions::EDIT_NEW_PRESET: // Edit a new alarm, preset with a template if (!initCheck()) exitCode = 1; else { // Execute the edit dialogue. Note that if no other instance of KAlarm is // running, this new instance will not exit after the dialogue is closed. // This is deliberate, since exiting would mean that KAlarm wouldn't // trigger the new alarm. KAlarm::editNewAlarm(options->templateName()); } break; case CommandOptions::NEW: // Display a message or file, execute a command, or send an email if (!initCheck() || !scheduleEvent(options->editAction(), options->text(), options->alarmTime(), options->lateCancel(), options->flags(), options->bgColour(), options->fgColour(), QFont(), options->audioFile(), options->audioVolume(), options->reminderMinutes(), (options->recurrence() ? *options->recurrence() : KARecurrence()), options->subRepeatInterval(), options->subRepeatCount(), options->fromID(), options->addressees(), options->subject(), options->attachments())) exitCode = 1; break; case CommandOptions::TRAY: // Display only the system tray icon if (Preferences::showInSystemTray() && QSystemTrayIcon::isSystemTrayAvailable()) { if (!initCheck() // open the calendar, start processing execution queue || !displayTrayIcon(true)) exitCode = 1; break; } Q_FALLTHROUGH(); // fall through to NONE case CommandOptions::NONE: // No arguments - run interactively & display the main window #ifndef NDEBUG if (options->simulationTime().isValid() && !firstInstance) break; // simulating time: don't open main window if already running #endif if (!initCheck()) exitCode = 1; else { if (mTrayWindow && mTrayWindow->assocMainWindow() && !mTrayWindow->assocMainWindow()->isVisible()) mTrayWindow->showAssocMainWindow(); else { MainWindow* win = MainWindow::create(); if (command == CommandOptions::TRAY) win->setWindowState(win->windowState() | Qt::WindowMinimized); win->show(); } } break; default: break; } } if (options != CommandOptions::firstInstance()) delete options; // If this is the first time through, redisplay any alarm message windows // from last time. if (firstInstance && !dontRedisplay && !exitCode) { /* First time through, so redisplay alarm message windows from last time. * But it is possible for session restoration in some circumstances to * not create any windows, in which case the alarm calendars will have * been deleted - if so, don't try to do anything. (This has been known * to happen under the Xfce desktop.) */ if (AlarmCalendar::resources()) { if (Resources::allCreated()) { mRedisplayAlarms = false; MessageWin::redisplayAlarms(); } else mRedisplayAlarms = true; } } --mActiveCount; firstInstance = false; // Quit the application if this was the last/only running "instance" of the program. // Executing 'return' doesn't work very well since the program continues to // run if no windows were created. if (quitIf(exitCode >= 0 ? exitCode : 0)) return exitCode; // exit this application instance return -1; // continue executing the application instance } /****************************************************************************** * Quit the program, optionally only if there are no more "instances" running. * Reply = true if program exited. */ bool KAlarmApp::quitIf(int exitCode, bool force) { if (force) { // Quit regardless, except for message windows mQuitting = true; MainWindow::closeAll(); mQuitting = false; displayTrayIcon(false); if (MessageWin::instanceCount(true)) // ignore always-hidden windows (e.g. audio alarms) return false; } else if (mQuitting) return false; // MainWindow::closeAll() causes quitIf() to be called again else { // Quit only if there are no more "instances" running mPendingQuit = false; if (mActiveCount > 0 || MessageWin::instanceCount(true)) // ignore always-hidden windows (e.g. audio alarms) return false; const int mwcount = MainWindow::count(); MainWindow* mw = mwcount ? MainWindow::firstWindow() : nullptr; if (mwcount > 1 || (mwcount && (!mw->isHidden() || !mw->isTrayParent()))) return false; // There are no windows left except perhaps a main window which is a hidden // tray icon parent, or an always-hidden message window. if (mTrayWindow) { // There is a system tray icon. // Don't exit unless the system tray doesn't seem to exist. if (checkSystemTray()) return false; } if (!mActionQueue.isEmpty() || !mCommandProcesses.isEmpty()) { // Don't quit yet if there are outstanding actions on the execution queue mPendingQuit = true; mPendingQuitCode = exitCode; return false; } } // This was the last/only running "instance" of the program, so exit completely. // NOTE: Everything which is terminated/deleted here must where applicable // be initialised in the initialise() method, in case KAlarm is // started again before application exit completes! qCDebug(KALARM_LOG) << "KAlarmApp::quitIf:" << exitCode << ": quitting"; MessageWin::stopAudio(true); if (mCancelRtcWake) { KAlarm::setRtcWakeTime(0, nullptr); KAlarm::deleteRtcWakeConfig(); } delete mAlarmTimer; // prevent checking for alarms after deleting calendars mAlarmTimer = nullptr; mInitialised = false; // prevent processQueue() from running AlarmCalendar::terminateCalendars(); exit(exitCode); return true; // sometimes we actually get to here, despite calling exit() } /****************************************************************************** * Called when the Quit menu item is selected. * Closes the system tray window and all main windows, but does not exit the * program if other windows are still open. */ void KAlarmApp::doQuit(QWidget* parent) { qCDebug(KALARM_LOG) << "KAlarmApp::doQuit"; if (KAMessageBox::warningCancelContinue(parent, i18nc("@info", "Quitting will disable alarms (once any alarm message windows are closed)."), QString(), KStandardGuiItem::quit(), KStandardGuiItem::cancel(), Preferences::QUIT_WARN ) != KMessageBox::Continue) return; if (!KAlarm::checkRtcWakeConfig(true).isEmpty()) { // A wake-on-suspend alarm is set if (KAMessageBox::warningCancelContinue(parent, i18nc("@info", "Quitting will cancel the scheduled Wake from Suspend."), QString(), KStandardGuiItem::quit() ) != KMessageBox::Continue) return; mCancelRtcWake = true; } if (!Preferences::autoStart()) { int option = KMessageBox::No; if (!Preferences::autoStartChangedByUser()) { option = KAMessageBox::questionYesNoCancel(parent, xi18nc("@info", "Do you want to start KAlarm at login?" "(Note that alarms will be disabled if KAlarm is not started.)"), QString(), KStandardGuiItem::yes(), KStandardGuiItem::no(), KStandardGuiItem::cancel(), Preferences::ASK_AUTO_START); } switch (option) { case KMessageBox::Yes: Preferences::setAutoStart(true); Preferences::setNoAutoStart(false); break; case KMessageBox::No: Preferences::setNoAutoStart(true); break; case KMessageBox::Cancel: default: return; } Preferences::self()->save(); } quitIf(0, true); } /****************************************************************************** * Display an error message for a fatal error. Prevent further actions since * the program state is unsafe. */ void KAlarmApp::displayFatalError(const QString& message) { if (!mFatalError) { mFatalError = 1; mFatalMessage = message; if (mInstance) QTimer::singleShot(0, mInstance, &KAlarmApp::quitFatal); } } /****************************************************************************** * Quit the program, once the fatal error message has been acknowledged. */ void KAlarmApp::quitFatal() { switch (mFatalError) { case 0: case 2: return; case 1: mFatalError = 2; KMessageBox::error(nullptr, mFatalMessage); // this is an application modal window mFatalError = 3; Q_FALLTHROUGH(); // fall through to '3' case 3: if (mInstance) mInstance->quitIf(1, true); break; } QTimer::singleShot(1000, this, &KAlarmApp::quitFatal); } /****************************************************************************** * Called by the alarm timer when the next alarm is due. * Also called when the execution queue has finished processing to check for the * next alarm. */ void KAlarmApp::checkNextDueAlarm() { if (!mAlarmsEnabled) return; // Find the first alarm due const KAEvent* nextEvent = AlarmCalendar::resources()->earliestAlarm(); if (!nextEvent) return; // there are no alarms pending const KADateTime nextDt = nextEvent->nextTrigger(KAEvent::ALL_TRIGGER).effectiveKDateTime(); const KADateTime now = KADateTime::currentDateTime(Preferences::timeSpec()); qint64 interval = now.msecsTo(nextDt); qCDebug(KALARM_LOG) << "KAlarmApp::checkNextDueAlarm: now:" << qPrintable(now.toString(QStringLiteral("%Y-%m-%d %H:%M %:Z"))) << ", next:" << qPrintable(nextDt.toString(QStringLiteral("%Y-%m-%d %H:%M %:Z"))) << ", due:" << interval; if (interval <= 0) { // Queue the alarm queueAlarmId(*nextEvent); qCDebug(KALARM_LOG) << "KAlarmApp::checkNextDueAlarm:" << nextEvent->id() << ": due now"; QTimer::singleShot(0, this, &KAlarmApp::processQueue); } else { // No alarm is due yet, so set timer to wake us when it's due. // Check for integer overflow before setting timer. #pragma message("TODO: use hibernation wakeup signal") #ifndef HIBERNATION_SIGNAL /* TODO: REPLACE THIS CODE WHEN A SYSTEM NOTIFICATION SIGNAL BECOMES * AVAILABLE FOR WAKEUP FROM HIBERNATION. * Re-evaluate the next alarm time every minute, in case the * system clock jumps. The most common case when the clock jumps * is when a laptop wakes from hibernation. If timers were left to * run, they would trigger late by the length of time the system * was asleep. */ if (interval > 60000) // 1 minute interval = 60000; #endif ++interval; // ensure we don't trigger just before the minute boundary if (interval > INT_MAX) interval = INT_MAX; qCDebug(KALARM_LOG) << "KAlarmApp::checkNextDueAlarm:" << nextEvent->id() << "wait" << interval/1000 << "seconds"; mAlarmTimer->start(static_cast(interval)); } } /****************************************************************************** * Called by the alarm timer when the next alarm is due. * Also called when the execution queue has finished processing to check for the * next alarm. */ void KAlarmApp::queueAlarmId(const KAEvent& event) { const EventId id(event); for (const ActionQEntry& entry : qAsConst(mActionQueue)) { if (entry.function == EVENT_HANDLE && entry.eventId == id) return; // the alarm is already queued } mActionQueue.enqueue(ActionQEntry(EVENT_HANDLE, id)); } /****************************************************************************** * Start processing the execution queue. */ void KAlarmApp::startProcessQueue() { if (!mInitialised) { qCDebug(KALARM_LOG) << "KAlarmApp::startProcessQueue"; mInitialised = true; QTimer::singleShot(0, this, &KAlarmApp::processQueue); // process anything already queued } } /****************************************************************************** * The main processing loop for KAlarm. * All KAlarm operations involving opening or updating calendar files are called * from this loop to ensure that only one operation is active at any one time. * This precaution is necessary because KAlarm's activities are mostly * asynchronous, being in response to D-Bus calls from other programs or timer * events, any of which can be received in the middle of performing another * operation. If a calendar file is opened or updated while another calendar * operation is in progress, the program has been observed to hang, or the first * calendar call has failed with data loss - clearly unacceptable!! */ void KAlarmApp::processQueue() { if (mInitialised && !mProcessingQueue) { qCDebug(KALARM_LOG) << "KAlarmApp::processQueue"; mProcessingQueue = true; // Refresh alarms if that's been queued KAlarm::refreshAlarmsIfQueued(); // Process queued events while (!mActionQueue.isEmpty()) { ActionQEntry& entry = mActionQueue.head(); if (entry.eventId.isEmpty()) { // It's a new alarm switch (entry.function) { case EVENT_TRIGGER: execAlarm(entry.event, entry.event.firstAlarm(), false); break; case EVENT_HANDLE: KAlarm::addEvent(entry.event, nullptr, nullptr, KAlarm::ALLOW_KORG_UPDATE | KAlarm::NO_RESOURCE_PROMPT); break; case EVENT_CANCEL: break; } } else handleEvent(entry.eventId, entry.function); mActionQueue.dequeue(); } // Purge the default archived alarms resource if it's time to do so if (mPurgeDaysQueued >= 0) { KAlarm::purgeArchive(mPurgeDaysQueued); mPurgeDaysQueued = -1; } // Now that the queue has been processed, quit if a quit was queued if (mPendingQuit) { if (quitIf(mPendingQuitCode)) return; // quitIf() can sometimes return, despite calling exit() } mProcessingQueue = false; // Schedule the application to be woken when the next alarm is due checkNextDueAlarm(); } } /****************************************************************************** * Called when a repeat-at-login alarm has been added externally. * Queues the alarm for triggering. * First, cancel any scheduled reminder or deferral for it, since these will be * superseded by the new at-login trigger. */ void KAlarmApp::atLoginEventAdded(const KAEvent& event) { KAEvent ev = event; if (!cancelReminderAndDeferral(ev)) { if (mAlarmsEnabled) { mActionQueue.enqueue(ActionQEntry(EVENT_HANDLE, EventId(ev))); if (mInitialised) QTimer::singleShot(0, this, &KAlarmApp::processQueue); } } } /****************************************************************************** * Called when the system tray main window is closed. */ void KAlarmApp::removeWindow(TrayWindow*) { mTrayWindow = nullptr; } /****************************************************************************** * Display or close the system tray icon. */ bool KAlarmApp::displayTrayIcon(bool show, MainWindow* parent) { qCDebug(KALARM_LOG) << "KAlarmApp::displayTrayIcon"; static bool creating = false; if (show) { if (!mTrayWindow && !creating) { if (!QSystemTrayIcon::isSystemTrayAvailable()) return false; if (!MainWindow::count()) { // We have to have at least one main window to act // as parent to the system tray icon (even if the // window is hidden). creating = true; // prevent main window constructor from creating an additional tray icon parent = MainWindow::create(); creating = false; } mTrayWindow = new TrayWindow(parent ? parent : MainWindow::firstWindow()); connect(mTrayWindow, &TrayWindow::deleted, this, &KAlarmApp::trayIconToggled); Q_EMIT trayIconToggled(); if (!checkSystemTray()) quitIf(0); // exit the application if there are no open windows } } else { delete mTrayWindow; mTrayWindow = nullptr; } return true; } /****************************************************************************** * Check whether the system tray icon has been housed in the system tray. */ bool KAlarmApp::checkSystemTray() { if (!mTrayWindow) return true; if (QSystemTrayIcon::isSystemTrayAvailable() == mNoSystemTray) { qCDebug(KALARM_LOG) << "KAlarmApp::checkSystemTray: changed ->" << mNoSystemTray; mNoSystemTray = !mNoSystemTray; // Store the new setting in the config file, so that if KAlarm exits it will // restart with the correct default. KConfigGroup config(KSharedConfig::openConfig(), "General"); config.writeEntry("NoSystemTray", mNoSystemTray); config.sync(); // Update other settings slotShowInSystemTrayChanged(); } return !mNoSystemTray; } /****************************************************************************** * Return the main window associated with the system tray icon. */ MainWindow* KAlarmApp::trayMainWindow() const { return mTrayWindow ? mTrayWindow->assocMainWindow() : nullptr; } /****************************************************************************** * Called when the show-in-system-tray preference setting has changed, to show * or hide the system tray icon. */ void KAlarmApp::slotShowInSystemTrayChanged() { const bool newShowInSysTray = wantShowInSystemTray(); if (newShowInSysTray != mOldShowInSystemTray) { // The system tray run mode has changed ++mActiveCount; // prevent the application from quitting MainWindow* win = mTrayWindow ? mTrayWindow->assocMainWindow() : nullptr; delete mTrayWindow; // remove the system tray icon if it is currently shown mTrayWindow = nullptr; mOldShowInSystemTray = newShowInSysTray; if (newShowInSysTray) { // Show the system tray icon displayTrayIcon(true); } else { // Stop showing the system tray icon if (win && win->isHidden()) { if (MainWindow::count() > 1) delete win; else { win->setWindowState(win->windowState() | Qt::WindowMinimized); win->show(); } } } --mActiveCount; } } /****************************************************************************** * Called when the start-of-day time preference setting has changed. * Change alarm times for date-only alarms. */ void KAlarmApp::changeStartOfDay() { DateTime::setStartOfDay(Preferences::startOfDay()); KAEvent::setStartOfDay(Preferences::startOfDay()); AlarmCalendar::resources()->adjustStartOfDay(); } /****************************************************************************** * Called when the default alarm message font preference setting has changed. * Notify KAEvent. */ void KAlarmApp::slotMessageFontChanged(const QFont& font) { KAEvent::setDefaultFont(font); } /****************************************************************************** * Called when the working time preference settings have changed. * Notify KAEvent. */ void KAlarmApp::slotWorkTimeChanged(const QTime& start, const QTime& end, const QBitArray& days) { KAEvent::setWorkTime(days, start, end); } /****************************************************************************** * Called when the holiday region preference setting has changed. * Notify KAEvent. */ void KAlarmApp::slotHolidaysChanged(const KHolidays::HolidayRegion& holidays) { KAEvent::setHolidays(holidays); } /****************************************************************************** * Called when the date for February 29th recurrences has changed in the * preferences settings. */ void KAlarmApp::slotFeb29TypeChanged(Preferences::Feb29Type type) { KARecurrence::Feb29Type rtype; switch (type) { default: case Preferences::Feb29_None: rtype = KARecurrence::Feb29_None; break; case Preferences::Feb29_Feb28: rtype = KARecurrence::Feb29_Feb28; break; case Preferences::Feb29_Mar1: rtype = KARecurrence::Feb29_Mar1; break; } KARecurrence::setDefaultFeb29Type(rtype); } /****************************************************************************** * Return whether the program is configured to be running in the system tray. */ bool KAlarmApp::wantShowInSystemTray() const { return Preferences::showInSystemTray() && QSystemTrayIcon::isSystemTrayAvailable(); } /****************************************************************************** * Called when all calendars have been fetched at startup. * Check whether there are any writable active calendars, and if not, warn the * user. */ void KAlarmApp::checkWritableCalendar() { if (mReadOnly) return; // don't need write access to calendars const bool treeFetched = Resources::allCreated(); if (treeFetched && mRedisplayAlarms) { mRedisplayAlarms = false; MessageWin::redisplayAlarms(); } if (!treeFetched || !AkonadiModel::instance()->isMigrationCompleted()) return; static bool done = false; if (done) return; done = true; qCDebug(KALARM_LOG) << "KAlarmApp::checkWritableCalendar"; // Check for, and remove, any duplicate Akonadi resources, i.e. those which // use the same calendar file/directory. AkonadiResource::removeDuplicateResources(); // Find whether there are any writable active alarm calendars const bool active = !Resources::enabledResources(CalEvent::ACTIVE, true).isEmpty(); if (!active) { qCWarning(KALARM_LOG) << "KAlarmApp::checkWritableCalendar: No writable active calendar"; KAMessageBox::information(MainWindow::mainMainWindow(), xi18nc("@info", "Alarms cannot be created or updated, because no writable active alarm calendar is enabled." "To fix this, use View | Show Calendars to check or change calendar statuses."), QString(), QStringLiteral("noWritableCal")); } } /****************************************************************************** * Called when a new resource has been added, to note the possible need to purge * its old alarms if it is the default archived calendar. */ void KAlarmApp::slotResourceAdded(const Resource& resource) { if (resource.alarmTypes() & CalEvent::ARCHIVED) mPendingPurges += resource.id(); } /****************************************************************************** * Called when a resource has been populated, to purge its old alarms if it is * the default archived calendar. */ void KAlarmApp::slotResourcePopulated(const Resource& resource) { if (mPendingPurges.removeAll(resource.id()) > 0) purgeNewArchivedDefault(resource); } /****************************************************************************** * Called when a new resource has been populated, or when a resource has been * set as the standard resource for its type. * If it is the default archived calendar, purge its old alarms if necessary. */ void KAlarmApp::purgeNewArchivedDefault(const Resource& resource) { if (Resources::isStandard(resource, CalEvent::ARCHIVED)) { qCDebug(KALARM_LOG) << "KAlarmApp::purgeNewArchivedDefault:" << resource.id() << ": standard archived..."; if (mArchivedPurgeDays >= 0) purge(mArchivedPurgeDays); else setArchivePurgeDays(); } } /****************************************************************************** * Called when the length of time to keep archived alarms changes in KAlarm's * preferences. * Set the number of days to keep archived alarms. * Alarms which are older are purged immediately, and at the start of each day. */ void KAlarmApp::setArchivePurgeDays() { const int newDays = Preferences::archivedKeepDays(); if (newDays != mArchivedPurgeDays) { const int oldDays = mArchivedPurgeDays; mArchivedPurgeDays = newDays; if (mArchivedPurgeDays <= 0) StartOfDayTimer::disconnect(this); if (mArchivedPurgeDays < 0) return; // keep indefinitely, so don't purge if (oldDays < 0 || mArchivedPurgeDays < oldDays) { // Alarms are now being kept for less long, so purge them purge(mArchivedPurgeDays); if (!mArchivedPurgeDays) return; // don't archive any alarms } // Start the purge timer to expire at the start of the next day // (using the user-defined start-of-day time). StartOfDayTimer::connect(this, SLOT(slotPurge())); } } /****************************************************************************** * Purge all archived events from the calendar whose end time is longer ago than * 'daysToKeep'. All events are deleted if 'daysToKeep' is zero. */ void KAlarmApp::purge(int daysToKeep) { if (mPurgeDaysQueued < 0 || daysToKeep < mPurgeDaysQueued) mPurgeDaysQueued = daysToKeep; // Do the purge once any other current operations are completed processQueue(); } /****************************************************************************** * Output a list of pending alarms, with their next scheduled occurrence. */ QStringList KAlarmApp::scheduledAlarmList() { QStringList alarms; const QVector events = KAlarm::getSortedActiveEvents(this); for (const KAEvent& event : events) { const KADateTime dateTime = event.nextTrigger(KAEvent::DISPLAY_TRIGGER).effectiveKDateTime().toLocalZone(); const Resource resource = Resources::resource(event.resourceId()); QString text(resource.configName() + QLatin1String(":")); text += event.id() + QLatin1Char(' ') + dateTime.toString(QStringLiteral("%Y%m%dT%H%M ")) + AlarmText::summary(event, 1); alarms << text; } return alarms; } /****************************************************************************** * Enable or disable alarm monitoring. */ void KAlarmApp::setAlarmsEnabled(bool enabled) { if (enabled != mAlarmsEnabled) { mAlarmsEnabled = enabled; Q_EMIT alarmEnabledToggled(enabled); if (!enabled) KAlarm::cancelRtcWake(nullptr); else if (!mProcessingQueue) checkNextDueAlarm(); } } /****************************************************************************** * Spread or collect alarm message and error message windows. */ void KAlarmApp::spreadWindows(bool spread) { spread = MessageWin::spread(spread); Q_EMIT spreadWindowsToggled(spread); } /****************************************************************************** * Called when the spread status of message windows changes. * Set the 'spread windows' action state. */ void KAlarmApp::setSpreadWindowsState(bool spread) { Q_EMIT spreadWindowsToggled(spread); } /****************************************************************************** * Check whether the window manager's handling of keyboard focus transfer * between application windows is broken. This is true for Ubuntu's Unity * desktop, where MessageWin windows steal keyboard focus from EditAlarmDlg * windows. */ bool KAlarmApp::windowFocusBroken() const { return mWindowFocusBroken; } /****************************************************************************** * Check whether window/keyboard focus currently needs to be fixed manually due * to the window manager not handling it correctly. This will occur if there are * both EditAlarmDlg and MessageWin windows currently active. */ bool KAlarmApp::needWindowFocusFix() const { return mWindowFocusBroken && MessageWin::instanceCount(true) && EditAlarmDlg::instanceCount(); } /****************************************************************************** * Called to schedule a new alarm, either in response to a DCOP notification or * to command line options. * Reply = true unless there was a parameter error or an error opening calendar file. */ bool KAlarmApp::scheduleEvent(KAEvent::SubAction action, const QString& text, const KADateTime& dateTime, int lateCancel, KAEvent::Flags flags, const QColor& bg, const QColor& fg, const QFont& font, const QString& audioFile, float audioVolume, int reminderMinutes, const KARecurrence& recurrence, const KCalendarCore::Duration& repeatInterval, int repeatCount, uint mailFromID, const KCalendarCore::Person::List& mailAddresses, const QString& mailSubject, const QStringList& mailAttachments) { qCDebug(KALARM_LOG) << "KAlarmApp::scheduleEvent:" << text; if (!dateTime.isValid()) return false; const KADateTime now = KADateTime::currentUtcDateTime(); if (lateCancel && dateTime < now.addSecs(-maxLateness(lateCancel))) return true; // alarm time was already archived too long ago KADateTime alarmTime = dateTime; // Round down to the nearest minute to avoid scheduling being messed up if (!dateTime.isDateOnly()) alarmTime.setTime(QTime(alarmTime.time().hour(), alarmTime.time().minute(), 0)); KAEvent event(alarmTime, text, bg, fg, font, action, lateCancel, flags, true); if (reminderMinutes) { const bool onceOnly = flags & KAEvent::REMINDER_ONCE; event.setReminder(reminderMinutes, onceOnly); } if (!audioFile.isEmpty()) event.setAudioFile(audioFile, audioVolume, -1, 0, (flags & KAEvent::REPEAT_SOUND) ? 0 : -1); if (!mailAddresses.isEmpty()) event.setEmail(mailFromID, mailAddresses, mailSubject, mailAttachments); event.setRecurrence(recurrence); event.setFirstRecurrence(); event.setRepetition(Repetition(repeatInterval, repeatCount - 1)); event.endChanges(); if (alarmTime <= now) { // Alarm is due for display already. // First execute it once without adding it to the calendar file. if (!mInitialised) mActionQueue.enqueue(ActionQEntry(event, EVENT_TRIGGER)); else execAlarm(event, event.firstAlarm(), false); // If it's a recurring alarm, reschedule it for its next occurrence if (!event.recurs() || event.setNextOccurrence(now) == KAEvent::NO_OCCURRENCE) return true; // It has recurrences in the future } // Queue the alarm for insertion into the calendar file mActionQueue.enqueue(ActionQEntry(event)); if (mInitialised) QTimer::singleShot(0, this, &KAlarmApp::processQueue); return true; } /****************************************************************************** * Called in response to a D-Bus request to trigger or cancel an event. * Optionally display the event. Delete the event from the calendar file and * from every main window instance. */ bool KAlarmApp::dbusHandleEvent(const EventId& eventID, EventFunc function) { qCDebug(KALARM_LOG) << "KAlarmApp::dbusHandleEvent:" << eventID; mActionQueue.append(ActionQEntry(function, eventID)); if (mInitialised) QTimer::singleShot(0, this, &KAlarmApp::processQueue); return true; } /****************************************************************************** * Called in response to a D-Bus request to list all pending alarms. */ QString KAlarmApp::dbusList() { qCDebug(KALARM_LOG) << "KAlarmApp::dbusList"; return scheduledAlarmList().join(QLatin1Char('\n')) + QLatin1Char('\n'); } /****************************************************************************** * Either: * a) Display the event and then delete it if it has no outstanding repetitions. * b) Delete the event. * c) Reschedule the event for its next repetition. If none remain, delete it. * If the event is deleted, it is removed from the calendar file and from every * main window instance. * Reply = false if event ID not found, or if more than one event with the same * ID is found. */ bool KAlarmApp::handleEvent(const EventId& id, EventFunc function, bool checkDuplicates) { // Delete any expired wake-on-suspend config data KAlarm::checkRtcWakeConfig(); const QString eventID(id.eventId()); KAEvent* event = AlarmCalendar::resources()->event(id, checkDuplicates); if (!event) { if (id.collectionId() != -1) qCWarning(KALARM_LOG) << "KAlarmApp::handleEvent: Event ID not found, or duplicated:" << eventID; else qCWarning(KALARM_LOG) << "KAlarmApp::handleEvent: Event ID not found:" << eventID; return false; } switch (function) { case EVENT_CANCEL: qCDebug(KALARM_LOG) << "KAlarmApp::handleEvent:" << eventID << ", CANCEL"; KAlarm::deleteEvent(*event, true); break; case EVENT_TRIGGER: // handle it if it's due, else execute it regardless case EVENT_HANDLE: // handle it if it's due { const KADateTime now = KADateTime::currentUtcDateTime(); qCDebug(KALARM_LOG) << "KAlarmApp::handleEvent:" << eventID << "," << (function==EVENT_TRIGGER?"TRIGGER:":"HANDLE:") << qPrintable(now.qDateTime().toString(QStringLiteral("yyyy-MM-dd hh:mm"))) << "UTC"; bool updateCalAndDisplay = false; bool alarmToExecuteValid = false; KAAlarm alarmToExecute; bool restart = false; // Check all the alarms in turn. // Note that the main alarm is fetched before any other alarms. for (KAAlarm alarm = event->firstAlarm(); alarm.isValid(); alarm = (restart ? event->firstAlarm() : event->nextAlarm(alarm)), restart = false) { // Check if the alarm is due yet. const KADateTime nextDT = alarm.dateTime(true).effectiveKDateTime(); const int secs = nextDT.secsTo(now); if (secs < 0) { // The alarm appears to be in the future. // Check if it's an invalid local time during a daylight // saving time shift, which has actually passed. if (alarm.dateTime().timeSpec() != KADateTime::LocalZone || nextDT > now.toTimeSpec(KADateTime::LocalZone)) { // This alarm is definitely not due yet qCDebug(KALARM_LOG) << "KAlarmApp::handleEvent: Alarm" << alarm.type() << "at" << nextDT.qDateTime() << ": not due"; continue; } } bool reschedule = false; bool rescheduleWork = false; if ((event->workTimeOnly() || event->holidaysExcluded()) && !alarm.deferred()) { // The alarm is restricted to working hours and/or non-holidays // (apart from deferrals). This needs to be re-evaluated every // time it triggers, since working hours could change. if (alarm.dateTime().isDateOnly()) { KADateTime dt(nextDT); dt.setDateOnly(true); reschedule = !event->isWorkingTime(dt); } else reschedule = !event->isWorkingTime(nextDT); rescheduleWork = reschedule; if (reschedule) qCDebug(KALARM_LOG) << "KAlarmApp::handleEvent: Alarm" << alarm.type() << "at" << nextDT.qDateTime() << ": not during working hours"; } if (!reschedule && alarm.repeatAtLogin()) { // Alarm is to be displayed at every login. qCDebug(KALARM_LOG) << "KAlarmApp::handleEvent: REPEAT_AT_LOGIN"; // Check if the main alarm is already being displayed. // (We don't want to display both at the same time.) if (alarmToExecute.isValid()) continue; // Set the time to display if it's a display alarm alarm.setTime(now); } if (!reschedule && event->lateCancel()) { // Alarm is due, and it is to be cancelled if too late. qCDebug(KALARM_LOG) << "KAlarmApp::handleEvent: LATE_CANCEL"; bool cancel = false; if (alarm.dateTime().isDateOnly()) { // The alarm has no time, so cancel it if its date is too far past const int maxlate = event->lateCancel() / 1440; // maximum lateness in days KADateTime limit(DateTime(nextDT.addDays(maxlate + 1)).effectiveKDateTime()); if (now >= limit) { // It's too late to display the scheduled occurrence. // Find the last previous occurrence of the alarm. DateTime next; const KAEvent::OccurType type = event->previousOccurrence(now, next, true); switch (type & ~KAEvent::OCCURRENCE_REPEAT) { case KAEvent::FIRST_OR_ONLY_OCCURRENCE: case KAEvent::RECURRENCE_DATE: case KAEvent::RECURRENCE_DATE_TIME: case KAEvent::LAST_RECURRENCE: limit.setDate(next.date().addDays(maxlate + 1)); if (now >= limit) { if (type == KAEvent::LAST_RECURRENCE || (type == KAEvent::FIRST_OR_ONLY_OCCURRENCE && !event->recurs())) cancel = true; // last occurrence (and there are no repetitions) else reschedule = true; } break; case KAEvent::NO_OCCURRENCE: default: reschedule = true; break; } } } else { // The alarm is timed. Allow it to be the permitted amount late before cancelling it. const int maxlate = maxLateness(event->lateCancel()); if (secs > maxlate) { // It's over the maximum interval late. // Find the most recent occurrence of the alarm. DateTime next; const KAEvent::OccurType type = event->previousOccurrence(now, next, true); switch (type & ~KAEvent::OCCURRENCE_REPEAT) { case KAEvent::FIRST_OR_ONLY_OCCURRENCE: case KAEvent::RECURRENCE_DATE: case KAEvent::RECURRENCE_DATE_TIME: case KAEvent::LAST_RECURRENCE: if (next.effectiveKDateTime().secsTo(now) > maxlate) { if (type == KAEvent::LAST_RECURRENCE || (type == KAEvent::FIRST_OR_ONLY_OCCURRENCE && !event->recurs())) cancel = true; // last occurrence (and there are no repetitions) else reschedule = true; } break; case KAEvent::NO_OCCURRENCE: default: reschedule = true; break; } } } if (cancel) { // All recurrences are finished, so cancel the event event->setArchive(); if (cancelAlarm(*event, alarm.type(), false)) return true; // event has been deleted updateCalAndDisplay = true; continue; } } if (reschedule) { // The latest repetition was too long ago, so schedule the next one switch (rescheduleAlarm(*event, alarm, false, (rescheduleWork ? nextDT : KADateTime()))) { case 1: // A working-time-only alarm has been rescheduled and the // rescheduled time is already due. Start processing the // event again. alarmToExecuteValid = false; restart = true; break; case -1: return true; // event has been deleted default: break; } updateCalAndDisplay = true; continue; } if (!alarmToExecuteValid) { qCDebug(KALARM_LOG) << "KAlarmApp::handleEvent: Alarm" << alarm.type() << ": execute"; alarmToExecute = alarm; // note the alarm to be displayed alarmToExecuteValid = true; // only trigger one alarm for the event } else qCDebug(KALARM_LOG) << "KAlarmApp::handleEvent: Alarm" << alarm.type() << ": skip"; } // If there is an alarm to execute, do this last after rescheduling/cancelling // any others. This ensures that the updated event is only saved once to the calendar. if (alarmToExecute.isValid()) execAlarm(*event, alarmToExecute, true, !alarmToExecute.repeatAtLogin()); else { if (function == EVENT_TRIGGER) { // The alarm is to be executed regardless of whether it's due. // Only trigger one alarm from the event - we don't want multiple // identical messages, for example. const KAAlarm alarm = event->firstAlarm(); if (alarm.isValid()) execAlarm(*event, alarm, false); } if (updateCalAndDisplay) KAlarm::updateEvent(*event); // update the window lists and calendar file else if (function != EVENT_TRIGGER) { qCDebug(KALARM_LOG) << "KAlarmApp::handleEvent: No action"; } } break; } } return true; } /****************************************************************************** * Called when an alarm action has completed, to perform any post-alarm actions. */ void KAlarmApp::alarmCompleted(const KAEvent& event) { if (!event.postAction().isEmpty()) { // doShellCommand() will error if the user is not authorised to run // shell commands. const QString command = event.postAction(); qCDebug(KALARM_LOG) << "KAlarmApp::alarmCompleted:" << event.id() << ":" << command; doShellCommand(command, event, nullptr, ProcData::POST_ACTION); } } /****************************************************************************** * Reschedule the alarm for its next recurrence after now. If none remain, * delete it. If the alarm is deleted and it is the last alarm for its event, * the event is removed from the calendar file and from every main window * instance. * If 'nextDt' is valid, the event is rescheduled for the next non-working * time occurrence after that. * Reply = 1 if 'nextDt' is valid and the rescheduled event is already due * = -1 if the event has been deleted * = 0 otherwise. */ int KAlarmApp::rescheduleAlarm(KAEvent& event, const KAAlarm& alarm, bool updateCalAndDisplay, const KADateTime& nextDt) { qCDebug(KALARM_LOG) << "KAlarmApp::rescheduleAlarm: Alarm type:" << alarm.type(); int reply = 0; bool update = false; event.startChanges(); if (alarm.repeatAtLogin()) { // Leave an alarm which repeats at every login until its main alarm triggers if (!event.reminderActive() && event.reminderMinutes() < 0) { // Executing an at-login alarm: first schedule the reminder // which occurs AFTER the main alarm. event.activateReminderAfter(KADateTime::currentUtcDateTime()); } // Repeat-at-login alarms are usually unchanged after triggering. // Ensure that the archive flag (which was set in execAlarm()) is saved. update = true; } else if (alarm.isReminder() || alarm.deferred()) { // It's a reminder alarm or an extra deferred alarm, so delete it event.removeExpiredAlarm(alarm.type()); update = true; } else { // Reschedule the alarm for its next occurrence. bool cancelled = false; DateTime last = event.mainDateTime(false); // note this trigger time if (last != event.mainDateTime(true)) last = DateTime(); // but ignore sub-repetition triggers bool next = nextDt.isValid(); KADateTime next_dt = nextDt; const KADateTime now = KADateTime::currentUtcDateTime(); do { const KAEvent::OccurType type = event.setNextOccurrence(next ? next_dt : now); switch (type) { case KAEvent::NO_OCCURRENCE: // All repetitions are finished, so cancel the event qCDebug(KALARM_LOG) << "KAlarmApp::rescheduleAlarm: No occurrence"; if (event.reminderMinutes() < 0 && last.isValid() && alarm.type() != KAAlarm::AT_LOGIN_ALARM && !event.mainExpired()) { // Set the reminder which is now due after the last main alarm trigger. // Note that at-login reminders are scheduled in execAlarm(). event.activateReminderAfter(last); updateCalAndDisplay = true; } if (cancelAlarm(event, alarm.type(), updateCalAndDisplay)) return -1; break; default: if (!(type & KAEvent::OCCURRENCE_REPEAT)) break; // Next occurrence is a repeat, so fall through to recurrence handling Q_FALLTHROUGH(); case KAEvent::RECURRENCE_DATE: case KAEvent::RECURRENCE_DATE_TIME: case KAEvent::LAST_RECURRENCE: // The event is due by now and repetitions still remain, so rewrite the event if (updateCalAndDisplay) update = true; break; case KAEvent::FIRST_OR_ONLY_OCCURRENCE: // The first occurrence is still due?!?, so don't do anything break; } if (cancelled) break; if (event.deferred()) { // Just in case there's also a deferred alarm, ensure it's removed event.removeExpiredAlarm(KAAlarm::DEFERRED_ALARM); update = true; } if (next) { // The alarm is restricted to working hours and/or non-holidays. // Check if the calculated next time is valid. next_dt = event.mainDateTime(true).effectiveKDateTime(); if (event.mainDateTime(false).isDateOnly()) { KADateTime dt(next_dt); dt.setDateOnly(true); next = !event.isWorkingTime(dt); } else next = !event.isWorkingTime(next_dt); } } while (next && next_dt <= now); reply = (!cancelled && next_dt.isValid() && (next_dt <= now)) ? 1 : 0; if (event.reminderMinutes() < 0 && last.isValid() && alarm.type() != KAAlarm::AT_LOGIN_ALARM) { // Set the reminder which is now due after the last main alarm trigger. // Note that at-login reminders are scheduled in execAlarm(). event.activateReminderAfter(last); } } event.endChanges(); if (update) KAlarm::updateEvent(event); // update the window lists and calendar file return reply; } /****************************************************************************** * Delete the alarm. If it is the last alarm for its event, the event is removed * from the calendar file and from every main window instance. * Reply = true if event has been deleted. */ bool KAlarmApp::cancelAlarm(KAEvent& event, KAAlarm::Type alarmType, bool updateCalAndDisplay) { qCDebug(KALARM_LOG) << "KAlarmApp::cancelAlarm"; if (alarmType == KAAlarm::MAIN_ALARM && !event.displaying() && event.toBeArchived()) { // The event is being deleted. Save it in the archived resources first. KAEvent ev(event); KAlarm::addArchivedEvent(ev); } event.removeExpiredAlarm(alarmType); if (!event.alarmCount()) { // If it's a command alarm being executed, mark it as deleted ProcData* pd = findCommandProcess(event.id()); if (pd) pd->eventDeleted = true; // Delete it KAlarm::deleteEvent(event, false); return true; } if (updateCalAndDisplay) KAlarm::updateEvent(event); // update the window lists and calendar file return false; } /****************************************************************************** * Cancel any reminder or deferred alarms in an repeat-at-login event. * This should be called when the event is first loaded. * If there are no more alarms left in the event, the event is removed from the * calendar file and from every main window instance. * Reply = true if event has been deleted. */ bool KAlarmApp::cancelReminderAndDeferral(KAEvent& event) { return cancelAlarm(event, KAAlarm::REMINDER_ALARM, false) || cancelAlarm(event, KAAlarm::DEFERRED_REMINDER_ALARM, false) || cancelAlarm(event, KAAlarm::DEFERRED_ALARM, true); } /****************************************************************************** * Execute an alarm by displaying its message or file, or executing its command. * Reply = ShellProcess instance if a command alarm * = MessageWin if an audio alarm * != 0 if successful * = -1 if execution has not completed * = 0 if the alarm is disabled, or if an error message was output. */ void* KAlarmApp::execAlarm(KAEvent& event, const KAAlarm& alarm, bool reschedule, bool allowDefer, bool noPreAction) { if (!mAlarmsEnabled || !event.enabled()) { // The event (or all events) is disabled qCDebug(KALARM_LOG) << "KAlarmApp::execAlarm:" << event.id() << ": disabled"; if (reschedule) rescheduleAlarm(event, alarm, true); return nullptr; } void* result = (void*)1; event.setArchive(); switch (alarm.action()) { case KAAlarm::COMMAND: if (!event.commandDisplay()) { // execCommandAlarm() will error if the user is not authorised // to run shell commands. result = execCommandAlarm(event, alarm); if (reschedule) rescheduleAlarm(event, alarm, true); break; } Q_FALLTHROUGH(); // fall through to MESSAGE case KAAlarm::MESSAGE: case KAAlarm::FILE: { // Display a message, file or command output, provided that the same event // isn't already being displayed MessageWin* win = MessageWin::findEvent(EventId(event)); // Find if we're changing a reminder message to the real message const bool reminder = (alarm.type() & KAAlarm::REMINDER_ALARM); const bool replaceReminder = !reminder && win && (win->alarmType() & KAAlarm::REMINDER_ALARM); if (!reminder && (!event.deferred() || (event.extraActionOptions() & KAEvent::ExecPreActOnDeferral)) && (replaceReminder || !win) && !noPreAction && !event.preAction().isEmpty()) { // It's not a reminder alarm, and it's not a deferred alarm unless the // pre-alarm action applies to deferred alarms, and there is no message // window (other than a reminder window) currently displayed for this // alarm, and we need to execute a command before displaying the new window. // // NOTE: The pre-action is not executed for a recurring alarm if an // alarm message window for a previous occurrence is still visible. // Check whether the command is already being executed for this alarm. for (const ProcData* pd : qAsConst(mCommandProcesses)) { if (pd->event->id() == event.id() && (pd->flags & ProcData::PRE_ACTION)) { qCDebug(KALARM_LOG) << "KAlarmApp::execAlarm: Already executing pre-DISPLAY command"; return pd->process; // already executing - don't duplicate the action } } // doShellCommand() will error if the user is not authorised to run // shell commands. const QString command = event.preAction(); qCDebug(KALARM_LOG) << "KAlarmApp::execAlarm: Pre-DISPLAY command:" << command; const int flags = (reschedule ? ProcData::RESCHEDULE : 0) | (allowDefer ? ProcData::ALLOW_DEFER : 0); if (doShellCommand(command, event, &alarm, (flags | ProcData::PRE_ACTION))) { AlarmCalendar::resources()->setAlarmPending(&event); return result; // display the message after the command completes } // Error executing command if (event.extraActionOptions() & KAEvent::CancelOnPreActError) { // Cancel the rest of the alarm execution qCDebug(KALARM_LOG) << "KAlarmApp::execAlarm:" << event.id() << ": pre-action failed: cancelled"; if (reschedule) rescheduleAlarm(event, alarm, true); return nullptr; } // Display the message even though it failed } if (!win) { // There isn't already a message for this event const int flags = (reschedule ? 0 : MessageWin::NO_RESCHEDULE) | (allowDefer ? 0 : MessageWin::NO_DEFER); (new MessageWin(&event, alarm, flags))->show(); } else if (replaceReminder) { // The caption needs to be changed from "Reminder" to "Message" win->cancelReminder(event, alarm); } else if (!win->hasDefer() && !alarm.repeatAtLogin()) { // It's a repeat-at-login message with no Defer button, // which has now reached its final trigger time and needs // to be replaced with a new message. win->showDefer(); win->showDateTime(event, alarm); } else { // Use the existing message window } if (win) { // Raise the existing message window and replay any sound win->repeat(alarm); // N.B. this reschedules the alarm } break; } case KAAlarm::EMAIL: { qCDebug(KALARM_LOG) << "KAlarmApp::execAlarm: EMAIL to:" << event.emailAddresses(QStringLiteral(",")); QStringList errmsgs; KAMail::JobData data(event, alarm, reschedule, (reschedule || allowDefer)); data.queued = true; int ans = KAMail::send(data, errmsgs); if (ans) { // The email has either been sent or failed - not queued if (ans < 0) result = nullptr; // failure data.queued = false; emailSent(data, errmsgs, (ans > 0)); } else { result = (void*)-1; // email has been queued } if (reschedule) rescheduleAlarm(event, alarm, true); break; } case KAAlarm::AUDIO: { // Play the sound, provided that the same event // isn't already playing MessageWin* win = MessageWin::findEvent(EventId(event)); if (!win) { // There isn't already a message for this event. const int flags = (reschedule ? 0 : MessageWin::NO_RESCHEDULE) | MessageWin::ALWAYS_HIDE; win = new MessageWin(&event, alarm, flags); } else { // There's an existing message window: replay the sound win->repeat(alarm); // N.B. this reschedules the alarm } return win; } default: return nullptr; } return result; } /****************************************************************************** * Called when sending an email has completed. */ void KAlarmApp::emailSent(KAMail::JobData& data, const QStringList& errmsgs, bool copyerr) { if (!errmsgs.isEmpty()) { // Some error occurred, although the email may have been sent successfully if (errmsgs.count() > 1) qCDebug(KALARM_LOG) << "KAlarmApp::emailSent:" << (copyerr ? "Copy error:" : "Failed:") << errmsgs[1]; MessageWin::showError(data.event, data.alarm.dateTime(), errmsgs); } else if (data.queued) Q_EMIT execAlarmSuccess(); } /****************************************************************************** * Execute the command specified in a command alarm. * To connect to the output ready signals of the process, specify a slot to be * called by supplying 'receiver' and 'slot' parameters. */ ShellProcess* KAlarmApp::execCommandAlarm(const KAEvent& event, const KAAlarm& alarm, const QObject* receiver, const char* slot) { // doShellCommand() will error if the user is not authorised to run // shell commands. const int flags = (event.commandXterm() ? ProcData::EXEC_IN_XTERM : 0) | (event.commandDisplay() ? ProcData::DISP_OUTPUT : 0); const QString command = event.cleanText(); if (event.commandScript()) { // Store the command script in a temporary file for execution qCDebug(KALARM_LOG) << "KAlarmApp::execCommandAlarm: Script"; const QString tmpfile = createTempScriptFile(command, false, event, alarm); if (tmpfile.isEmpty()) { setEventCommandError(event, KAEvent::CMD_ERROR); return nullptr; } return doShellCommand(tmpfile, event, &alarm, (flags | ProcData::TEMP_FILE), receiver, slot); } else { qCDebug(KALARM_LOG) << "KAlarmApp::execCommandAlarm:" << command; return doShellCommand(command, event, &alarm, flags, receiver, slot); } } /****************************************************************************** * Execute a shell command line specified by an alarm. * If the PRE_ACTION bit of 'flags' is set, the alarm will be executed via * execAlarm() once the command completes, the execAlarm() parameters being * derived from the remaining bits in 'flags'. * 'flags' must contain the bit PRE_ACTION or POST_ACTION if and only if it is * a pre- or post-alarm action respectively. * To connect to the output ready signals of the process, specify a slot to be * called by supplying 'receiver' and 'slot' parameters. * * Note that if shell access is not authorised, the attempt to run the command * will be errored. */ ShellProcess* KAlarmApp::doShellCommand(const QString& command, const KAEvent& event, const KAAlarm* alarm, int flags, const QObject* receiver, const char* slot) { qCDebug(KALARM_LOG) << "KAlarmApp::doShellCommand:" << command << "," << event.id(); QIODevice::OpenMode mode = QIODevice::WriteOnly; QString cmd; QString tmpXtermFile; if (flags & ProcData::EXEC_IN_XTERM) { // Execute the command in a terminal window. cmd = composeXTermCommand(command, event, alarm, flags, tmpXtermFile); if (cmd.isEmpty()) { qCWarning(KALARM_LOG) << "KAlarmApp::doShellCommand: Command failed (no terminal selected)"; const QStringList errors{i18nc("@info", "Failed to execute command\n(no terminal selected for command alarms)")}; commandErrorMsg(nullptr, event, alarm, flags, errors); return nullptr; } } else { cmd = command; mode = QIODevice::ReadWrite; } ProcData* pd = nullptr; ShellProcess* proc = nullptr; if (!cmd.isEmpty()) { // Use ShellProcess, which automatically checks whether the user is // authorised to run shell commands. proc = new ShellProcess(cmd); proc->setEnv(QStringLiteral("KALARM_UID"), event.id(), true); proc->setOutputChannelMode(KProcess::MergedChannels); // combine stdout & stderr connect(proc, &ShellProcess::shellExited, this, &KAlarmApp::slotCommandExited); if ((flags & ProcData::DISP_OUTPUT) && receiver && slot) { connect(proc, SIGNAL(receivedStdout(ShellProcess*)), receiver, slot); connect(proc, SIGNAL(receivedStderr(ShellProcess*)), receiver, slot); } if (mode == QIODevice::ReadWrite && !event.logFile().isEmpty()) { // Output is to be appended to a log file. // Set up a logging process to write the command's output to. QString heading; if (alarm && alarm->dateTime().isValid()) { const QString dateTime = alarm->dateTime().formatLocale(); heading = QStringLiteral("\n******* KAlarm %1 *******\n").arg(dateTime); } else heading = QStringLiteral("\n******* KAlarm *******\n"); QFile logfile(event.logFile()); if (logfile.open(QIODevice::Append | QIODevice::Text)) { QTextStream out(&logfile); out << heading; logfile.close(); } proc->setStandardOutputFile(event.logFile(), QIODevice::Append); } pd = new ProcData(proc, new KAEvent(event), (alarm ? new KAAlarm(*alarm) : nullptr), flags); if (flags & ProcData::TEMP_FILE) pd->tempFiles += command; if (!tmpXtermFile.isEmpty()) pd->tempFiles += tmpXtermFile; mCommandProcesses.append(pd); if (proc->start(mode)) return proc; } // Error executing command - report it qCWarning(KALARM_LOG) << "KAlarmApp::doShellCommand: Command failed to start"; commandErrorMsg(proc, event, alarm, flags); if (pd) { mCommandProcesses.removeAt(mCommandProcesses.indexOf(pd)); delete pd; } return nullptr; } /****************************************************************************** * Compose a command line to execute the given command in a terminal window. * 'tempScriptFile' receives the name of a temporary script file which is * invoked by the command line, if applicable. * Reply = command line, or empty string if error. */ QString KAlarmApp::composeXTermCommand(const QString& command, const KAEvent& event, const KAAlarm* alarm, int flags, QString& tempScriptFile) const { qCDebug(KALARM_LOG) << "KAlarmApp::composeXTermCommand:" << command << "," << event.id(); tempScriptFile.clear(); QString cmd = Preferences::cmdXTermCommand(); if (cmd.isEmpty()) return QString(); // no terminal application is configured cmd.replace(QLatin1String("%t"), KAboutData::applicationData().displayName()); // set the terminal window title if (cmd.indexOf(QLatin1String("%C")) >= 0) { // Execute the command from a temporary script file if (flags & ProcData::TEMP_FILE) cmd.replace(QLatin1String("%C"), command); // the command is already calling a temporary file else { tempScriptFile = createTempScriptFile(command, true, event, *alarm); if (tempScriptFile.isEmpty()) return QString(); cmd.replace(QLatin1String("%C"), tempScriptFile); // %C indicates where to insert the command } } else if (cmd.indexOf(QLatin1String("%W")) >= 0) { // Execute the command from a temporary script file, // with a sleep after the command is executed tempScriptFile = createTempScriptFile(command + QLatin1String("\nsleep 86400\n"), true, event, *alarm); if (tempScriptFile.isEmpty()) return QString(); cmd.replace(QLatin1String("%W"), tempScriptFile); // %w indicates where to insert the command } else if (cmd.indexOf(QLatin1String("%w")) >= 0) { // Append a sleep to the command. // Quote the command in case it contains characters such as [>|;]. const QString exec = KShell::quoteArg(command + QLatin1String("; sleep 86400")); cmd.replace(QLatin1String("%w"), exec); // %w indicates where to insert the command string } else { // Set the command to execute. // Put it in quotes in case it contains characters such as [>|;]. const QString exec = KShell::quoteArg(command); if (cmd.indexOf(QLatin1String("%c")) >= 0) cmd.replace(QLatin1String("%c"), exec); // %c indicates where to insert the command string else cmd.append(exec); // otherwise, simply append the command string } return cmd; } /****************************************************************************** * Create a temporary script file containing the specified command string. * Reply = path of temporary file, or null string if error. */ QString KAlarmApp::createTempScriptFile(const QString& command, bool insertShell, const KAEvent& event, const KAAlarm& alarm) const { QTemporaryFile tmpFile; tmpFile.setAutoRemove(false); // don't delete file when it is destructed if (!tmpFile.open()) qCCritical(KALARM_LOG) << "Unable to create a temporary script file"; else { tmpFile.setPermissions(QFile::ReadUser | QFile::WriteUser | QFile::ExeUser); QTextStream stream(&tmpFile); if (insertShell) stream << "#!" << ShellProcess::shellPath() << "\n"; stream << command; stream.flush(); if (tmpFile.error() != QFile::NoError) qCCritical(KALARM_LOG) << "Error" << tmpFile.errorString() << " writing to temporary script file"; else return tmpFile.fileName(); } const QStringList errmsgs(i18nc("@info", "Error creating temporary script file")); MessageWin::showError(event, alarm.dateTime(), errmsgs, QStringLiteral("Script")); return QString(); } /****************************************************************************** * Called when a command alarm's execution completes. */ void KAlarmApp::slotCommandExited(ShellProcess* proc) { qCDebug(KALARM_LOG) << "KAlarmApp::slotCommandExited"; // Find this command in the command list for (int i = 0, end = mCommandProcesses.count(); i < end; ++i) { ProcData* pd = mCommandProcesses.at(i); if (pd->process == proc) { // Found the command. Check its exit status. bool executeAlarm = pd->preAction(); const ShellProcess::Status status = proc->status(); if (status == ShellProcess::SUCCESS && !proc->exitCode()) { qCDebug(KALARM_LOG) << "KAlarmApp::slotCommandExited:" << pd->event->id() << ": SUCCESS"; clearEventCommandError(*pd->event, pd->preAction() ? KAEvent::CMD_ERROR_PRE : pd->postAction() ? KAEvent::CMD_ERROR_POST : KAEvent::CMD_ERROR); } else { QString errmsg = proc->errorMessage(); if (status == ShellProcess::SUCCESS || status == ShellProcess::NOT_FOUND) qCWarning(KALARM_LOG) << "KAlarmApp::slotCommandExited:" << pd->event->id() << ":" << errmsg << "exit status =" << status << ", code =" << proc->exitCode(); else qCWarning(KALARM_LOG) << "KAlarmApp::slotCommandExited:" << pd->event->id() << ":" << errmsg << "exit status =" << status; if (pd->messageBoxParent) { // Close the existing informational KMessageBox for this process const QList dialogs = pd->messageBoxParent->findChildren(); if (!dialogs.isEmpty()) delete dialogs[0]; setEventCommandError(*pd->event, pd->preAction() ? KAEvent::CMD_ERROR_PRE : pd->postAction() ? KAEvent::CMD_ERROR_POST : KAEvent::CMD_ERROR); if (!pd->tempFile()) { errmsg += QLatin1Char('\n'); errmsg += proc->command(); } KAMessageBox::error(pd->messageBoxParent, errmsg); } else commandErrorMsg(proc, *pd->event, pd->alarm, pd->flags); if (executeAlarm && (pd->event->extraActionOptions() & KAEvent::CancelOnPreActError)) { qCDebug(KALARM_LOG) << "KAlarmApp::slotCommandExited:" << pd->event->id() << ": pre-action failed: cancelled"; if (pd->reschedule()) rescheduleAlarm(*pd->event, *pd->alarm, true); executeAlarm = false; } } if (pd->preAction()) AlarmCalendar::resources()->setAlarmPending(pd->event, false); if (executeAlarm) execAlarm(*pd->event, *pd->alarm, pd->reschedule(), pd->allowDefer(), true); mCommandProcesses.removeAt(i); delete pd; break; } } // If there are now no executing shell commands, quit if a quit was queued if (mPendingQuit && mCommandProcesses.isEmpty()) quitIf(mPendingQuitCode); } /****************************************************************************** * Output an error message for a shell command, and record the alarm's error status. */ void KAlarmApp::commandErrorMsg(const ShellProcess* proc, const KAEvent& event, const KAAlarm* alarm, int flags, const QStringList& errors) { KAEvent::CmdErrType cmderr; QString dontShowAgain; QStringList errmsgs = errors; if (flags & ProcData::PRE_ACTION) { if (event.extraActionOptions() & KAEvent::DontShowPreActError) return; // don't notify user of any errors for the alarm errmsgs += i18nc("@info", "Pre-alarm action:"); dontShowAgain = QStringLiteral("Pre"); cmderr = KAEvent::CMD_ERROR_PRE; } else if (flags & ProcData::POST_ACTION) { errmsgs += i18nc("@info", "Post-alarm action:"); dontShowAgain = QStringLiteral("Post"); cmderr = (event.commandError() == KAEvent::CMD_ERROR_PRE) ? KAEvent::CMD_ERROR_PRE_POST : KAEvent::CMD_ERROR_POST; } else { dontShowAgain = QStringLiteral("Exec"); cmderr = KAEvent::CMD_ERROR; } // Record the alarm's error status setEventCommandError(event, cmderr); // Display an error message if (proc) { errmsgs += proc->errorMessage(); if (!(flags & ProcData::TEMP_FILE)) errmsgs += proc->command(); dontShowAgain += QString::number(proc->status()); } MessageWin::showError(event, (alarm ? alarm->dateTime() : DateTime()), errmsgs, dontShowAgain); } /****************************************************************************** * Notes that an informational KMessageBox is displayed for this process. */ void KAlarmApp::commandMessage(ShellProcess* proc, QWidget* parent) { // Find this command in the command list for (ProcData* pd : qAsConst(mCommandProcesses)) { if (pd->process == proc) { pd->messageBoxParent = parent; break; } } } /****************************************************************************** * If this is the first time through, open the calendar file, and start * processing the execution queue. */ bool KAlarmApp::initCheck(bool calendarOnly, bool waitForCollection, ResourceId resourceId) { static bool firstTime = true; if (firstTime) qCDebug(KALARM_LOG) << "KAlarmApp::initCheck: first time"; if (initialise() || firstTime) { /* Need to open the display calendar now, since otherwise if display * alarms are immediately due, they will often be processed while * MessageWin::redisplayAlarms() is executing open() (but before open() * completes), which causes problems!! */ AlarmCalendar::displayCalendar()->open(); if (!AlarmCalendar::resources()->open()) return false; } if (firstTime) { setArchivePurgeDays(); // Warn the user if there are no writable active alarm calendars checkWritableCalendar(); firstTime = false; } if (!calendarOnly) startProcessQueue(); // start processing the execution queue if (waitForCollection) { // Wait for one or all calendar resources to be populated if (!waitUntilPopulated(resourceId, AKONADI_TIMEOUT)) return false; } return true; } /****************************************************************************** * Wait for one or all enabled resources to be populated. * Reply = true if successful. */ bool KAlarmApp::waitUntilPopulated(ResourceId id, int timeout) { qCDebug(KALARM_LOG) << "KAlarmApp::waitUntilPopulated" << id; const Resource res = Resources::resource(id); if ((id < 0 && !Resources::allPopulated()) || (id >= 0 && !res.isPopulated())) { // Use AutoQPointer to guard against crash on application exit while // the event loop is still running. It prevents double deletion (both // on deletion of parent, and on return from this function). AutoQPointer loop = new QEventLoop(AlarmListModel::all()); //TODO: The choice of parent object for QEventLoop can prevent EntityTreeModel signals // from activating connected slots in AkonadiModel, which prevents resources from // being informed that collections have loaded. Need to find a better parent // object - Qt item models seem to work, but what else? // These don't work: Resources::instance(), qApp(), theApp(), MainWindow::mainMainWindow(), AlarmCalendar::resources(), QStandardItemModel. // These do work: CollectionControlModel::instance(), AlarmListModel::all(). if (id < 0) connect(Resources::instance(), &Resources::resourcesPopulated, loop, &QEventLoop::quit); else connect(Resources::instance(), &Resources::resourcePopulated, [loop, &id](Resource& r) { if (r.id() == id) loop->quit(); }); if (timeout > 0) QTimer::singleShot(timeout * 1000, loop, &QEventLoop::quit); loop->exec(); } return (id < 0) ? Resources::allPopulated() : res.isPopulated(); } /****************************************************************************** * Called when an audio thread starts or stops. */ void KAlarmApp::notifyAudioPlaying(bool playing) { Q_EMIT audioPlaying(playing); } /****************************************************************************** * Stop audio play. */ void KAlarmApp::stopAudio() { MessageWin::stopAudio(); } /****************************************************************************** * Set the command error for the specified alarm. */ void KAlarmApp::setEventCommandError(const KAEvent& event, KAEvent::CmdErrType err) const { ProcData* pd = findCommandProcess(event.id()); if (pd && pd->eventDeleted) return; // the alarm has been deleted, so can't set error status if (err == KAEvent::CMD_ERROR_POST && event.commandError() == KAEvent::CMD_ERROR_PRE) err = KAEvent::CMD_ERROR_PRE_POST; event.setCommandError(err); KAEvent* ev = AlarmCalendar::resources()->event(EventId(event)); if (ev && ev->commandError() != err) ev->setCommandError(err); Resource resource = Resources::resourceForEvent(event.id()); resource.handleCommandErrorChange(event); } /****************************************************************************** * Clear the command error for the specified alarm. */ void KAlarmApp::clearEventCommandError(const KAEvent& event, KAEvent::CmdErrType err) const { ProcData* pd = findCommandProcess(event.id()); if (pd && pd->eventDeleted) return; // the alarm has been deleted, so can't set error status KAEvent::CmdErrType newerr = static_cast(event.commandError() & ~err); event.setCommandError(newerr); KAEvent* ev = AlarmCalendar::resources()->event(EventId(event)); if (ev) { newerr = static_cast(ev->commandError() & ~err); ev->setCommandError(newerr); } Resource resource = Resources::resourceForEvent(event.id()); resource.handleCommandErrorChange(event); } /****************************************************************************** * Find the currently executing command process for an event ID, if any. */ KAlarmApp::ProcData* KAlarmApp::findCommandProcess(const QString& eventId) const { for (ProcData* pd : qAsConst(mCommandProcesses)) { if (pd->event->id() == eventId) return pd; } return nullptr; } KAlarmApp::ProcData::ProcData(ShellProcess* p, KAEvent* e, KAAlarm* a, int f) : process(p) , event(e) , alarm(a) , messageBoxParent(nullptr) , flags(f) , eventDeleted(false) { } KAlarmApp::ProcData::~ProcData() { while (!tempFiles.isEmpty()) { // Delete the temporary file called by the XTerm command QFile f(tempFiles.first()); f.remove(); tempFiles.removeFirst(); } delete process; delete event; delete alarm; } // vim: et sw=4: diff --git a/src/kamail.cpp b/src/kamail.cpp index a6107f90..7cf0b63c 100644 --- a/src/kamail.cpp +++ b/src/kamail.cpp @@ -1,967 +1,967 @@ /* * kamail.cpp - email functions * Program: kalarm * Copyright © 2002-2019 David Jarvie * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kamail.h" #include "kalarm.h" #include "functions.h" #include "kalarmapp.h" #include "mainwindow.h" #include "messagebox.h" #include "preferences.h" #include "kalarm_debug.h" #include "kmailinterface.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 #include #include #include static const QLatin1String KMAIL_DBUS_SERVICE("org.kde.kmail"); //static const QLatin1String KMAIL_DBUS_PATH("/KMail"); namespace HeaderParsing { bool parseAddress( const char* & scursor, const char * const send, KMime::Types::Address & result, bool isCRLF=false ); } static void initHeaders(KMime::Message&, KAMail::JobData&); static KMime::Types::Mailbox::List parseAddresses(const QString& text, QString& invalidItem); static QString extractEmailAndNormalize(const QString& emailAddress); static QStringList extractEmailsAndNormalize(const QString& emailAddresses); static QByteArray autoDetectCharset(const QString& text); static const QTextCodec* codecForName(const QByteArray& str); QString KAMail::i18n_NeedFromEmailAddress() { return i18nc("@info", "A 'From' email address must be configured in order to execute email alarms."); } QString KAMail::i18n_sent_mail() { return i18nc("@info KMail folder name: this should be translated the same as in kmail", "sent-mail"); } KAMail* KAMail::mInstance = nullptr; // used only to enable signals/slots to work QQueue KAMail::mJobs; QQueue KAMail::mJobData; KAMail* KAMail::instance() { if (!mInstance) mInstance = new KAMail(); return mInstance; } /****************************************************************************** * Send the email message specified in an event. * Reply = 1 if the message was sent - 'errmsgs' may contain copy error messages. * = 0 if the message is queued for sending. * = -1 if the message was not sent - 'errmsgs' contains the error messages. */ int KAMail::send(JobData& jobdata, QStringList& errmsgs) { QString err; KIdentityManagement::Identity identity; jobdata.from = Preferences::emailAddress(); if (jobdata.event.emailFromId() && Preferences::emailFrom() == Preferences::MAIL_FROM_KMAIL) { identity = Identities::identityManager()->identityForUoid(jobdata.event.emailFromId()); if (identity.isNull()) { qCCritical(KALARM_LOG) << "KAMail::send: Identity" << jobdata.event.emailFromId() << "not found"; errmsgs = errors(xi18nc("@info", "Invalid 'From' email address.Email identity %1 not found", jobdata.event.emailFromId())); return -1; } if (identity.primaryEmailAddress().isEmpty()) { qCCritical(KALARM_LOG) << "KAMail::send: Identity" << identity.identityName() << "uoid" << identity.uoid() << ": no email address"; errmsgs = errors(xi18nc("@info", "Invalid 'From' email address.Email identity %1 has no email address", identity.identityName())); return -1; } jobdata.from = identity.fullEmailAddr(); } if (jobdata.from.isEmpty()) { switch (Preferences::emailFrom()) { case Preferences::MAIL_FROM_KMAIL: errmsgs = errors(xi18nc("@info", "No 'From' email address is configured (no default email identity found)" "Please set it in KMail or in the KAlarm Configuration dialog.")); break; case Preferences::MAIL_FROM_SYS_SETTINGS: errmsgs = errors(xi18nc("@info", "No 'From' email address is configured." "Please set a default address in KMail or KDE System Settings or in the KAlarm Configuration dialog.")); break; case Preferences::MAIL_FROM_ADDR: default: errmsgs = errors(xi18nc("@info", "No 'From' email address is configured." "Please set it in the KAlarm Configuration dialog.")); break; } return -1; } jobdata.bcc = (jobdata.event.emailBcc() ? Preferences::emailBccAddress() : QString()); qCDebug(KALARM_LOG) << "KAMail::send: To:" << jobdata.event.emailAddresses(QStringLiteral(",")) << endl << "Subject:" << jobdata.event.emailSubject(); KMime::Message::Ptr message = KMime::Message::Ptr(new KMime::Message); MailTransport::TransportManager* manager = MailTransport::TransportManager::self(); MailTransport::Transport* transport = nullptr; if (Preferences::emailClient() == Preferences::sendmail) { qCDebug(KALARM_LOG) << "KAMail::send: Sending via sendmail"; const QStringList paths{QStringLiteral("/sbin"), QStringLiteral("/usr/sbin"), QStringLiteral("/usr/lib")}; QString command = QStandardPaths::findExecutable(QStringLiteral("sendmail"), paths); if (!command.isNull()) { command += QStringLiteral(" -f "); command += extractEmailAndNormalize(jobdata.from); command += QStringLiteral(" -oi -t "); initHeaders(*message, jobdata); } else { command = QStandardPaths::findExecutable(QStringLiteral("mail"), paths); if (command.isNull()) { qCCritical(KALARM_LOG) << "KAMail::send: sendmail not found"; errmsgs = errors(xi18nc("@info", "%1 not found", QStringLiteral("sendmail"))); // give up return -1; } command += QStringLiteral(" -s "); command += KShell::quoteArg(jobdata.event.emailSubject()); if (!jobdata.bcc.isEmpty()) { command += QStringLiteral(" -b "); command += extractEmailAndNormalize(jobdata.bcc); } command += QLatin1Char(' '); command += jobdata.event.emailPureAddresses(QStringLiteral(" ")); // locally provided, okay } // Add the body and attachments to the message. // (Sendmail requires attachments to have already been included in the message.) err = appendBodyAttachments(*message, jobdata); if (!err.isNull()) { qCCritical(KALARM_LOG) << "KAMail::send: Error compiling message:" << err; errmsgs = errors(err); return -1; } // Execute the send command FILE* fd = ::popen(command.toLocal8Bit().constData(), "w"); if (!fd) { qCCritical(KALARM_LOG) << "KAMail::send: Unable to open a pipe to " << command; errmsgs = errors(); return -1; } message->assemble(); QByteArray encoded = message->encodedContent(); fwrite(encoded.constData(), encoded.length(), 1, fd); pclose(fd); #ifdef KMAIL_SUPPORTED if (Preferences::emailCopyToKMail()) { // Create a copy of the sent email in KMail's 'sent-mail' folder, // or if there was a send error, in KMail's 'outbox' folder. err = addToKMailFolder(jobdata, "sent-mail", true); if (!err.isNull()) errmsgs += errors(err, COPY_ERROR); // not a fatal error - continue } #endif if (jobdata.allowNotify) notifyQueued(jobdata.event); return 1; } else { qCDebug(KALARM_LOG) << "KAMail::send: Sending via KDE"; const int transportId = identity.transport().isEmpty() ? -1 : identity.transport().toInt(); transport = manager->transportById(transportId, true); if (!transport) { qCCritical(KALARM_LOG) << "KAMail::send: No mail transport found for identity" << identity.identityName() << "uoid" << identity.uoid(); errmsgs = errors(xi18nc("@info", "No mail transport configured for email identity %1", identity.identityName())); return -1; } qCDebug(KALARM_LOG) << "KAMail::send: Using transport" << transport->name() << ", id=" << transport->id(); initHeaders(*message, jobdata); err = appendBodyAttachments(*message, jobdata); if (!err.isNull()) { qCCritical(KALARM_LOG) << "KAMail::send: Error compiling message:" << err; errmsgs = errors(err); return -1; } MailTransport::MessageQueueJob* mailjob = new MailTransport::MessageQueueJob(qApp); mailjob->setMessage(message); mailjob->transportAttribute().setTransportId(transport->id()); // MessageQueueJob email addresses must be pure, i.e. without display name. Note // that display names are included in the actual headers set up by initHeaders(). mailjob->addressAttribute().setFrom(extractEmailAndNormalize(jobdata.from)); mailjob->addressAttribute().setTo(extractEmailsAndNormalize(jobdata.event.emailAddresses(QStringLiteral(",")))); if (!jobdata.bcc.isEmpty()) mailjob->addressAttribute().setBcc(extractEmailsAndNormalize(jobdata.bcc)); MailTransport::SentBehaviourAttribute::SentBehaviour sentAction = (Preferences::emailClient() == Preferences::kmail || Preferences::emailCopyToKMail()) ? MailTransport::SentBehaviourAttribute::MoveToDefaultSentCollection : MailTransport::SentBehaviourAttribute::Delete; mailjob->sentBehaviourAttribute().setSentBehaviour(sentAction); mJobs.enqueue(mailjob); mJobData.enqueue(jobdata); if (mJobs.count() == 1) { // There are no jobs already active or queued, so send now connect(mailjob, &KJob::result, instance(), &KAMail::slotEmailSent); mailjob->start(); } } return 0; } /****************************************************************************** * Called when sending an email is complete. */ void KAMail::slotEmailSent(KJob* job) { bool copyerr = false; QStringList errmsgs; if (job->error()) { qCCritical(KALARM_LOG) << "KAMail::slotEmailSent: Failed:" << job->errorString(); errmsgs = errors(job->errorString(), SEND_ERROR); } JobData jobdata; if (mJobs.isEmpty() || mJobData.isEmpty() || job != mJobs.head()) { // The queue has been corrupted, so we can't locate the job's data qCCritical(KALARM_LOG) << "KAMail::slotEmailSent: Wrong job at head of queue: wiping queue"; mJobs.clear(); mJobData.clear(); if (!errmsgs.isEmpty()) theApp()->emailSent(jobdata, errmsgs); errmsgs.clear(); errmsgs += i18nc("@info", "Emails may not have been sent"); errmsgs += i18nc("@info", "Program error"); theApp()->emailSent(jobdata, errmsgs); return; } mJobs.dequeue(); jobdata = mJobData.dequeue(); if (jobdata.allowNotify) notifyQueued(jobdata.event); theApp()->emailSent(jobdata, errmsgs, copyerr); if (!mJobs.isEmpty()) { // Send the next queued email connect(mJobs.head(), &KJob::result, instance(), &KAMail::slotEmailSent); mJobs.head()->start(); } } /****************************************************************************** * Create the headers part of the email. */ void initHeaders(KMime::Message& message, KAMail::JobData& data) { KMime::Headers::Date* date = new KMime::Headers::Date; date->setDateTime(KADateTime::currentDateTime(Preferences::timeSpec()).qDateTime()); message.setHeader(date); KMime::Headers::From* from = new KMime::Headers::From; from->fromUnicodeString(data.from, autoDetectCharset(data.from)); message.setHeader(from); KMime::Headers::To* to = new KMime::Headers::To; const KCalendarCore::Person::List toList = data.event.emailAddressees(); for (const KCalendarCore::Person& who : toList) to->addAddress(who.email().toLatin1(), who.name()); message.setHeader(to); if (!data.bcc.isEmpty()) { KMime::Headers::Bcc* bcc = new KMime::Headers::Bcc; bcc->fromUnicodeString(data.bcc, autoDetectCharset(data.bcc)); message.setHeader(bcc); } KMime::Headers::Subject* subject = new KMime::Headers::Subject; const QString str = data.event.emailSubject(); subject->fromUnicodeString(str, autoDetectCharset(str)); message.setHeader(subject); KMime::Headers::UserAgent* agent = new KMime::Headers::UserAgent; agent->fromUnicodeString(KAboutData::applicationData().displayName() + QLatin1String("/" KALARM_VERSION), "us-ascii"); message.setHeader(agent); KMime::Headers::MessageID* id = new KMime::Headers::MessageID; id->generate(data.from.mid(data.from.indexOf(QLatin1Char('@')) + 1).toLatin1()); message.setHeader(id); } /****************************************************************************** * Append the body and attachments to the email text. * Reply = reason for error * = empty string if successful. */ QString KAMail::appendBodyAttachments(KMime::Message& message, JobData& data) { const QStringList attachments = data.event.emailAttachments(); if (!attachments.count()) { // There are no attachments, so simply append the message body message.contentType()->setMimeType("text/plain"); message.contentType()->setCharset("utf-8"); message.fromUnicodeString(data.event.message()); auto encodings = KMime::encodingsForData(message.body()); encodings.removeAll(KMime::Headers::CE8Bit); // not handled by KMime message.contentTransferEncoding()->setEncoding(encodings.at(0)); message.assemble(); } else { // There are attachments, so the message must be in MIME format message.contentType()->setMimeType("multipart/mixed"); message.contentType()->setBoundary(KMime::multiPartBoundary()); if (!data.event.message().isEmpty()) { // There is a message body KMime::Content* content = new KMime::Content(); content->contentType()->setMimeType("text/plain"); content->contentType()->setCharset("utf-8"); content->fromUnicodeString(data.event.message()); auto encodings = KMime::encodingsForData(content->body()); encodings.removeAll(KMime::Headers::CE8Bit); // not handled by KMime content->contentTransferEncoding()->setEncoding(encodings.at(0)); content->assemble(); message.addContent(content); } // Append each attachment in turn for (const QString& att : attachments) { const QString attachment = QString::fromLatin1(att.toLocal8Bit()); const QUrl url = QUrl::fromUserInput(attachment, QString(), QUrl::AssumeLocalFile); const QString attachError = xi18nc("@info", "Error attaching file: %1", attachment); QByteArray contents; bool atterror = false; if (!url.isLocalFile()) { KIO::UDSEntry uds; auto statJob = KIO::stat(url, KIO::StatJob::SourceSide, 2); KJobWidgets::setWindow(statJob, MainWindow::mainMainWindow()); if (!statJob->exec()) { qCCritical(KALARM_LOG) << "KAMail::appendBodyAttachments: Not found:" << attachment; return xi18nc("@info", "Attachment not found: %1", attachment); } KFileItem fi(statJob->statResult(), url); if (fi.isDir() || !fi.isReadable()) { qCCritical(KALARM_LOG) << "KAMail::appendBodyAttachments: Not file/not readable:" << attachment; return attachError; } // Read the file contents auto downloadJob = KIO::storedGet(url); KJobWidgets::setWindow(downloadJob, MainWindow::mainMainWindow()); if (!downloadJob->exec()) { qCCritical(KALARM_LOG) << "KAMail::appendBodyAttachments: Load failure:" << attachment; return attachError; } contents = downloadJob->data(); if (static_cast(contents.size()) < fi.size()) { qCDebug(KALARM_LOG) << "KAMail::appendBodyAttachments: Read error:" << attachment; atterror = true; } } else { QFile f(url.toLocalFile()); if (!f.open(QIODevice::ReadOnly)) { qCCritical(KALARM_LOG) << "KAMail::appendBodyAttachments: Load failure:" << attachment; return attachError; } contents = f.readAll(); } QByteArray coded = KCodecs::base64Encode(contents); KMime::Content* content = new KMime::Content(); content->setBody(coded + "\n\n"); // Set the content type QMimeDatabase mimeDb; QString typeName = mimeDb.mimeTypeForUrl(url).name(); KMime::Headers::ContentType* ctype = new KMime::Headers::ContentType; ctype->fromUnicodeString(typeName, autoDetectCharset(typeName)); ctype->setName(attachment, "local"); content->setHeader(ctype); // Set the encoding KMime::Headers::ContentTransferEncoding* cte = new KMime::Headers::ContentTransferEncoding; cte->setEncoding(KMime::Headers::CEbase64); cte->setDecoded(false); content->setHeader(cte); content->assemble(); message.addContent(content); if (atterror) return attachError; } message.assemble(); } return QString(); } /****************************************************************************** * If any of the destination email addresses are non-local, display a * notification message saying that an email has been queued for sending. */ void KAMail::notifyQueued(const KAEvent& event) { KMime::Types::Address addr; const QString localhost = QStringLiteral("localhost"); const QString hostname = QHostInfo::localHostName(); const KCalendarCore::Person::List addresses = event.emailAddressees(); for (const KCalendarCore::Person& address : addresses) { const QByteArray email = address.email().toLocal8Bit(); const char* em = email.constData(); if (!email.isEmpty() && HeaderParsing::parseAddress(em, em + email.length(), addr)) { const QString domain = addr.mailboxList.at(0).addrSpec().domain; if (!domain.isEmpty() && domain != localhost && domain != hostname) { KAMessageBox::information(MainWindow::mainMainWindow(), i18nc("@info", "An email has been queued to be sent"), QString(), Preferences::EMAIL_QUEUED_NOTIFY); return; } } } } /****************************************************************************** * Fetch the user's email address configured in KMail or KDE System Settings. */ QString KAMail::controlCentreAddress() { KEMailSettings e; return e.getSetting(KEMailSettings::EmailAddress); } /****************************************************************************** * Parse a list of email addresses, optionally containing display names, * entered by the user. * Reply = the invalid item if error, else empty string. */ QString KAMail::convertAddresses(const QString& items, KCalendarCore::Person::List& list) { list.clear(); QString invalidItem; const KMime::Types::Mailbox::List mailboxes = parseAddresses(items, invalidItem); if (!invalidItem.isEmpty()) return invalidItem; for (const KMime::Types::Mailbox& mailbox : mailboxes) { const KCalendarCore::Person person(mailbox.name(), mailbox.addrSpec().asString()); list += person; } return QString(); } /****************************************************************************** * Check the validity of an email address. * Because internal email addresses don't have to abide by the usual internet * email address rules, only some basic checks are made. * Reply = 1 if alright, 0 if empty, -1 if error. */ int KAMail::checkAddress(QString& address) { address = address.trimmed(); // Check that there are no list separator characters present if (address.indexOf(QLatin1Char(',')) >= 0 || address.indexOf(QLatin1Char(';')) >= 0) return -1; const int n = address.length(); if (!n) return 0; int start = 0; int end = n - 1; if (address.at(end) == QLatin1Char('>')) { // The email address is in <...> if ((start = address.indexOf(QLatin1Char('<'))) < 0) return -1; ++start; --end; } const int i = address.indexOf(QLatin1Char('@'), start); if (i >= 0) { if (i == start || i == end) // check @ isn't the first or last character // || address.indexOf(QLatin1Char('@'), i + 1) >= 0) // check for multiple @ characters return -1; } /* else { // Allow the @ character to be missing if it's a local user if (!getpwnam(address.mid(start, end - start + 1).toLocal8Bit())) return false; } for (int i = start; i <= end; ++i) { char ch = address.at(i).toLatin1(); if (ch == '.' || ch == '@' || ch == '-' || ch == '_' || (ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9')) continue; return false; }*/ return 1; } /****************************************************************************** * Convert a comma or semicolon delimited list of attachments into a * QStringList. The items are checked for validity. * Reply = the invalid item if error, else empty string. */ QString KAMail::convertAttachments(const QString& items, QStringList& list) { list.clear(); int length = items.length(); for (int next = 0; next < length; ) { // Find the first delimiter character (, or ;) int i = items.indexOf(QLatin1Char(','), next); if (i < 0) i = items.length(); int sc = items.indexOf(QLatin1Char(';'), next); if (sc < 0) sc = items.length(); if (sc < i) i = sc; QString item = items.mid(next, i - next).trimmed(); switch (checkAttachment(item)) { case 1: list += item; break; case 0: break; // empty attachment name case -1: default: return item; // error } next = i + 1; } return QString(); } /****************************************************************************** * Check for the existence of the attachment file. * If non-null, '*url' receives the QUrl of the attachment. * Reply = 1 if attachment exists * = 0 if null name * = -1 if doesn't exist. */ int KAMail::checkAttachment(QString& attachment, QUrl* url) { attachment = attachment.trimmed(); if (attachment.isEmpty()) { if (url) *url = QUrl(); return 0; } // Check that the file exists QUrl u = QUrl::fromUserInput(attachment, QString(), QUrl::AssumeLocalFile); u.setPath(QDir::cleanPath(u.path())); if (url) *url = u; return checkAttachment(u) ? 1 : -1; } /****************************************************************************** * Check for the existence of the attachment file. */ bool KAMail::checkAttachment(const QUrl& url) { auto statJob = KIO::stat(url); KJobWidgets::setWindow(statJob, MainWindow::mainMainWindow()); if (!statJob->exec()) return false; // doesn't exist KFileItem fi(statJob->statResult(), url); if (fi.isDir() || !fi.isReadable()) return false; return true; } /****************************************************************************** * Set the appropriate error messages for a given error string. */ QStringList KAMail::errors(const QString& err, ErrType prefix) { QString error1; switch (prefix) { case SEND_FAIL: error1 = i18nc("@info", "Failed to send email"); break; case SEND_ERROR: error1 = i18nc("@info", "Error sending email"); break; #ifdef KMAIL_SUPPORTED case COPY_ERROR: error1 = i18nc("@info", "Error copying sent email to KMail %1 folder", i18n_sent_mail()); break; #endif } if (err.isEmpty()) return QStringList(error1); return QStringList{QStringLiteral("%1:").arg(error1), err}; } /****************************************************************************** * Get the body of an email from KMail, given its serial number. */ QString KAMail::getMailBody(quint32 serialNumber) { //TODO: Need to use Akonadi instead const QList args{serialNumber, (int)0}; QDBusInterface iface(KMAIL_DBUS_SERVICE, QString(), QStringLiteral("KMailIface")); const QDBusReply reply = iface.callWithArgumentList(QDBus::Block, QStringLiteral("getDecodedBodyPart"), args); if (!reply.isValid()) { qCCritical(KALARM_LOG) << "KAMail::getMailBody: D-Bus call failed:" << reply.error().message(); return QString(); } return reply.value(); } /****************************************************************************** * Extract the pure addresses from given email addresses. */ QString extractEmailAndNormalize(const QString& emailAddress) { return KEmailAddress::extractEmailAddress(KEmailAddress::normalizeAddressesAndEncodeIdn(emailAddress)); } QStringList extractEmailsAndNormalize(const QString& emailAddresses) { const QStringList splitEmails(KEmailAddress::splitAddressList(emailAddresses)); QStringList normalizedEmail; for (const QString& email : splitEmails) { normalizedEmail << KEmailAddress::extractEmailAddress(KEmailAddress::normalizeAddressesAndEncodeIdn(email)); } return normalizedEmail; } //----------------------------------------------------------------------------- // Based on KMail KMMsgBase::autoDetectCharset(). QByteArray autoDetectCharset(const QString& text) { for (QByteArray encoding : {"us-ascii", "iso-8859-1", "locale", "utf-8"}) { if (encoding == "locale") encoding = QTextCodec::codecForLocale()->name().toLower(); if (text.isEmpty()) return encoding; if (encoding == "us-ascii") { if (KMime::isUsAscii(text)) return encoding; } else { const QTextCodec* codec = codecForName(encoding); if (!codec) qCDebug(KALARM_LOG) << "KAMail::autoDetectCharset: Something is wrong and I cannot get a codec. [" << encoding <<"]"; else { if (codec->canEncode(text)) return encoding; } } } return QByteArray(); } //----------------------------------------------------------------------------- // Based on KMail KMMsgBase::codecForName(). const QTextCodec* codecForName(const QByteArray& str) { if (str.isEmpty()) return nullptr; QByteArray codec = str.toLower(); return KCharsets::charsets()->codecForName(QLatin1String(codec)); } /****************************************************************************** * Parse a string containing multiple addresses, separated by comma or semicolon, * while retaining Unicode name parts. * Note that this only needs to parse strings input into KAlarm, so it only * needs to accept the common syntax for email addresses, not obsolete syntax. */ KMime::Types::Mailbox::List parseAddresses(const QString& text, QString& invalidItem) { KMime::Types::Mailbox::List list; int state = 0; int start = 0; // start of this item int endName = 0; // character after end of name int startAddr = 0; // start of address int endAddr = 0; // character after end of address char lastch = '\0'; bool ended = false; // found the end of the item for (int i = 0, count = text.length(); i <= count; ++i) { if (i == count) ended = true; else { const char ch = text[i].toLatin1(); switch (state) { case 0: // looking for start of item if (ch == ' ' || ch == '\t') continue; start = i; state = (ch == '"') ? 10 : 1; break; case 1: // looking for start of address, or end of item switch (ch) { case '<': startAddr = i + 1; state = 2; break; case ',': case ';': ended = true; break; case ' ': break; default: endName = i + 1; break; } break; case 2: // looking for '>' at end of address if (ch == '>') { endAddr = i; state = 3; } break; case 3: // looking for item separator if (ch == ',' || ch == ';') ended = true; else if (ch != ' ') { invalidItem = text.mid(start); return KMime::Types::Mailbox::List(); } break; case 10: // looking for closing quote if (ch == '"' && lastch != '\\') { ++start; // remove opening quote from name endName = i; state = 11; } lastch = ch; break; case 11: // looking for '<' if (ch == '<') { startAddr = i + 1; state = 2; } break; } } if (ended) { // Found the end of the item - add it to the list if (!startAddr) { startAddr = start; endAddr = endName; endName = 0; } const QString addr = text.mid(startAddr, endAddr - startAddr); KMime::Types::Mailbox mbox; mbox.fromUnicodeString(addr); if (mbox.address().isEmpty()) { invalidItem = text.mid(start, endAddr - start); return KMime::Types::Mailbox::List(); } if (endName) { int len = endName - start; QString name = text.mid(start, endName - start); if (name.at(0) == QLatin1Char('"') && name.at(len - 1) == QLatin1Char('"')) name = name.mid(1, len - 2); mbox.setName(name); } list.append(mbox); endName = startAddr = endAddr = 0; start = i + 1; state = 0; ended = false; } } return list; } /*============================================================================= = HeaderParsing : modified and additional functions. = The following functions are modified from, or additional to, those in = libkmime kmime_header_parsing.cpp. =============================================================================*/ namespace HeaderParsing { using namespace KMime; using namespace KMime::Types; using namespace KMime::HeaderParsing; /****************************************************************************** * New function. * Allow a local user name to be specified as an email address. */ bool parseUserName( const char* & scursor, const char * const send, QString & result, bool isCRLF ) { QString maybeLocalPart; QString tmp; if ( scursor != send ) { // first, eat any whitespace eatCFWS( scursor, send, isCRLF ); char ch = *scursor++; switch ( ch ) { case '.': // dot case '@': case '"': // quoted-string return false; default: // atom scursor--; // re-set scursor to point to ch again tmp.clear(); if ( parseAtom( scursor, send, result, false /* no 8bit */ ) ) { if (getpwnam(result.toLocal8Bit().constData())) return true; } return false; // parseAtom can only fail if the first char is non-atext. } } return false; } /****************************************************************************** * Modified function. * Allow a local user name to be specified as an email address, and reinstate * the original scursor on error return. */ bool parseAddress( const char* & scursor, const char * const send, Address & result, bool isCRLF ) { // address := mailbox / group eatCFWS( scursor, send, isCRLF ); if ( scursor == send ) return false; // first try if it's a single mailbox: Mailbox maybeMailbox; const char * oldscursor = scursor; if ( parseMailbox( scursor, send, maybeMailbox, isCRLF ) ) { // yes, it is: result.displayName.clear(); result.mailboxList.append( maybeMailbox ); return true; } scursor = oldscursor; // KAlarm: Allow a local user name to be specified // no, it's not a single mailbox. Try if it's a local user name: QString maybeUserName; if ( parseUserName( scursor, send, maybeUserName, isCRLF ) ) { // yes, it is: maybeMailbox.setName( QString() ); AddrSpec addrSpec; addrSpec.localPart = maybeUserName; addrSpec.domain.clear(); maybeMailbox.setAddress( addrSpec ); result.displayName.clear(); result.mailboxList.append( maybeMailbox ); return true; } scursor = oldscursor; Address maybeAddress; // no, it's not a single mailbox. Try if it's a group: if ( !parseGroup( scursor, send, maybeAddress, isCRLF ) ) { scursor = oldscursor; // KAlarm: reinstate original scursor on error return return false; } result = maybeAddress; return true; } } // namespace HeaderParsing // vim: et sw=4: diff --git a/src/lib/colourbutton.h b/src/lib/colourbutton.h index 9c465a3c..a9128a75 100644 --- a/src/lib/colourbutton.h +++ b/src/lib/colourbutton.h @@ -1,67 +1,67 @@ /* * colourbutton.h - colour selection button * Program: kalarm * Copyright © 2008 David Jarvie * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef COLOURBUTTON_H #define COLOURBUTTON_H -#include +#include /** * @short A colour selection button with read-only option. * * The ColourButton class is a KColorButton with a read-only option. * * The widget may be set as read-only. This has the same effect as disabling it, except * that its appearance is unchanged. * * @author David Jarvie */ class ColourButton : public KColorButton { Q_OBJECT public: /** Constructor. * @param parent The parent object of this widget. */ explicit ColourButton(QWidget* parent = nullptr); /** Returns the selected colour. */ QColor colour() const { return color(); } /** Sets the selected colour to @p c. */ void setColour(const QColor& c) { setColor(c); } /** Returns true if the widget is read only. */ bool isReadOnly() const { return mReadOnly; } /** Sets whether the button can be changed by the user. * @param readOnly True to set the widget read-only, false to set it read-write. */ virtual void setReadOnly(bool readOnly); protected: void mousePressEvent(QMouseEvent*) override; void mouseReleaseEvent(QMouseEvent*) override; void mouseMoveEvent(QMouseEvent*) override; void keyPressEvent(QKeyEvent*) override; void keyReleaseEvent(QKeyEvent*) override; private: bool mReadOnly{false}; // value cannot be changed }; #endif // COLOURBUTTON_H // vim: et sw=4: diff --git a/src/lib/combobox.h b/src/lib/combobox.h index d8123f69..32a5e138 100644 --- a/src/lib/combobox.h +++ b/src/lib/combobox.h @@ -1,66 +1,66 @@ /* * combobox.h - combo box with read-only option * Program: kalarm * Copyright © 2002,2005-2007 David Jarvie * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef COMBOBOX_H #define COMBOBOX_H -#include +#include class QMouseEvent; class QKeyEvent; /** * @short A KComboBox with read-only option. * * The ComboBox class is a KComboBox with a read-only option. * * The widget may be set as read-only. This has the same effect as disabling it, except * that its appearance is unchanged. * * @author David Jarvie */ class ComboBox : public KComboBox { Q_OBJECT public: /** Constructor. * @param parent The parent object of this widget. */ explicit ComboBox(QWidget* parent = nullptr); /** Returns true if the widget is read only. */ bool isReadOnly() const { return mReadOnly; } /** Sets whether the combo box is read-only for the user. If read-only, * its state cannot be changed by the user. * @param readOnly True to set the widget read-only, false to set it read-write. */ virtual void setReadOnly(bool readOnly); protected: void mousePressEvent(QMouseEvent*) override; void mouseReleaseEvent(QMouseEvent*) override; void mouseMoveEvent(QMouseEvent*) override; void keyPressEvent(QKeyEvent*) override; void keyReleaseEvent(QKeyEvent*) override; private: bool mReadOnly{false}; // value cannot be changed }; #endif // COMBOBOX_H // vim: et sw=4: diff --git a/src/lib/filedialog.cpp b/src/lib/filedialog.cpp index df11ec5b..69da14de 100644 --- a/src/lib/filedialog.cpp +++ b/src/lib/filedialog.cpp @@ -1,92 +1,92 @@ /* * filedialog.cpp - file save dialogue, with append option & confirm overwrite * Program: kalarm * Copyright © 2009 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 "filedialog.h" #include "autoqpointer.h" #include "kalarm_debug.h" #include #include -#include +#include #include QCheckBox* FileDialog::mAppendCheck = nullptr; FileDialog::FileDialog(const QUrl &startDir, const QString &filter, QWidget *parent) : KFileCustomDialog(parent) { fileWidget()->setFilter(filter); fileWidget()->setStartDir(startDir); } QString FileDialog::getSaveFileName(const QUrl& dir, const QString& filter, QWidget* parent, const QString& caption, bool* append) { bool defaultDir = dir.isEmpty(); bool specialDir = !defaultDir && dir.scheme() == QLatin1String("kfiledialog"); // 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 FileDialog(specialDir ? dir : QUrl(), filter, parent); if (!specialDir && !defaultDir) { if (!dir.isLocalFile()) qCWarning(KALARM_LOG) << "FileDialog::getSaveFileName called with non-local start dir " << dir; dlg->fileWidget()->setSelectedUrl(dir); // may also be a filename } dlg->setOperationMode(KFileWidget::Saving); dlg->fileWidget()->setMode(KFile::File | KFile::LocalOnly); dlg->fileWidget()->setConfirmOverwrite(true); if (!caption.isEmpty()) dlg->setWindowTitle(caption); mAppendCheck = nullptr; if (append) { // Show an 'append' option in the dialogue. // Note that the dialogue will take ownership of the QCheckBox. mAppendCheck = new QCheckBox(i18nc("@option:check", "Append to existing file"), nullptr); connect(mAppendCheck, &QCheckBox::toggled, dlg.data(), &FileDialog::appendToggled); dlg->setCustomWidget(mAppendCheck); *append = false; } dlg->setWindowModality(Qt::WindowModal); dlg->exec(); if (!dlg) return QString(); // dialogue was deleted QString filename = dlg->fileWidget()->selectedFile(); if (!filename.isEmpty()) { if (append) *append = mAppendCheck->isChecked(); KRecentDocument::add(QUrl::fromLocalFile(filename)); } return filename; } void FileDialog::appendToggled(bool ticked) { fileWidget()->setConfirmOverwrite(!ticked); } // vim: et sw=4: diff --git a/src/lib/lineedit.cpp b/src/lib/lineedit.cpp index 56ee8f27..f46f46e9 100644 --- a/src/lib/lineedit.cpp +++ b/src/lib/lineedit.cpp @@ -1,210 +1,210 @@ /* * lineedit.cpp - Line edit widget with extra drag and drop options * Program: kalarm * Copyright © 2003-2019 David Jarvie * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "lineedit.h" #include #include #include -#include -#include +#include +#include #include #include #include #include #include #include /*============================================================================= = Class LineEdit = Line edit which accepts drag and drop of text, URLs and/or email addresses. * It has an option to prevent its contents being selected when it receives = focus. =============================================================================*/ LineEdit::LineEdit(Type type, QWidget* parent) : KLineEdit(parent) , mType(type) { init(); } LineEdit::LineEdit(QWidget* parent) : KLineEdit(parent) , mType(Text) { init(); } void LineEdit::init() { setAcceptDrops(false); if (mType == Url) { setCompletionMode(KCompletion::CompletionShell); KUrlCompletion* comp = new KUrlCompletion(KUrlCompletion::FileCompletion); comp->setReplaceHome(true); setCompletionObject(comp); setAutoDeleteCompletionObject(true); } else setCompletionMode(KCompletion::CompletionNone); } /****************************************************************************** * Called when the line edit receives focus. * If 'noSelect' is true, prevent the contents being selected. */ void LineEdit::focusInEvent(QFocusEvent* e) { QFocusEvent newe(QEvent::FocusIn, (mNoSelect ? Qt::OtherFocusReason : e->reason())); KLineEdit::focusInEvent(&newe); mNoSelect = false; } QString LineEdit::text() const { if (mType == Url) return KShell::tildeExpand(KLineEdit::text()); return KLineEdit::text(); } void LineEdit::setText(const QString& text) { KLineEdit::setText(text); setCursorPosition(mSetCursorAtEnd ? text.length() : 0); } void LineEdit::dragEnterEvent(QDragEnterEvent* e) { const QMimeData* data = e->mimeData(); bool ok; if (KCalUtils::ICalDrag::canDecode(data)) ok = false; // don't accept "text/calendar" objects else ok = (data->hasText() || data->hasUrls() || (mType != Url && KPIM::MailList::canDecode(data)) || (mType == Emails && KContacts::VCardDrag::canDecode(data))); if (ok) e->accept(rect()); else e->ignore(rect()); } void LineEdit::dropEvent(QDropEvent* e) { const QMimeData* data = e->mimeData(); QString newText; QStringList newEmails; QList files; KContacts::Addressee::List addrList; if (mType != Url && KPIM::MailList::canDecode(data)) { KPIM::MailList mailList = KPIM::MailList::fromMimeData(data); // KMail message(s) - ignore all but the first if (mailList.count()) { if (mType == Emails) newText = mailList.first().from(); else setText(mailList.first().subject()); // replace any existing text } } // This must come before QUrl else if (mType == Emails && KContacts::VCardDrag::canDecode(data) && KContacts::VCardDrag::fromMimeData(data, addrList)) { // KAddressBook entries for (KContacts::Addressee::List::Iterator it = addrList.begin(); it != addrList.end(); ++it) { const QString em((*it).fullEmail()); if (!em.isEmpty()) newEmails.append(em); } } else if (!(files = data->urls()).isEmpty()) { // URL(s) switch (mType) { case Url: // URL entry field - ignore all but the first dropped URL setText(files.first().toDisplayString()); // replace any existing text break; case Emails: { // Email entry field - ignore all but mailto: URLs const QString mailto = QStringLiteral("mailto"); for (const QUrl& file : qAsConst(files)) { if (file.scheme() == mailto) newEmails.append(file.path()); } break; } case Text: newText = files.first().toDisplayString(); break; } } else if (data->hasText()) { // Plain text const QString txt = data->text(); if (mType == Emails) { // Remove newlines from a list of email addresses, and allow an eventual mailto: scheme const QString mailto = QStringLiteral("mailto:"); newEmails = txt.split(QRegExp(QLatin1String("[\r\n]+")), QString::SkipEmptyParts); for (QStringList::Iterator it = newEmails.begin(); it != newEmails.end(); ++it) { if ((*it).startsWith(mailto)) { const QUrl url = QUrl::fromUserInput(*it); *it = url.path(); } } } else { const int newline = txt.indexOf(QLatin1Char('\n')); newText = (newline >= 0) ? txt.left(newline) : txt; } } if (newEmails.count()) { newText = newEmails.join(QLatin1Char(',')); const int c = cursorPosition(); if (c > 0) newText.prepend(QLatin1Char(',')); if (c < static_cast(text().length())) newText.append(QLatin1Char(',')); } if (!newText.isEmpty()) insert(newText); } // vim: et sw=4: diff --git a/src/lib/lineedit.h b/src/lib/lineedit.h index 7fbfa9ec..ac08fe84 100644 --- a/src/lib/lineedit.h +++ b/src/lib/lineedit.h @@ -1,100 +1,100 @@ /* * lineedit.h - line edit widget with extra drag and drop options * Program: kalarm * Copyright © 2003-2019 David Jarvie * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef LINEEDIT_H #define LINEEDIT_H -#include +#include class QDragEnterEvent; class QFocusEvent; class QDropEvent; /** * @short Line edit widget with extra drag and drop options. * * The LineEdit class is a line edit widget which accepts specified types of drag and * drop content. * * The widget will always accept drag and drop of text, except text/calendar mime type, * and of URLs. It will accept additional mime types depending on its configuration: * Text type accepts email address lists. * Email type accepts email address lists and VCard data (e.g. from KAddressBook). * * The class also provides an option to prevent its contents being selected when the * widget receives focus. * * @author David Jarvie */ class LineEdit : public KLineEdit { Q_OBJECT public: /** Types of drag and drop content which will be accepted. * @li Text - the line edit contains general text. It accepts text, a URL * or an email from KMail (the subject line is used). If multiple * URLs or emails are dropped, only the first is used; the * rest are ignored. * @li Url - the line edit contains a URL. It accepts text or a URL. If * multiple URLs are dropped, only the first URL is used; the * rest are ignored. * @li Emails - the line edit contains email addresses. It accepts text, * mailto: URLs, emails from KMail (the From address is used) * or vcard data (e.g. from KAddressBook). If multiple emails * are dropped, only the first is used; the rest are ignored. */ enum Type { Text, Url, Emails }; /** Constructor. * @param type The content type for the line edit. * @param parent The parent object of this widget. */ explicit LineEdit(Type type, QWidget* parent = nullptr); /** Constructs a line edit whose content type is Text. * @param parent The parent object of this widget. */ explicit LineEdit(QWidget* parent = nullptr); /** Return the entered text. * If the type is Url, tilde expansion is performed. */ QString text() const; /** Prevents the line edit's contents being selected when the widget receives focus. */ void setNoSelect() { mNoSelect = true; } /** Sets whether the cursor should be set at the beginning or end of the text when * setText() is called. */ void setCursorAtEnd(bool end = true) { mSetCursorAtEnd = end; } public Q_SLOTS: /** Sets the contents of the line edit to be @p str. */ void setText(const QString& str) override; protected: void focusInEvent(QFocusEvent*) override; void dragEnterEvent(QDragEnterEvent*) override; void dropEvent(QDropEvent*) override; private: void init(); Type mType; bool mNoSelect{false}; bool mSetCursorAtEnd{false}; // setText() should position cursor at end }; #endif // LINEEDIT_H // vim: et sw=4: diff --git a/src/lib/messagebox.cpp b/src/lib/messagebox.cpp index b9fbf772..3387200a 100644 --- a/src/lib/messagebox.cpp +++ b/src/lib/messagebox.cpp @@ -1,136 +1,136 @@ /* * messagebox.cpp - enhanced KMessageBox class * Program: kalarm * Copyright © 2004,2005,2007,2008,2011,2014 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 "messagebox.h" -#include -#include +#include +#include QMap KAMessageBox::mContinueDefaults; const KMessageBox::Options KAMessageBox::NoAppModal = KMessageBox::Options(KMessageBox::Notify | KMessageBox::WindowModal); /****************************************************************************** * Set the default button for continue/cancel message boxes with the specified * 'dontAskAgainName'. */ void KAMessageBox::setContinueDefault(const QString& dontAskAgainName, KMessageBox::ButtonCode defaultButton) { mContinueDefaults[dontAskAgainName] = (defaultButton == KMessageBox::Cancel ? KMessageBox::Cancel : KMessageBox::Continue); } /****************************************************************************** * Get the default button for continue/cancel message boxes with the specified * 'dontAskAgainName'. */ KMessageBox::ButtonCode KAMessageBox::getContinueDefault(const QString& dontAskAgainName) { KMessageBox::ButtonCode defaultButton = KMessageBox::Continue; if (!dontAskAgainName.isEmpty()) { QMap::ConstIterator it = mContinueDefaults.constFind(dontAskAgainName); if (it != mContinueDefaults.constEnd()) defaultButton = it.value(); } return defaultButton; } /****************************************************************************** * If there is no current setting for whether a non-yes/no message box should be * shown, set it to 'defaultShow'. * If a continue/cancel message box has Cancel as the default button, either * setContinueDefault() or warningContinueCancel() must have been called * previously to set this for this 'dontShowAgainName' value. * Reply = true if 'defaultShow' was written. */ bool KAMessageBox::setDefaultShouldBeShownContinue(const QString& dontShowAgainName, bool defaultShow) { if (dontShowAgainName.isEmpty()) return false; // First check whether there is an existing setting KConfigGroup config(KSharedConfig::openConfig(), "Notification Messages"); if (config.hasKey(dontShowAgainName)) return false; // There is no current setting, so write one saveDontShowAgainContinue(dontShowAgainName, !defaultShow); return true; } /****************************************************************************** * Return whether a non-yes/no message box should be shown. * If the message box has Cancel as the default button, either setContinueDefault() * or warningContinueCancel() must have been called previously to set this for this * 'dontShowAgainName' value. */ bool KAMessageBox::shouldBeShownContinue(const QString& dontShowAgainName) { if (getContinueDefault(dontShowAgainName) != KMessageBox::Cancel) return KMessageBox::shouldBeShownContinue(dontShowAgainName); // Cancel is the default button, so we have to use a yes/no message box KMessageBox::ButtonCode b; return shouldBeShownYesNo(dontShowAgainName, b); } /****************************************************************************** * Save whether the yes/no message box should not be shown again. * If 'dontShow' is true, the message box will be suppressed and it will return * 'result'. */ void KAMessageBox::saveDontShowAgainYesNo(const QString& dontShowAgainName, bool dontShow, KMessageBox::ButtonCode result) { saveDontShowAgain(dontShowAgainName, true, dontShow, (result == KMessageBox::Yes ? "yes" : "no")); } /****************************************************************************** * Save whether a non-yes/no message box should not be shown again. * If 'dontShow' is true, the message box will be suppressed and it will return * Continue. * If the message box has Cancel as the default button, either setContinueDefault() * or warningContinueCancel() must have been called previously to set this for this * 'dontShowAgainName' value. */ void KAMessageBox::saveDontShowAgainContinue(const QString& dontShowAgainName, bool dontShow) { if (getContinueDefault(dontShowAgainName) == KMessageBox::Cancel) saveDontShowAgainYesNo(dontShowAgainName, dontShow, KMessageBox::Yes); else saveDontShowAgain(dontShowAgainName, false, dontShow); } /****************************************************************************** * Save whether the message box should not be shown again. */ void KAMessageBox::saveDontShowAgain(const QString& dontShowAgainName, bool yesno, bool dontShow, const char* yesnoResult) { if (dontShowAgainName.isEmpty()) return; KConfigGroup config(KSharedConfig::openConfig(), "Notification Messages"); KConfig::WriteConfigFlags flags = (dontShowAgainName[0] == QLatin1Char(':')) ? KConfig::Global | KConfig::Persistent : KConfig::Persistent; if (yesno) config.writeEntry(dontShowAgainName, QString::fromLatin1(dontShow ? yesnoResult : ""), flags); else config.writeEntry(dontShowAgainName, !dontShow, flags); config.sync(); } // vim: et sw=4: diff --git a/src/lib/messagebox.h b/src/lib/messagebox.h index 756fb51b..db433078 100644 --- a/src/lib/messagebox.h +++ b/src/lib/messagebox.h @@ -1,198 +1,198 @@ /* * messagebox.h - enhanced KMessageBox class * Program: kalarm * Copyright © 2004-2019 David Jarvie * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef MESSAGEBOX_H #define MESSAGEBOX_H -#include -#include +#include +#include /** * @short Enhanced KMessageBox. * * The KAMessageBox class provides an extension to KMessageBox, including the option for * Continue/Cancel message boxes to have a default button of Cancel. * * Note that this class is not called simply MessageBox due to a name clash on Windows. * * @author David Jarvie */ class KAMessageBox { public: /** KAMessageBox types. * @li CONT_CANCEL_DEF_CONT - Continue/Cancel, with Continue as the default button. * @li CONT_CANCEL_DEF_CANCEL - Continue/Cancel, with Cancel as the default button. * @li YES_NO_DEF_NO - Yes/No, with No as the default button. */ enum AskType { // KAMessageBox types CONT_CANCEL_DEF_CONT, // Continue/Cancel, with default = Continue CONT_CANCEL_DEF_CANCEL, // Continue/Cancel, with default = Cancel YES_NO_DEF_NO // Yes/No, with default = No }; /** Gets the default button for the Continue/Cancel message box with the specified * "don't ask again" name. * @param dontAskAgainName The identifier controlling whether the message box is suppressed. */ static KMessageBox::ButtonCode getContinueDefault(const QString& dontAskAgainName); /** Sets the default button for the Continue/Cancel message box with the specified * "don't ask again" name. * @param dontAskAgainName The identifier controlling whether the message box is suppressed. * @param defaultButton The default button for the message box. Valid values are Continue or Cancel. */ static void setContinueDefault(const QString& dontAskAgainName, KMessageBox::ButtonCode defaultButton); /** If there is no current setting for whether a non-Yes/No message box should be * shown, sets it to @p defaultShow. * If a Continue/Cancel message box has Cancel as the default button, either * setContinueDefault() or warningContinueCancel() must have been called * previously to set this for the specified @p dontShowAgainName value. * @return true if @p defaultShow was written. */ static bool setDefaultShouldBeShownContinue(const QString& dontShowAgainName, bool defaultShow); /** Returns whether a non-Yes/No message box should be shown. * If the message box has Cancel as the default button, either setContinueDefault() * or warningContinueCancel() must have been called previously to set this for the * specified @p dontShowAgainName value. * @param dontShowAgainName The identifier controlling whether the message box is suppressed. */ static bool shouldBeShownContinue(const QString& dontShowAgainName); /** Stores whether the Yes/No message box should or should not be shown again. * @param dontShowAgainName The identifier controlling whether the message box is suppressed. * @param dontShow If true, the message box will be suppressed and will return @p result. * @param result The button code to return if the message box is suppressed. */ static void saveDontShowAgainYesNo(const QString& dontShowAgainName, bool dontShow = true, KMessageBox::ButtonCode result = KMessageBox::No); /** Stores whether a non-Yes/No message box should or should not be shown again. * If the message box has Cancel as the default button, either setContinueDefault() * or warningContinueCancel() must have been called previously to set this for the * specified @p dontShowAgainName value. * @param dontShowAgainName The identifier controlling whether the message box is suppressed. * @param dontShow If true, the message box will be suppressed and will return Continue. */ static void saveDontShowAgainContinue(const QString& dontShowAgainName, bool dontShow = true); /** Same as KMessageBox::detailedError() except that it defaults to window-modal, * not application-modal. */ static void detailedError(QWidget* parent, const QString& text, const QString& details, const QString& caption = QString(), KMessageBox::Options options = KMessageBox::Options(KMessageBox::Notify|KMessageBox::WindowModal)) { KMessageBox::detailedError(parent, text, details, caption, options); } /** Same as KMessageBox::detailedSorry() except that it defaults to window-modal, * not application-modal. */ static void detailedSorry(QWidget* parent, const QString& text, const QString& details, const QString& caption = QString(), KMessageBox::Options options = KMessageBox::Options(KMessageBox::Notify|KMessageBox::WindowModal)) { KMessageBox::detailedSorry(parent, text, details, caption, options); } /** Same as KMessageBox::informationList() except that it defaults to window-modal, * not application-modal. */ static void informationList(QWidget* parent, const QString& text, const QStringList& details, const QString& caption = QString(), const QString& dontShowAgainName = QString(), KMessageBox::Options options = KMessageBox::Options(KMessageBox::Notify|KMessageBox::WindowModal)) { KMessageBox::informationList(parent, text, details, caption, dontShowAgainName, options); } /** Same as KMessageBox::error() except that it defaults to window-modal, * not application-modal. */ static void error(QWidget* parent, const QString& text, const QString& caption = QString(), KMessageBox::Options options = KMessageBox::Options(KMessageBox::Notify|KMessageBox::WindowModal)) { KMessageBox::error(parent, text, caption, options); } /** Same as KMessageBox::information() except that it defaults to window-modal, * not application-modal. */ static void information(QWidget* parent, const QString& text, const QString& caption = QString(), const QString& dontShowAgainName = QString(), KMessageBox::Options options = KMessageBox::Options(KMessageBox::Notify|KMessageBox::WindowModal)) { KMessageBox::information(parent, text, caption, dontShowAgainName, options); } /** Same as KMessageBox::sorry() except that it defaults to window-modal, * not application-modal. */ static void sorry(QWidget* parent, const QString& text, const QString& caption = QString(), KMessageBox::Options options = KMessageBox::Options(KMessageBox::Notify|KMessageBox::WindowModal)) { KMessageBox::sorry(parent, text, caption, options); } /** Same as KMessageBox::questionYesNo() except that it defaults to window-modal, * not application-modal. */ static int questionYesNo(QWidget* parent, const QString& text, const QString& caption = QString(), const KGuiItem& buttonYes = KStandardGuiItem::yes(), const KGuiItem& buttonNo = KStandardGuiItem::no(), const QString& dontAskAgainName = QString(), KMessageBox::Options options = KMessageBox::Options(KMessageBox::Notify|KMessageBox::WindowModal)) { return KMessageBox::questionYesNo(parent, text, caption, buttonYes, buttonNo, dontAskAgainName, options); } /** Same as KMessageBox::questionYesNoCancel() except that it defaults * to window-modal, not application-modal. */ static int questionYesNoCancel(QWidget* parent, const QString& text, const QString& caption = QString(), const KGuiItem& buttonYes = KStandardGuiItem::yes(), const KGuiItem& buttonNo = KStandardGuiItem::no(), const KGuiItem& buttonCancel = KStandardGuiItem::cancel(), const QString& dontAskAgainName = QString(), KMessageBox::Options options = KMessageBox::Options(KMessageBox::Notify|KMessageBox::WindowModal)) { return KMessageBox::questionYesNoCancel(parent, text, caption, buttonYes, buttonNo, buttonCancel, dontAskAgainName, options); } /** Same as KMessageBox::warningContinueCancel() except that the * default button is Cancel, and it defaults to window-modal, not * application-modal. * @param parent Parent widget * @param text Message string * @param caption Caption (window title) of the message box * @param buttonContinue The text for the first button (default = "Continue") * @param buttonCancel The text for the second button (default = "Cancel") * @param dontAskAgainName If specified, the message box will only be suppressed * if the user chose Continue last time * @param options Other options */ static int warningCancelContinue(QWidget* parent, const QString& text, const QString& caption = QString(), const KGuiItem& buttonContinue = KStandardGuiItem::cont(), const KGuiItem& buttonCancel = KStandardGuiItem::cancel(), const QString& dontAskAgainName = QString(), KMessageBox::Options options = KMessageBox::Options(KMessageBox::Notify|KMessageBox::WindowModal)) { return KMessageBox::warningContinueCancel(parent, text, caption, buttonContinue, buttonCancel, dontAskAgainName, KMessageBox::Options(options | KMessageBox::Dangerous)); } /** Same as KMessageBox::warningContinueCancel() except that it * defaults to window-modal, not application-modal. */ static int warningContinueCancel(QWidget* parent, const QString& text, const QString& caption = QString(), const KGuiItem& buttonContinue = KStandardGuiItem::cont(), const KGuiItem& buttonCancel = KStandardGuiItem::cancel(), const QString& dontAskAgainName = QString(), KMessageBox::Options options = KMessageBox::Options(KMessageBox::Notify|KMessageBox::WindowModal)) { return KMessageBox::warningContinueCancel(parent, text, caption, buttonContinue, buttonCancel, dontAskAgainName, options); } /** Same as KMessageBox::warningYesNo() except that it defaults to window-modal, * not application-modal. */ static int warningYesNo(QWidget* parent, const QString& text, const QString& caption = QString(), const KGuiItem& buttonYes = KStandardGuiItem::yes(), const KGuiItem& buttonNo = KStandardGuiItem::no(), const QString& dontAskAgainName = QString(), KMessageBox::Options options = KMessageBox::Options(KMessageBox::Notify|KMessageBox::Dangerous|KMessageBox::WindowModal)) { return KMessageBox::warningYesNo(parent, text, caption, buttonYes, buttonNo, dontAskAgainName, options); } /** Shortcut to represent Options(Notify | WindowModal). */ static const KMessageBox::Options NoAppModal; private: static void saveDontShowAgain(const QString& dontShowAgainName, bool yesno, bool dontShow, const char* yesnoResult = nullptr); static QMap mContinueDefaults; }; #endif // vim: et sw=4: diff --git a/src/lib/shellprocess.cpp b/src/lib/shellprocess.cpp index 0dd7f76e..5ada0882 100644 --- a/src/lib/shellprocess.cpp +++ b/src/lib/shellprocess.cpp @@ -1,225 +1,225 @@ /* * shellprocess.cpp - execute a shell process * Program: kalarm * Copyright © 2004,2005,2007,2008 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 "kalarm_debug.h" #include -#include +#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) { } /****************************************************************************** * 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) << "ShellProcess::slotExited:" << exitCode << "," << exitStatus; mStdinQueue.clear(); mStatus = SUCCESS; mExitCode = exitCode; if (exitStatus != NormalExit) { 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) << "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/shellprocess.h b/src/lib/shellprocess.h index e153e591..5ae143e5 100644 --- a/src/lib/shellprocess.h +++ b/src/lib/shellprocess.h @@ -1,149 +1,149 @@ /* * shellprocess.h - execute a process through the shell * Program: kalarm * Copyright © 2004-2019 David Jarvie * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef SHELLPROCESS_H #define SHELLPROCESS_H /** @file shellprocess.h - execute a process through the shell */ -#include +#include #include #include /** * @short Enhanced KProcess to run a shell command. * * The ShellProcess class runs a shell command and interprets the shell exit status * as far as possible. It blocks execution if shell access is prohibited. It buffers * data written to the process's stdin. * * Before executing any command, ShellProcess checks whether shell commands are * allowed at all. If not (e.g. if the user is running in kiosk mode), it blocks * execution. * * Derived from KProcess, this class additionally tries to interpret the shell * exit status. Different shells use different exit codes. Currently, if bash or ksh * report that the command could not be found or could not be executed, the NOT_FOUND * status is returned. * * Writes to the process's stdin are buffered, so that unlike with KProcess, there * is no need to wait for the write to complete before writing again. * * @author David Jarvie */ class ShellProcess : public KProcess { Q_OBJECT public: /** Current status of the shell process. * @li INACTIVE - start() has not yet been called to run the command. * @li RUNNING - the command is currently running. * @li SUCCESS - the command appears to have exited successfully. * @li UNAUTHORISED - shell commands are not authorised for this user. * @li DIED - the command didn't exit cleanly, i.e. was killed or died. * @li NOT_FOUND - the command was either not found or not executable. * @li START_FAIL - the command couldn't be started for other reasons. */ enum Status { INACTIVE, // start() has not yet been called to run the command RUNNING, // command is currently running SUCCESS, // command appears to have exited successfully UNAUTHORISED, // shell commands are not authorised for this user DIED, // command didn't exit cleanly, i.e. was killed or died NOT_FOUND, // command either not found or not executable START_FAIL // command couldn't be started for other reasons }; /** Constructor. * @param command The command line to be run when start() is called. */ explicit ShellProcess(const QString& command); /** Executes the configured command. * @param openMode WriteOnly for stdin only, ReadOnly for stdout/stderr only, else ReadWrite. */ bool start(OpenMode = ReadWrite); /** Returns the current status of the shell process. */ Status status() const { return mStatus; } /** Returns the shell exit code. Only valid if status() == SUCCESS or NOT_FOUND. */ int exitCode() const { return mExitCode; } /** Returns whether the command was run successfully. * @return True if the command has been run and appears to have exited successfully. */ bool normalExit() const { return mStatus == SUCCESS; } /** Returns the command configured to be run. */ const QString& command() const { return mCommand; } /** Returns the error message corresponding to the command exit status. * @return Error message if an error occurred. Null string if the command has not yet * exited, or if the command ran successfully. */ QString errorMessage() const; /** Writes a string to the process's STDIN. */ void writeStdin(const char* buffer, int bufflen); /** Tell the process to exit once any outstanding STDIN strings have been written. */ void stdinExit(); /** Returns whether the user is authorised to run shell commands. Shell commands may * be prohibited in kiosk mode, for example. */ static bool authorised(); /** Determines which shell to use. * @return file name of shell, excluding path. */ static const QByteArray& shellName() { shellPath(); return mShellName; } /** Determines which shell to use. * @return path name of shell. */ static const QByteArray& shellPath(); Q_SIGNALS: /** Signal emitted when the shell process execution completes. It is not emitted * if start() did not attempt to start the command execution, e.g. in kiosk mode. */ void shellExited(ShellProcess*); /** Signal emitted when input is available from the process's stdout. */ void receivedStdout(ShellProcess*); /** Signal emitted when input is available from the process's stderr. */ void receivedStderr(ShellProcess*); private Q_SLOTS: void writtenStdin(qint64 bytes); void stdoutReady() { Q_EMIT receivedStdout(this); } void stderrReady() { Q_EMIT receivedStderr(this); } void slotExited(int exitCode, QProcess::ExitStatus); private: // Prohibit the following inherited methods ShellProcess& operator<<(const QString&); ShellProcess& operator<<(const QStringList&); static QByteArray mShellName; // name of shell to be used static QByteArray mShellPath; // path of shell to be used static bool mInitialised; // true once static data has been initialised static bool mAuthorised; // true if shell commands are authorised QString mCommand; // copy of command to be executed QQueue mStdinQueue; // queued strings to send to STDIN qint64 mStdinBytes{0}; // bytes still to be written from first queued string int mExitCode; // shell exit value (if mStatus == SUCCESS or NOT_FOUND) Status mStatus{INACTIVE}; // current execution status bool mStdinExit{false}; // exit once STDIN queue has been written }; #endif // SHELLPROCESS_H // vim: et sw=4: diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index d3657c93..25e2ba11 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -1,1621 +1,1621 @@ /* * mainwindow.cpp - main application window * Program: kalarm * Copyright © 2001-2019 David Jarvie * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "mainwindow.h" #include "alarmcalendar.h" #include "alarmlistdelegate.h" #include "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 "resources/resources.h" #include "resources/eventmodel.h" #include "kalarm_debug.h" #include #include #include #include #include #include #include #include #include #include using namespace KCalendarCore; using namespace KCalUtils; #include -#include +#include #include #include -#include +#include #include #include -#include -#include -#include -#include -#include -#include +#include +#include +#include +#include +#include +#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace KAlarmCal; namespace { const QString UI_FILE(QStringLiteral("kalarmui.rc")); const char* WINDOW_NAME = "MainWindow"; const char* VIEW_GROUP = "View"; const char* SHOW_COLUMNS = "ShowColumns"; const char* SHOW_ARCHIVED_KEY = "ShowArchivedAlarms"; const char* SHOW_RESOURCES_KEY = "ShowResources"; QString undoText; QString undoTextStripped; QList undoShortcut; QString redoText; QString redoTextStripped; QList redoShortcut; } /*============================================================================= = Class: MainWindow =============================================================================*/ MainWindow::WindowList MainWindow::mWindowList; TemplateDlg* MainWindow::mTemplateDlg = nullptr; /****************************************************************************** * Construct an instance. * To avoid resize() events occurring while still opening the calendar (and * resultant crashes), the calendar is opened before constructing the instance. */ MainWindow* MainWindow::create(bool restored) { theApp()->checkCalendar(); // ensure calendar is open return new MainWindow(restored); } MainWindow::MainWindow(bool restored) : MainWindowBase(nullptr, Qt::WindowContextHelpButtonHint) { qCDebug(KALARM_LOG) << "MainWindow:"; setAttribute(Qt::WA_DeleteOnClose); setWindowModality(Qt::WindowModal); setObjectName(QStringLiteral("MainWin")); // used by LikeBack setPlainCaption(KAboutData::applicationData().displayName()); KConfigGroup config(KSharedConfig::openConfig(), VIEW_GROUP); mShowResources = config.readEntry(SHOW_RESOURCES_KEY, false); mShowArchived = config.readEntry(SHOW_ARCHIVED_KEY, false); const QList showColumns = config.readEntry(SHOW_COLUMNS, QList()); if (!restored) { KConfigGroup wconfig(KSharedConfig::openConfig(), WINDOW_NAME); mResourcesWidth = wconfig.readEntry(QStringLiteral("Splitter %1").arg(QApplication::desktop()->width()), (int)0); } setAcceptDrops(true); // allow drag-and-drop onto this window mSplitter = new QSplitter(Qt::Horizontal, this); mSplitter->setChildrenCollapsible(false); mSplitter->installEventFilter(this); setCentralWidget(mSplitter); // Create the calendar resource selector widget Akonadi::ControlGui::widgetNeedsAkonadi(this); mResourceSelector = new ResourceSelector(mSplitter); mSplitter->setStretchFactor(0, 0); // don't resize resource selector when window is resized mSplitter->setStretchFactor(1, 1); // Create the alarm list widget mListFilterModel = AlarmListModel::create(this); mListFilterModel->setEventTypeFilter(mShowArchived ? CalEvent::ACTIVE | CalEvent::ARCHIVED : CalEvent::ACTIVE); mListView = new AlarmListView(WINDOW_NAME, mSplitter); mListView->setModel(mListFilterModel); mListView->setColumnsVisible(showColumns); mListView->setItemDelegate(new AlarmListDelegate(mListView)); connect(mListView->selectionModel(), &QItemSelectionModel::selectionChanged, this, &MainWindow::slotSelection); connect(mListView, &AlarmListView::contextMenuRequested, this, &MainWindow::slotContextMenuRequested); connect(mListView, &AlarmListView::columnsVisibleChanged, this, &MainWindow::slotAlarmListColumnsChanged); connect(Resources::instance(), &Resources::settingsChanged, this, &MainWindow::slotCalendarStatusChanged); connect(mResourceSelector, &ResourceSelector::resized, this, &MainWindow::resourcesResized); mListView->installEventFilter(this); initActions(); setAutoSaveSettings(QLatin1String(WINDOW_NAME), true); // save toolbars, window sizes etc. mWindowList.append(this); if (mWindowList.count() == 1) { // It's the first main window if (theApp()->wantShowInSystemTray()) theApp()->displayTrayIcon(true, this); // create system tray icon for run-in-system-tray mode else if (theApp()->trayWindow()) theApp()->trayWindow()->setAssocMainWindow(this); // associate this window with the system tray icon } slotCalendarStatusChanged(); // initialise action states now that window is registered } MainWindow::~MainWindow() { qCDebug(KALARM_LOG) << "~MainWindow"; bool trayParent = isTrayParent(); // must call before removing from window list mWindowList.removeAt(mWindowList.indexOf(this)); // Prevent view updates during window destruction delete mResourceSelector; mResourceSelector = nullptr; delete mListView; mListView = nullptr; if (theApp()->trayWindow()) { if (trayParent) delete theApp()->trayWindow(); else theApp()->trayWindow()->removeWindow(this); } KSharedConfig::openConfig()->sync(); // save any new window size to disc theApp()->quitIf(); } /****************************************************************************** * Called when the QApplication::saveStateRequest() signal has been emitted. * Save settings to the session managed config file, for restoration * when the program is restored. */ void MainWindow::saveProperties(KConfigGroup& config) { config.writeEntry("HiddenTrayParent", isTrayParent() && isHidden()); config.writeEntry("ShowArchived", mShowArchived); config.writeEntry("ShowColumns", mListView->columnsVisible()); config.writeEntry("ResourcesWidth", mResourceSelector->isHidden() ? 0 : mResourceSelector->width()); } /****************************************************************************** * Read settings from the session managed config file. * This function is automatically called whenever the app is being * restored. Read in whatever was saved in saveProperties(). */ void MainWindow::readProperties(const KConfigGroup& config) { mHiddenTrayParent = config.readEntry("HiddenTrayParent", true); mShowArchived = config.readEntry("ShowArchived", false); mResourcesWidth = config.readEntry("ResourcesWidth", (int)0); mShowResources = (mResourcesWidth > 0); mListView->setColumnsVisible(config.readEntry("ShowColumns", QList())); } /****************************************************************************** * Get the main main window, i.e. the parent of the system tray icon, or if * none, the first main window to be created. Visible windows take precedence * over hidden ones. */ MainWindow* MainWindow::mainMainWindow() { MainWindow* tray = theApp()->trayWindow() ? theApp()->trayWindow()->assocMainWindow() : nullptr; if (tray && tray->isVisible()) return tray; for (int i = 0, end = mWindowList.count(); i < end; ++i) if (mWindowList[i]->isVisible()) return mWindowList[i]; if (tray) return tray; if (mWindowList.isEmpty()) return nullptr; return mWindowList[0]; } /****************************************************************************** * Check whether this main window is effectively the parent of the system tray icon. */ bool MainWindow::isTrayParent() const { TrayWindow* tray = theApp()->trayWindow(); if (!tray || !QSystemTrayIcon::isSystemTrayAvailable()) return false; if (tray->assocMainWindow() == this) return true; return mWindowList.count() == 1; } /****************************************************************************** * Close all main windows. */ void MainWindow::closeAll() { while (!mWindowList.isEmpty()) delete mWindowList[0]; // N.B. the destructor removes the window from the list } /****************************************************************************** * Intercept events for the splitter widget. */ bool MainWindow::eventFilter(QObject* obj, QEvent* e) { if (obj == mSplitter) { switch (e->type()) { case QEvent::Resize: // Don't change resources size while WINDOW is being resized. // Resize event always occurs before Paint. mResizing = true; break; case QEvent::Paint: // Allow resources to be resized again mResizing = false; break; default: break; } } else if (obj == mListView) { switch (e->type()) { case QEvent::KeyPress: { QKeyEvent* ke = static_cast(e); if (ke->key() == Qt::Key_Delete && ke->modifiers() == Qt::ShiftModifier) { // Prevent Shift-Delete being processed by EventListDelegate mActionDeleteForce->trigger(); return true; } break; } default: break; } } return false; } /****************************************************************************** * Called when the window's size has changed (before it is painted). * Sets the last column in the list view to extend at least to the right hand * edge of the list view. * Records the new size in the config file. */ void MainWindow::resizeEvent(QResizeEvent* re) { // Save the window's new size only if it's the first main window MainWindowBase::resizeEvent(re); if (mResourcesWidth > 0) { QList widths; widths.append(mResourcesWidth); widths.append(width() - mResourcesWidth - mSplitter->handleWidth()); mSplitter->setSizes(widths); } } /****************************************************************************** * Called when the resources panel has been resized. * Records the new size in the config file. */ void MainWindow::resourcesResized() { if (!mShown || mResizing) return; QList widths = mSplitter->sizes(); if (widths.count() > 1) { mResourcesWidth = widths[0]; // Width is reported as non-zero when resource selector is // actually invisible, so note a zero width in these circumstances. if (mResourcesWidth <= 5) mResourcesWidth = 0; else if (mainMainWindow() == this) { KConfigGroup config(KSharedConfig::openConfig(), WINDOW_NAME); config.writeEntry(QStringLiteral("Splitter %1").arg(QApplication::desktop()->width()), mResourcesWidth); config.sync(); } } } /****************************************************************************** * Called when the window is first displayed. * Sets the last column in the list view to extend at least to the right hand * edge of the list view. */ void MainWindow::showEvent(QShowEvent* se) { if (mResourcesWidth > 0) { QList widths; widths.append(mResourcesWidth); widths.append(width() - mResourcesWidth - mSplitter->handleWidth()); mSplitter->setSizes(widths); } MainWindowBase::showEvent(se); mShown = true; } /****************************************************************************** * Display the window. */ void MainWindow::show() { MainWindowBase::show(); if (mMenuError) { // Show error message now that the main window has been displayed. // Waiting until now lets the user easily associate the message with // the main window which is faulty. KAMessageBox::error(this, xi18nc("@info", "Failure to create menus (perhaps %1 missing or corrupted)", UI_FILE)); mMenuError = false; } } /****************************************************************************** * Called after the window is hidden. */ void MainWindow::hideEvent(QHideEvent* he) { MainWindowBase::hideEvent(he); } /****************************************************************************** * Initialise the menu, toolbar and main window actions. */ void MainWindow::initActions() { KActionCollection* actions = actionCollection(); mActionTemplates = new QAction(i18nc("@action", "&Templates..."), this); actions->addAction(QStringLiteral("templates"), mActionTemplates); connect(mActionTemplates, &QAction::triggered, this, &MainWindow::slotTemplates); mActionNew = new NewAlarmAction(false, i18nc("@action", "&New"), this, actions); actions->addAction(QStringLiteral("new"), mActionNew); QAction* action = mActionNew->displayAlarmAction(QStringLiteral("newDisplay")); connect(action, &QAction::triggered, this, &MainWindow::slotNewDisplay); action = mActionNew->commandAlarmAction(QStringLiteral("newCommand")); connect(action, &QAction::triggered, this, &MainWindow::slotNewCommand); action = mActionNew->emailAlarmAction(QStringLiteral("newEmail")); connect(action, &QAction::triggered, this, &MainWindow::slotNewEmail); action = mActionNew->audioAlarmAction(QStringLiteral("newAudio")); connect(action, &QAction::triggered, this, &MainWindow::slotNewAudio); TemplateMenuAction* templateMenuAction = mActionNew->fromTemplateAlarmAction(QStringLiteral("newFromTemplate")); connect(templateMenuAction, &TemplateMenuAction::selected, this, &MainWindow::slotNewFromTemplate); mActionCreateTemplate = new QAction(i18nc("@action", "Create Tem&plate..."), this); actions->addAction(QStringLiteral("createTemplate"), mActionCreateTemplate); connect(mActionCreateTemplate, &QAction::triggered, this, &MainWindow::slotNewTemplate); mActionCopy = new QAction(QIcon::fromTheme(QStringLiteral("edit-copy")), i18nc("@action", "&Copy..."), this); actions->addAction(QStringLiteral("copy"), mActionCopy); actions->setDefaultShortcut(mActionCopy, QKeySequence(Qt::SHIFT + Qt::Key_Insert)); connect(mActionCopy, &QAction::triggered, this, &MainWindow::slotCopy); mActionModify = new QAction(QIcon::fromTheme(QStringLiteral("document-properties")), i18nc("@action", "&Edit..."), this); actions->addAction(QStringLiteral("modify"), mActionModify); actions->setDefaultShortcut(mActionModify, QKeySequence(Qt::CTRL + Qt::Key_E)); connect(mActionModify, &QAction::triggered, this, &MainWindow::slotModify); mActionDelete = new QAction(QIcon::fromTheme(QStringLiteral("edit-delete")), i18nc("@action", "&Delete"), this); actions->addAction(QStringLiteral("delete"), mActionDelete); actions->setDefaultShortcut(mActionDelete, QKeySequence::Delete); connect(mActionDelete, &QAction::triggered, this, &MainWindow::slotDeleteIf); // Set up Shift-Delete as a shortcut to delete without confirmation mActionDeleteForce = new QAction(i18nc("@action", "Delete Without Confirmation"), this); actions->addAction(QStringLiteral("delete-force"), mActionDeleteForce); actions->setDefaultShortcut(mActionDeleteForce, QKeySequence::Delete + Qt::SHIFT); connect(mActionDeleteForce, &QAction::triggered, this, &MainWindow::slotDeleteForce); mActionReactivate = new QAction(i18nc("@action", "Reac&tivate"), this); actions->addAction(QStringLiteral("undelete"), mActionReactivate); actions->setDefaultShortcut(mActionReactivate, QKeySequence(Qt::CTRL + Qt::Key_R)); connect(mActionReactivate, &QAction::triggered, this, &MainWindow::slotReactivate); mActionEnable = new QAction(this); actions->addAction(QStringLiteral("disable"), mActionEnable); actions->setDefaultShortcut(mActionEnable, QKeySequence(Qt::CTRL + Qt::Key_B)); connect(mActionEnable, &QAction::triggered, this, &MainWindow::slotEnable); action = new QAction(i18nc("@action", "Wake From Suspend..."), this); actions->addAction(QStringLiteral("wakeSuspend"), action); connect(action, &QAction::triggered, this, &MainWindow::slotWakeFromSuspend); action = KAlarm::createStopPlayAction(this); actions->addAction(QStringLiteral("stopAudio"), action); KGlobalAccel::setGlobalShortcut(action, QList()); // allow user to set a global shortcut mActionShowArchived = new KToggleAction(i18nc("@action", "Show Archi&ved Alarms"), this); actions->addAction(QStringLiteral("showArchivedAlarms"), mActionShowArchived); actions->setDefaultShortcut(mActionShowArchived, QKeySequence(Qt::CTRL + Qt::Key_P)); connect(mActionShowArchived, &KToggleAction::triggered, this, &MainWindow::slotShowArchived); mActionToggleTrayIcon = new KToggleAction(i18nc("@action", "Show in System &Tray"), this); actions->addAction(QStringLiteral("showInSystemTray"), mActionToggleTrayIcon); connect(mActionToggleTrayIcon, &KToggleAction::triggered, this, &MainWindow::slotToggleTrayIcon); mActionToggleResourceSel = new KToggleAction(QIcon::fromTheme(QStringLiteral("view-choose")), i18nc("@action", "Show &Calendars"), this); actions->addAction(QStringLiteral("showResources"), mActionToggleResourceSel); connect(mActionToggleResourceSel, &KToggleAction::triggered, this, &MainWindow::slotToggleResourceSelector); mActionSpreadWindows = KAlarm::createSpreadWindowsAction(this); actions->addAction(QStringLiteral("spread"), mActionSpreadWindows); KGlobalAccel::setGlobalShortcut(mActionSpreadWindows, QList()); // allow user to set a global shortcut mActionImportAlarms = new QAction(i18nc("@action", "Import &Alarms..."), this); actions->addAction(QStringLiteral("importAlarms"), mActionImportAlarms); connect(mActionImportAlarms, &QAction::triggered, this, &MainWindow::slotImportAlarms); mActionImportBirthdays = new QAction(i18nc("@action", "Import &Birthdays..."), this); actions->addAction(QStringLiteral("importBirthdays"), mActionImportBirthdays); connect(mActionImportBirthdays, &QAction::triggered, this, &MainWindow::slotBirthdays); mActionExportAlarms = new QAction(i18nc("@action", "E&xport Selected Alarms..."), this); actions->addAction(QStringLiteral("exportAlarms"), mActionExportAlarms); connect(mActionExportAlarms, &QAction::triggered, this, &MainWindow::slotExportAlarms); mActionExport = new QAction(i18nc("@action", "E&xport..."), this); actions->addAction(QStringLiteral("export"), mActionExport); connect(mActionExport, &QAction::triggered, this, &MainWindow::slotExportAlarms); action = new QAction(QIcon::fromTheme(QStringLiteral("view-refresh")), i18nc("@action", "&Refresh Alarms"), this); actions->addAction(QStringLiteral("refreshAlarms"), action); connect(action, &QAction::triggered, this, &MainWindow::slotRefreshAlarms); KToggleAction* toggleAction = KAlarm::createAlarmEnableAction(this); actions->addAction(QStringLiteral("alarmsEnable"), toggleAction); if (undoText.isNull()) { // Get standard texts, etc., for Undo and Redo actions QAction* act = KStandardAction::undo(this, nullptr, actions); undoShortcut = act->shortcuts(); undoText = act->text(); undoTextStripped = KLocalizedString::removeAcceleratorMarker(undoText); delete act; act = KStandardAction::redo(this, nullptr, actions); redoShortcut = act->shortcuts(); redoText = act->text(); redoTextStripped = KLocalizedString::removeAcceleratorMarker(redoText); delete act; } mActionUndo = new KToolBarPopupAction(QIcon::fromTheme(QStringLiteral("edit-undo")), undoText, this); actions->addAction(QStringLiteral("edit_undo"), mActionUndo); actions->setDefaultShortcuts(mActionUndo, undoShortcut); connect(mActionUndo, &KToolBarPopupAction::triggered, this, &MainWindow::slotUndo); mActionRedo = new KToolBarPopupAction(QIcon::fromTheme(QStringLiteral("edit-redo")), redoText, this); actions->addAction(QStringLiteral("edit_redo"), mActionRedo); actions->setDefaultShortcuts(mActionRedo, redoShortcut); connect(mActionRedo, &KToolBarPopupAction::triggered, this, &MainWindow::slotRedo); KStandardAction::find(mListView, SLOT(slotFind()), actions); mActionFindNext = KStandardAction::findNext(mListView, SLOT(slotFindNext()), actions); mActionFindPrev = KStandardAction::findPrev(mListView, SLOT(slotFindPrev()), actions); KStandardAction::selectAll(mListView, SLOT(selectAll()), actions); KStandardAction::deselect(mListView, SLOT(clearSelection()), actions); // Quit only once the event loop is called; otherwise, the parent window will // be deleted while still processing the action, resulting in a crash. QAction* act = KStandardAction::quit(nullptr, nullptr, actions); connect(act, &QAction::triggered, this, &MainWindow::slotQuit, Qt::QueuedConnection); KStandardAction::keyBindings(this, SLOT(slotConfigureKeys()), actions); KStandardAction::configureToolbars(this, SLOT(slotConfigureToolbar()), actions); KStandardAction::preferences(this, SLOT(slotPreferences()), actions); mResourceSelector->initActions(actions); setStandardToolBarMenuEnabled(true); createGUI(UI_FILE); // Load menu and toolbar settings applyMainWindowSettings(KSharedConfig::openConfig()->group(WINDOW_NAME)); mContextMenu = static_cast(factory()->container(QStringLiteral("listContext"), this)); mActionsMenu = static_cast(factory()->container(QStringLiteral("actions"), this)); QMenu* resourceMenu = static_cast(factory()->container(QStringLiteral("resourceContext"), this)); mResourceSelector->setContextMenu(resourceMenu); mMenuError = (!mContextMenu || !mActionsMenu || !resourceMenu); connect(mActionUndo->menu(), &QMenu::aboutToShow, this, &MainWindow::slotInitUndoMenu); connect(mActionUndo->menu(), &QMenu::triggered, this, &MainWindow::slotUndoItem); connect(mActionRedo->menu(), &QMenu::aboutToShow, this, &MainWindow::slotInitRedoMenu); connect(mActionRedo->menu(), &QMenu::triggered, this, &MainWindow::slotRedoItem); connect(Undo::instance(), &Undo::changed, this, &MainWindow::slotUndoStatus); connect(mListView, &AlarmListView::findActive, this, &MainWindow::slotFindActive); Preferences::connect(SIGNAL(archivedKeepDaysChanged(int)), this, SLOT(updateKeepArchived(int))); Preferences::connect(SIGNAL(showInSystemTrayChanged(bool)), this, SLOT(updateTrayIconAction())); connect(theApp(), &KAlarmApp::trayIconToggled, this, &MainWindow::updateTrayIconAction); // Set menu item states setEnableText(true); mActionShowArchived->setChecked(mShowArchived); if (!Preferences::archivedKeepDays()) mActionShowArchived->setEnabled(false); mActionToggleResourceSel->setChecked(mShowResources); slotToggleResourceSelector(); updateTrayIconAction(); // set the correct text for this action mActionUndo->setEnabled(Undo::haveUndo()); mActionRedo->setEnabled(Undo::haveRedo()); mActionFindNext->setEnabled(false); mActionFindPrev->setEnabled(false); mActionCopy->setEnabled(false); mActionModify->setEnabled(false); mActionDelete->setEnabled(false); mActionReactivate->setEnabled(false); mActionEnable->setEnabled(false); mActionCreateTemplate->setEnabled(false); mActionExport->setEnabled(false); Undo::emitChanged(); // set the Undo/Redo menu texts // Daemon::monitoringAlarms(); } /****************************************************************************** * Enable or disable the Templates menu item in every main window instance. */ void MainWindow::enableTemplateMenuItem(bool enable) { for (int i = 0, end = mWindowList.count(); i < end; ++i) mWindowList[i]->mActionTemplates->setEnabled(enable); } /****************************************************************************** * Refresh the alarm list in every main window instance. */ void MainWindow::refresh() { qCDebug(KALARM_LOG) << "MainWindow::refresh"; AkonadiModel::instance()->reload(); } /****************************************************************************** * Called when the keep archived alarm setting changes in the user preferences. * Enable/disable Show archived alarms option. */ void MainWindow::updateKeepArchived(int days) { qCDebug(KALARM_LOG) << "MainWindow::updateKeepArchived:" << (bool)days; if (mShowArchived && !days) slotShowArchived(); // toggle Show Archived option setting mActionShowArchived->setEnabled(days); } /****************************************************************************** * Select an alarm in the displayed list. */ void MainWindow::selectEvent(const QString& eventId) { mListView->clearSelection(); const QModelIndex index = mListFilterModel->eventIndex(eventId); if (index.isValid()) { mListView->select(index); mListView->scrollTo(index); } } /****************************************************************************** * Return the single selected alarm in the displayed list. */ KAEvent MainWindow::selectedEvent() const { return mListView->selectedEvent(); } /****************************************************************************** * Deselect all alarms in the displayed list. */ void MainWindow::clearSelection() { mListView->clearSelection(); } /****************************************************************************** * Called when the New button is clicked to edit a new alarm to add to the list. */ void MainWindow::slotNew(EditAlarmDlg::Type type) { KAlarm::editNewAlarm(type, mListView); } /****************************************************************************** * Called when a template is selected from the New From Template popup menu. * Executes a New Alarm dialog, preset from the selected template. */ void MainWindow::slotNewFromTemplate(const KAEvent* tmplate) { KAlarm::editNewAlarm(tmplate, mListView); } /****************************************************************************** * Called when the New Template button is clicked to create a new template * based on the currently selected alarm. */ void MainWindow::slotNewTemplate() { KAEvent event = mListView->selectedEvent(); if (event.isValid()) KAlarm::editNewTemplate(&event, this); } /****************************************************************************** * Called when the Copy button is clicked to edit a copy of an existing alarm, * to add to the list. */ void MainWindow::slotCopy() { KAEvent event = mListView->selectedEvent(); if (event.isValid()) KAlarm::editNewAlarm(&event, this); } /****************************************************************************** * Called when the Modify button is clicked to edit the currently highlighted * alarm in the list. */ void MainWindow::slotModify() { KAEvent event = mListView->selectedEvent(); if (event.isValid()) KAlarm::editAlarm(&event, this); // edit alarm (view-only mode if archived or read-only) } /****************************************************************************** * Called when the Delete button is clicked to delete the currently highlighted * alarms in the list. */ void MainWindow::slotDelete(bool force) { QVector events = mListView->selectedEvents(); if (!force && Preferences::confirmAlarmDeletion()) { int n = events.count(); if (KAMessageBox::warningContinueCancel(this, i18ncp("@info", "Do you really want to delete the selected alarm?", "Do you really want to delete the %1 selected alarms?", n), i18ncp("@title:window", "Delete Alarm", "Delete Alarms", n), KGuiItem(i18nc("@action:button", "&Delete"), QStringLiteral("edit-delete")), KStandardGuiItem::cancel(), Preferences::CONFIRM_ALARM_DELETION) != KMessageBox::Continue) return; } // Remove any events which have just triggered, from the list to delete. Undo::EventList undos; for (int i = 0; i < events.count(); ) { Resource res = Resources::resourceForEvent(events[i].id()); if (!res.isValid()) events.remove(i); else undos.append(events[i++], res); } if (events.isEmpty()) qCDebug(KALARM_LOG) << "MainWindow::slotDelete: No alarms left to delete"; else { // Delete the events from the calendar and displays KAlarm::deleteEvents(events, true, this); Undo::saveDeletes(undos); } } /****************************************************************************** * Called when the Reactivate button is clicked to reinstate the currently * highlighted archived alarms in the list. */ void MainWindow::slotReactivate() { QVector events = mListView->selectedEvents(); mListView->clearSelection(); // Add the alarms to the displayed lists and to the calendar file Undo::EventList undos; QVector ineligibleIDs; KAlarm::reactivateEvents(events, ineligibleIDs, nullptr, this); // Create the undo list, excluding ineligible events for (int i = 0, end = events.count(); i < end; ++i) { if (!ineligibleIDs.contains(EventId(events[i]))) undos.append(events[i], Resources::resourceForEvent(events[i].id())); } Undo::saveReactivates(undos); } /****************************************************************************** * Called when the Enable/Disable button is clicked to enable or disable the * currently highlighted alarms in the list. */ void MainWindow::slotEnable() { bool enable = mActionEnableEnable; // save since changed in response to KAlarm::enableEvent() QVector events = mListView->selectedEvents(); QVector eventCopies; for (int i = 0, end = events.count(); i < end; ++i) eventCopies += events[i]; KAlarm::enableEvents(eventCopies, enable, this); slotSelection(); // update Enable/Disable action text } /****************************************************************************** * Called when the columns visible in the alarm list view have changed. */ void MainWindow::slotAlarmListColumnsChanged() { KConfigGroup config(KSharedConfig::openConfig(), VIEW_GROUP); config.writeEntry(SHOW_COLUMNS, mListView->columnsVisible()); config.sync(); } /****************************************************************************** * Called when the Show Archived Alarms menu item is selected or deselected. */ void MainWindow::slotShowArchived() { mShowArchived = !mShowArchived; mActionShowArchived->setChecked(mShowArchived); mActionShowArchived->setToolTip(mShowArchived ? i18nc("@info:tooltip", "Hide Archived Alarms") : i18nc("@info:tooltip", "Show Archived Alarms")); mListFilterModel->setEventTypeFilter(mShowArchived ? CalEvent::ACTIVE | CalEvent::ARCHIVED : CalEvent::ACTIVE); mListView->reset(); KConfigGroup config(KSharedConfig::openConfig(), VIEW_GROUP); config.writeEntry(SHOW_ARCHIVED_KEY, mShowArchived); config.sync(); } /****************************************************************************** * Called when the Spread Windows global shortcut is selected, to spread alarm * windows so that they are all visible. */ void MainWindow::slotSpreadWindowsShortcut() { mActionSpreadWindows->trigger(); } /****************************************************************************** * Called when the Wake From Suspend menu option is selected. */ void MainWindow::slotWakeFromSuspend() { (WakeFromSuspendDlg::create(this))->show(); } /****************************************************************************** * Called when the Import Alarms menu item is selected, to merge alarms from an * external calendar into the current calendars. */ void MainWindow::slotImportAlarms() { AlarmCalendar::resources()->importAlarms(this); } /****************************************************************************** * Called when the Export Alarms menu item is selected, to export the selected * alarms to an external calendar. */ void MainWindow::slotExportAlarms() { QVector events = mListView->selectedEvents(); if (!events.isEmpty()) { KAEvent::List evts = KAEvent::ptrList(events); AlarmCalendar::exportAlarms(evts, this); } } /****************************************************************************** * Called when the Import Birthdays menu item is selected, to display birthdays * from the address book for selection as alarms. */ void MainWindow::slotBirthdays() { // Use AutoQPointer to guard against crash on application exit while // the dialogue is still open. It prevents double deletion (both on // deletion of MainWindow, and on return from this function). AutoQPointer dlg = new BirthdayDlg(this); if (dlg->exec() == QDialog::Accepted) { QVector events = dlg->events(); if (!events.isEmpty()) { mListView->clearSelection(); // Add alarm to the displayed lists and to the calendar file KAlarm::UpdateResult status = KAlarm::addEvents(events, dlg, true, true); Undo::EventList undos; for (int i = 0, end = events.count(); i < end; ++i) undos.append(events[i], Resources::resourceForEvent(events[i].id())); Undo::saveAdds(undos, i18nc("@info", "Import birthdays")); if (status != KAlarm::UPDATE_FAILED) KAlarm::outputAlarmWarnings(dlg); } } } /****************************************************************************** * Called when the Templates menu item is selected, to display the alarm * template editing dialog. */ void MainWindow::slotTemplates() { if (!mTemplateDlg) { mTemplateDlg = TemplateDlg::create(this); enableTemplateMenuItem(false); // disable menu item in all windows connect(mTemplateDlg, &QDialog::finished, this, &MainWindow::slotTemplatesEnd); mTemplateDlg->show(); } } /****************************************************************************** * Called when the alarm template editing dialog has exited. */ void MainWindow::slotTemplatesEnd() { if (mTemplateDlg) { mTemplateDlg->deleteLater(); // this deletes the dialog once it is safe to do so mTemplateDlg = nullptr; enableTemplateMenuItem(true); // re-enable menu item in all windows } } /****************************************************************************** * Called when the Display System Tray Icon menu item is selected. */ void MainWindow::slotToggleTrayIcon() { theApp()->displayTrayIcon(!theApp()->trayIconDisplayed(), this); } /****************************************************************************** * Called when the Show Resource Selector menu item is selected. */ void MainWindow::slotToggleResourceSelector() { mShowResources = mActionToggleResourceSel->isChecked(); if (mShowResources) { if (mResourcesWidth <= 0) { mResourcesWidth = mResourceSelector->sizeHint().width(); mResourceSelector->resize(mResourcesWidth, mResourceSelector->height()); QList widths = mSplitter->sizes(); if (widths.count() == 1) { int listwidth = widths[0] - mSplitter->handleWidth() - mResourcesWidth; mListView->resize(listwidth, mListView->height()); widths.append(listwidth); widths[0] = mResourcesWidth; } mSplitter->setSizes(widths); } mResourceSelector->show(); } else mResourceSelector->hide(); KConfigGroup config(KSharedConfig::openConfig(), VIEW_GROUP); config.writeEntry(SHOW_RESOURCES_KEY, mShowResources); config.sync(); } /****************************************************************************** * Called when an error occurs in the resource calendar, to display a message. */ void MainWindow::showErrorMessage(const QString& msg) { KAMessageBox::error(this, msg); } /****************************************************************************** * Called when the system tray icon is created or destroyed. * Set the system tray icon menu text according to whether or not the system * tray icon is currently visible. */ void MainWindow::updateTrayIconAction() { mActionToggleTrayIcon->setEnabled(QSystemTrayIcon::isSystemTrayAvailable()); mActionToggleTrayIcon->setChecked(theApp()->trayIconDisplayed()); } /****************************************************************************** * Called when the active status of Find changes. */ void MainWindow::slotFindActive(bool active) { mActionFindNext->setEnabled(active); mActionFindPrev->setEnabled(active); } /****************************************************************************** * Called when the Undo action is selected. */ void MainWindow::slotUndo() { Undo::undo(this, KLocalizedString::removeAcceleratorMarker(mActionUndo->text())); } /****************************************************************************** * Called when the Redo action is selected. */ void MainWindow::slotRedo() { Undo::redo(this, KLocalizedString::removeAcceleratorMarker(mActionRedo->text())); } /****************************************************************************** * Called when an Undo item is selected. */ void MainWindow::slotUndoItem(QAction* action) { int id = mUndoMenuIds[action]; Undo::undo(id, this, Undo::actionText(Undo::UNDO, id)); } /****************************************************************************** * Called when a Redo item is selected. */ void MainWindow::slotRedoItem(QAction* action) { int id = mUndoMenuIds[action]; Undo::redo(id, this, Undo::actionText(Undo::REDO, id)); } /****************************************************************************** * Called when the Undo menu is about to show. * Populates the menu. */ void MainWindow::slotInitUndoMenu() { initUndoMenu(mActionUndo->menu(), Undo::UNDO); } /****************************************************************************** * Called when the Redo menu is about to show. * Populates the menu. */ void MainWindow::slotInitRedoMenu() { initUndoMenu(mActionRedo->menu(), Undo::REDO); } /****************************************************************************** * Populate the undo or redo menu. */ void MainWindow::initUndoMenu(QMenu* menu, Undo::Type type) { menu->clear(); mUndoMenuIds.clear(); const QString& action = (type == Undo::UNDO) ? undoTextStripped : redoTextStripped; QList ids = Undo::ids(type); for (int i = 0, end = ids.count(); i < end; ++i) { int id = ids[i]; QString actText = Undo::actionText(type, id); QString descrip = Undo::description(type, id); QString text = descrip.isEmpty() ? i18nc("@action Undo/Redo [action]", "%1 %2", action, actText) : i18nc("@action Undo [action]: message", "%1 %2: %3", action, actText, descrip); QAction* act = menu->addAction(text); mUndoMenuIds[act] = id; } } /****************************************************************************** * Called when the status of the Undo or Redo list changes. * Change the Undo or Redo text to include the action which would be undone/redone. */ void MainWindow::slotUndoStatus(const QString& undo, const QString& redo) { if (undo.isNull()) { mActionUndo->setEnabled(false); mActionUndo->setText(undoText); } else { mActionUndo->setEnabled(true); mActionUndo->setText(QStringLiteral("%1 %2").arg(undoText, undo)); } if (redo.isNull()) { mActionRedo->setEnabled(false); mActionRedo->setText(redoText); } else { mActionRedo->setEnabled(true); mActionRedo->setText(QStringLiteral("%1 %2").arg(redoText, redo)); } } /****************************************************************************** * Called when the Refresh Alarms menu item is selected. */ void MainWindow::slotRefreshAlarms() { KAlarm::refreshAlarms(); } /****************************************************************************** * Called when the "Configure KAlarm" menu item is selected. */ void MainWindow::slotPreferences() { KAlarmPrefDlg::display(); } /****************************************************************************** * Called when the Configure Keys menu item is selected. */ void MainWindow::slotConfigureKeys() { KShortcutsDialog::configure(actionCollection(), KShortcutsEditor::LetterShortcutsAllowed, this); } /****************************************************************************** * Called when the Configure Toolbars menu item is selected. */ void MainWindow::slotConfigureToolbar() { KConfigGroup grp(KSharedConfig::openConfig()->group(WINDOW_NAME)); saveMainWindowSettings(grp); KEditToolBar dlg(factory()); connect(&dlg, &KEditToolBar::newToolBarConfig, this, &MainWindow::slotNewToolbarConfig); dlg.exec(); } /****************************************************************************** * Called when OK or Apply is clicked in the Configure Toolbars dialog, to save * the new configuration. */ void MainWindow::slotNewToolbarConfig() { createGUI(UI_FILE); applyMainWindowSettings(KSharedConfig::openConfig()->group(WINDOW_NAME)); } /****************************************************************************** * Called when the Quit menu item is selected. * Note that this must be called by the event loop, not directly from the menu * item, since otherwise the window will be deleted while still processing the * menu, resulting in a crash. */ void MainWindow::slotQuit() { theApp()->doQuit(this); } /****************************************************************************** * Called when the user or the session manager attempts to close the window. */ void MainWindow::closeEvent(QCloseEvent* ce) { if (!qApp->isSavingSession()) { // The user (not the session manager) wants to close the window. if (isTrayParent()) { // It's the parent window of the system tray icon, so just hide // it to prevent the system tray icon closing. hide(); theApp()->quitIf(); ce->ignore(); return; } } ce->accept(); } /****************************************************************************** * Called when the drag cursor enters a main or system tray window, to accept * or reject the dragged object. */ void MainWindow::executeDragEnterEvent(QDragEnterEvent* e) { const QMimeData* data = e->mimeData(); bool accept = ICalDrag::canDecode(data) ? !e->source() // don't accept "text/calendar" objects from this application : data->hasText() || data->hasUrls() || KPIM::MailList::canDecode(data); if (accept) e->acceptProposedAction(); } /****************************************************************************** * Called when an object is dropped on the window. * If the object is recognised, the edit alarm dialog is opened appropriately. */ void MainWindow::dropEvent(QDropEvent* e) { executeDropEvent(this, e); } static QString getMailHeader(const char* header, KMime::Content& content) { KMime::Headers::Base* hd = content.headerByType(header); return hd ? hd->asUnicodeString() : QString(); } /****************************************************************************** * Called when an object is dropped on a main or system tray window, to * evaluate the action required and extract the text. */ void MainWindow::executeDropEvent(MainWindow* win, QDropEvent* e) { qCDebug(KALARM_LOG) << "MainWindow::executeDropEvent: Formats:" << e->mimeData()->formats(); const QMimeData* data = e->mimeData(); KAEvent::SubAction action = KAEvent::MESSAGE; QByteArray bytes; AlarmText alarmText; KPIM::MailList mailList; QList urls; MemoryCalendar::Ptr calendar(new MemoryCalendar(Preferences::timeSpecAsZone())); #ifndef NDEBUG QString fmts = data->formats().join(QLatin1String(", ")); qCDebug(KALARM_LOG) << "MainWindow::executeDropEvent:" << fmts; #endif /* The order of the tests below matters, since some dropped objects * provide more than one mime type. * Don't change them without careful thought !! */ if (!(bytes = data->data(QStringLiteral("message/rfc822"))).isEmpty()) { // Email message(s). Ignore all but the first. qCDebug(KALARM_LOG) << "MainWindow::executeDropEvent: email"; KMime::Content content; content.setContent(bytes); content.parse(); QString body; if (content.textContent()) body = content.textContent()->decodedText(true, true); // strip trailing newlines & spaces unsigned long sernum = 0; if (KPIM::MailList::canDecode(data)) { // Get its KMail serial number to allow the KMail message // to be called up from the alarm message window. mailList = KPIM::MailList::fromMimeData(data); if (!mailList.isEmpty()) sernum = mailList.at(0).serialNumber(); } alarmText.setEmail(getMailHeader("To", content), getMailHeader("From", content), getMailHeader("Cc", content), getMailHeader("Date", content), getMailHeader("Subject", content), body, sernum); } else if (KPIM::MailList::canDecode(data)) { mailList = KPIM::MailList::fromMimeData(data); // KMail message(s). Ignore all but the first. qCDebug(KALARM_LOG) << "MainWindow::executeDropEvent: KMail_list"; if (mailList.isEmpty()) return; const KPIM::MailSummary& summary = mailList.at(0); QDateTime dt; dt.setSecsSinceEpoch(summary.date()); const QString body = KAMail::getMailBody(summary.serialNumber()); alarmText.setEmail(summary.to(), summary.from(), QString(), QLocale().toString(dt), summary.subject(), body, summary.serialNumber()); } else if (ICalDrag::fromMimeData(data, calendar)) { // iCalendar - If events are included, use the first event qCDebug(KALARM_LOG) << "MainWindow::executeDropEvent: iCalendar"; const Event::List events = calendar->rawEvents(); if (!events.isEmpty()) { Event::Ptr event = events[0]; if (event->alarms().isEmpty()) { Alarm::Ptr alarm = event->newAlarm(); alarm->setEnabled(true); alarm->setTime(event->dtStart()); alarm->setDisplayAlarm(event->summary().isEmpty() ? event->description() : event->summary()); event->addAlarm(alarm); } KAEvent ev(event); KAlarm::editNewAlarm(&ev, win); return; } // If todos are included, use the first todo const Todo::List todos = calendar->rawTodos(); if (!todos.isEmpty()) { Todo::Ptr todo = todos[0]; alarmText.setTodo(todo); KADateTime start(todo->dtStart(true)); KADateTime due(todo->dtDue(true)); bool haveBothTimes = false; if (todo->hasDueDate()) { if (start.isValid()) haveBothTimes = true; else start = due; } if (todo->allDay()) start.setDateOnly(true); KAEvent::Flags flags = KAEvent::DEFAULT_FONT; if (start.isDateOnly()) flags |= KAEvent::ANY_TIME; KAEvent ev(start, alarmText.displayText(), Preferences::defaultBgColour(), Preferences::defaultFgColour(), QFont(), KAEvent::MESSAGE, 0, flags, true); ev.startChanges(); if (todo->recurs()) { ev.setRecurrence(*todo->recurrence()); ev.setNextOccurrence(KADateTime::currentUtcDateTime()); } const Alarm::List alarms = todo->alarms(); if (!alarms.isEmpty() && alarms[0]->type() == Alarm::Display) { // A display alarm represents a reminder int offset = 0; if (alarms[0]->hasStartOffset()) offset = alarms[0]->startOffset().asSeconds(); else if (alarms[0]->hasEndOffset()) { offset = alarms[0]->endOffset().asSeconds(); if (haveBothTimes) { // Get offset relative to start time instead of due time offset += start.secsTo(due); } } if (offset / 60) ev.setReminder(-offset / 60, false); } ev.endChanges(); KAlarm::editNewAlarm(&ev, win); } return; } else if (!(urls = data->urls()).isEmpty()) { const QUrl& url(urls.at(0)); const Akonadi::Item item = Akonadi::Item::fromUrl(url); if (item.isValid()) { // It's an Akonadi item qCDebug(KALARM_LOG) << "MainWindow::executeDropEvent: Akonadi item" << item.id(); if (QUrlQuery(url).queryItemValue(QStringLiteral("type")) == QLatin1String("message/rfc822")) { // It's an email held in Akonadi qCDebug(KALARM_LOG) << "MainWindow::executeDropEvent: Akonadi email"; Akonadi::ItemFetchJob* job = new Akonadi::ItemFetchJob(item); job->fetchScope().fetchFullPayload(); Akonadi::Item::List items; if (job->exec()) items = job->items(); if (items.isEmpty()) { qCWarning(KALARM_LOG) << "MainWindow::executeDropEvent: Akonadi item" << item.id() << "not found"; return; } const Akonadi::Item& it = items.at(0); if (!it.isValid() || !it.hasPayload()) { qCWarning(KALARM_LOG) << "MainWindow::executeDropEvent: invalid email"; return; } KMime::Message::Ptr message = it.payload(); QString body; if (message->textContent()) body = message->textContent()->decodedText(true, true); // strip trailing newlines & spaces alarmText.setEmail(getMailHeader("To", *message), getMailHeader("From", *message), getMailHeader("Cc", *message), getMailHeader("Date", *message), getMailHeader("Subject", *message), body, it.id()); } } else { qCDebug(KALARM_LOG) << "MainWindow::executeDropEvent: URL"; // Try to find the mime type of the file, without downloading a remote file QMimeDatabase mimeDb; const QString mimeTypeName = mimeDb.mimeTypeForUrl(url).name(); action = mimeTypeName.startsWith(QLatin1String("audio/")) ? KAEvent::AUDIO : KAEvent::FILE; alarmText.setText(url.toDisplayString()); } } if (alarmText.isEmpty()) { if (data->hasText()) { const QString text = data->text(); qCDebug(KALARM_LOG) << "MainWindow::executeDropEvent: text"; alarmText.setText(text); } else return; } if (!alarmText.isEmpty()) { if (action == KAEvent::MESSAGE && (alarmText.isEmail() || alarmText.isScript())) { // If the alarm text could be interpreted as an email or command script, // prompt for which type of alarm to create. QStringList types; types += i18nc("@item:inlistbox", "Display Alarm"); if (alarmText.isEmail()) types += i18nc("@item:inlistbox", "Email Alarm"); else if (alarmText.isScript()) types += i18nc("@item:inlistbox", "Command Alarm"); bool ok = false; QString type = QInputDialog::getItem(mainMainWindow(), i18nc("@title:window", "Alarm Type"), i18nc("@info", "Choose alarm type to create:"), types, 0, false, &ok); if (!ok) return; // user didn't press OK int i = types.indexOf(type); if (i == 1) action = alarmText.isEmail() ? KAEvent::EMAIL : KAEvent::COMMAND; } KAlarm::editNewAlarm(action, win, &alarmText); } } /****************************************************************************** * Called when the status of a calendar has changed. * Enable or disable actions appropriately. */ void MainWindow::slotCalendarStatusChanged() { // Find whether there are any writable calendars bool active = !Resources::enabledResources(CalEvent::ACTIVE, true).isEmpty(); bool templat = !Resources::enabledResources(CalEvent::TEMPLATE, true).isEmpty(); for (int i = 0, end = mWindowList.count(); i < end; ++i) { MainWindow* w = mWindowList[i]; w->mActionImportAlarms->setEnabled(active || templat); w->mActionImportBirthdays->setEnabled(active); w->mActionCreateTemplate->setEnabled(templat); // Note: w->mActionNew enabled status is set in the NewAlarmAction class. w->slotSelection(); } } /****************************************************************************** * Called when the selected items in the ListView change. * Enables the actions appropriately. */ void MainWindow::slotSelection() { // Find which events have been selected QVector events = mListView->selectedEvents(); int count = events.count(); if (!count) { selectionCleared(); // disable actions Q_EMIT selectionChanged(); return; } // Find whether there are any writable resources bool active = mActionNew->isEnabled(); bool readOnly = false; bool allArchived = true; bool enableReactivate = true; bool enableEnableDisable = true; bool enableEnable = false; bool enableDisable = false; AlarmCalendar* resources = AlarmCalendar::resources(); const KADateTime now = KADateTime::currentUtcDateTime(); for (int i = 0; i < count; ++i) { KAEvent* ev = resources->event(EventId(events.at(i))); // get up-to-date status KAEvent* event = ev ? ev : &events[i]; bool expired = event->expired(); if (!expired) allArchived = false; if (resources->eventReadOnly(event->id())) readOnly = true; if (enableReactivate && (!expired || !event->occursAfter(now, true))) enableReactivate = false; if (enableEnableDisable) { if (expired) enableEnableDisable = enableEnable = enableDisable = false; else { if (!enableEnable && !event->enabled()) enableEnable = true; if (!enableDisable && event->enabled()) enableDisable = true; } } } qCDebug(KALARM_LOG) << "MainWindow::slotSelection: true"; mActionCreateTemplate->setEnabled((count == 1) && !Resources::enabledResources(CalEvent::TEMPLATE, true).isEmpty()); mActionExportAlarms->setEnabled(true); mActionExport->setEnabled(true); mActionCopy->setEnabled(active && count == 1); mActionModify->setEnabled(count == 1); mActionDelete->setEnabled(!readOnly && (active || allArchived)); mActionReactivate->setEnabled(active && enableReactivate); mActionEnable->setEnabled(active && !readOnly && (enableEnable || enableDisable)); if (enableEnable || enableDisable) setEnableText(enableEnable); Q_EMIT selectionChanged(); } /****************************************************************************** * Called when a context menu is requested in the ListView. * Displays a context menu to modify or delete the selected item. */ void MainWindow::slotContextMenuRequested(const QPoint& globalPos) { qCDebug(KALARM_LOG) << "MainWindow::slotContextMenuRequested"; if (mContextMenu) mContextMenu->popup(globalPos); } /****************************************************************************** * Disables actions when no item is selected. */ void MainWindow::selectionCleared() { mActionCreateTemplate->setEnabled(false); mActionExportAlarms->setEnabled(false); mActionExport->setEnabled(false); mActionCopy->setEnabled(false); mActionModify->setEnabled(false); mActionDelete->setEnabled(false); mActionReactivate->setEnabled(false); mActionEnable->setEnabled(false); } /****************************************************************************** * Set the text of the Enable/Disable menu action. */ void MainWindow::setEnableText(bool enable) { mActionEnableEnable = enable; mActionEnable->setText(enable ? i18nc("@action", "Ena&ble") : i18nc("@action", "Disa&ble")); } /****************************************************************************** * Display or hide the specified main window. * This should only be called when the application doesn't run in the system tray. */ MainWindow* MainWindow::toggleWindow(MainWindow* win) { if (win && mWindowList.indexOf(win) != -1) { // A window is specified (and it exists) if (win->isVisible()) { // The window is visible, so close it win->close(); return nullptr; } else { // The window is hidden, so display it win->hide(); // in case it's on a different desktop win->setWindowState(win->windowState() & ~Qt::WindowMinimized); win->raise(); win->activateWindow(); return win; } } // No window is specified, or the window doesn't exist. Open a new one. win = create(); win->show(); return win; } /****************************************************************************** * Called when the Edit button is clicked in an alarm message window. * This controls the alarm edit dialog created by the alarm window, and allows * it to remain unaffected by the alarm window closing. * See MessageWin::slotEdit() for more information. */ void MainWindow::editAlarm(EditAlarmDlg* dlg, const KAEvent& event) { mEditAlarmMap[dlg] = event; connect(dlg, &KEditToolBar::accepted, this, &MainWindow::editAlarmOk); connect(dlg, &KEditToolBar::destroyed, this, &MainWindow::editAlarmDeleted); dlg->setAttribute(Qt::WA_DeleteOnClose, true); // ensure no memory leaks dlg->show(); } /****************************************************************************** * Called when OK is clicked in the alarm edit dialog shown by editAlarm(). * Updates the event which has been edited. */ void MainWindow::editAlarmOk() { EditAlarmDlg* dlg = qobject_cast(sender()); if (!dlg) return; QMap::Iterator it = mEditAlarmMap.find(dlg); if (it == mEditAlarmMap.end()) return; KAEvent event = it.value(); mEditAlarmMap.erase(it); if (!event.isValid()) return; if (dlg->result() != QDialog::Accepted) return; Resource res = Resources::resourceForEvent(event.id()); KAlarm::updateEditedAlarm(dlg, event, res); } /****************************************************************************** * Called when the alarm edit dialog shown by editAlarm() is deleted. * Removes the dialog from the pending list. */ void MainWindow::editAlarmDeleted(QObject* obj) { mEditAlarmMap.remove(static_cast(obj)); } // vim: et sw=4: diff --git a/src/messagewin.cpp b/src/messagewin.cpp index 48b21115..9392b8b1 100644 --- a/src/messagewin.cpp +++ b/src/messagewin.cpp @@ -1,2416 +1,2416 @@ /* * messagewin.cpp - displays an alarm message * Program: kalarm * Copyright © 2001-2019 David Jarvie * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "messagewin.h" #include "messagewin_p.h" #include "config-kalarm.h" #include "alarmcalendar.h" #include "autoqpointer.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 "resources/resources.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 #if KDEPIM_HAVE_X11 -#include +#include #include #include #include #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace KCalendarCore; 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()) , 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()) , mAkonadiItemId(event->akonadiItemId()) , mCommandError(event->commandError()) , mRestoreHeight(0) , mAudioRepeatPause(event->repeatSoundPause()) , mConfirmAck(event->confirmAck()) , mNoDefer(true) , mInvalid(false) , mEvent(*event) , mOriginalEvent(*event) , mResource(Resources::resourceForEvent(mEventId.eventId())) , mAlwaysHide(flags & ALWAYS_HIDE) , mNoPostAction(alarm.type() & KAAlarm::REMINDER_ALARM) , mBeep(event->beep()) , mSpeak(event->speak()) , mRescheduleEvent(!(flags & NO_RESCHEDULE)) { 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(mEventId.eventId()); 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) , mEventId(*event) , mAlarmType(KAAlarm::MAIN_ALARM) , mAction(event->actionSubType()) , mAkonadiItemId(-1) , mCommandError(KAEvent::CMD_NO_ERROR) , mErrorMsgs(errmsgs) , mDontShowAgain(dontShowAgain) , mRestoreHeight(0) , mConfirmAck(false) , mShowEdit(false) , mNoDefer(true) , mInvalid(false) , mEvent(*event) , mOriginalEvent(*event) , mErrorWindow(true) , mNoPostAction(true) { 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) { qCDebug(KALARM_LOG) << "MessageWin:" << (void*)this << "restore"; setAttribute(WidgetFlags); setWindowModality(Qt::WindowModal); setObjectName(QStringLiteral("RestoredMsgWin")); // used by LikeBack getWorkAreaAndModal(); mWindowList.append(this); } /****************************************************************************** * Destructor. Perform any post-alarm actions before tidying up. */ MessageWin::~MessageWin() { qCDebug(KALARM_LOG) << "~MessageWin" << (void*)this << mEventId; if (AudioThread::mAudioOwner == this && !mAudioThread.isNull()) mAudioThread->quit(); mErrorMessages.remove(mEventId); mWindowList.removeAll(this); delete mTempFile; if (!mRecreating) { if (!mNoPostAction && !mEvent.postAction().isEmpty()) theApp()->alarmCompleted(mEvent); if (!instanceCount(true)) theApp()->quitIf(); // no visible windows remain - check whether to quit } } /****************************************************************************** * Construct the message window. */ void MessageWin::initView() { const bool reminder = (!mErrorWindow && (mAlarmType & KAAlarm::REMINDER_ALARM)); const int leading = fontMetrics().leading(); setCaption((mAlarmType & KAAlarm::REMINDER_ALARM) ? i18nc("@title:window", "Reminder") : i18nc("@title:window", "Message")); QWidget* topWidget = new QWidget(this); setCentralWidget(topWidget); QVBoxLayout* topLayout = new QVBoxLayout(topWidget); int dcm = style()->pixelMetric(QStyle::PM_DefaultChildMargin); topLayout->setContentsMargins(dcm, dcm, dcm, dcm); topLayout->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); QPalette labelPalette = palette(); labelPalette.setColor(backgroundRole(), labelPalette.color(QPalette::Window)); // Show the alarm date/time, together with a reminder text where appropriate. // Alarm date/time: display time zone if not local time zone. mTimeLabel = new QLabel(topWidget); mTimeLabel->setText(dateTimeToDisplay()); mTimeLabel->setFrameStyle(QFrame::StyledPanel); mTimeLabel->setPalette(labelPalette); mTimeLabel->setAutoFillBackground(true); topLayout->addWidget(mTimeLabel, 0, Qt::AlignHCenter); mTimeLabel->setWhatsThis(i18nc("@info:whatsthis", "The scheduled date/time for the message (as opposed to the actual time of display).")); if (mDateTime.isValid()) { // Reminder if (reminder) { // Create a label "time\nReminder" by inserting the time at the // start of the translated string, allowing for possible HTML tags // enclosing "Reminder". QString s = i18nc("@info", "Reminder"); QRegExp re(QStringLiteral("^(<[^>]+>)*")); re.indexIn(s); s.insert(re.matchedLength(), mTimeLabel->text() + QLatin1String("
")); mTimeLabel->setText(s); mTimeLabel->setAlignment(Qt::AlignHCenter); } } else mTimeLabel->hide(); if (!mErrorWindow) { // It's a normal alarm message window switch (mAction) { case KAEvent::FILE: { // Display the file name KSqueezedTextLabel* label = new KSqueezedTextLabel(mMessage, topWidget); label->setFrameStyle(QFrame::StyledPanel); label->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard); label->setPalette(labelPalette); label->setAutoFillBackground(true); label->setWhatsThis(i18nc("@info:whatsthis", "The file whose contents are displayed below")); topLayout->addWidget(label, 0, Qt::AlignHCenter); // Display contents of file const QUrl url = QUrl::fromUserInput(mMessage, QString(), QUrl::AssumeLocalFile); auto statJob = KIO::stat(url, KIO::StatJob::SourceSide, 0, KIO::HideProgressInfo); const bool exists = statJob->exec(); const bool isDir = statJob->statResult().isDir(); bool opened = false; if (exists && !isDir) { auto job = KIO::storedGet(url); KJobWidgets::setWindow(job, MainWindow::mainMainWindow()); if (job->exec()) { opened = true; const QByteArray data = job->data(); QMimeDatabase db; QMimeType mime = db.mimeTypeForUrl(url); if (mime.name() == QLatin1String("application/octet-stream")) mime = db.mimeTypeForData(mTempFile); const KAlarm::FileType fileType = KAlarm::fileType(mime); switch (fileType) { case KAlarm::Image: case KAlarm::TextFormatted: delete mTempFile; mTempFile = new QTemporaryFile; mTempFile->open(); mTempFile->write(data); break; default: break; } 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); switch (fileType) { case KAlarm::Image: view->setHtml(QLatin1String("
fileName() + QLatin1String("\">
")); mTempFile->close(); // keep the file available to be displayed break; case KAlarm::TextFormatted: view->QTextBrowser::setSource(QUrl::fromLocalFile(mTempFile->fileName())); //krazy:exclude=qclasses delete mTempFile; mTempFile = nullptr; break; default: view->setPlainText(QString::fromUtf8(data)); break; } view->setMinimumSize(view->sizeHint()); topLayout->addWidget(view); // Set the default size to 20 lines square. // Note that after the first file has been displayed, this size // is overridden by the user-set default stored in the config file. // So there is no need to calculate an accurate size. int h = 20*view->fontMetrics().lineSpacing() + 2*view->frameWidth(); view->resize(QSize(h, h).expandedTo(view->sizeHint())); view->setWhatsThis(i18nc("@info:whatsthis", "The contents of the file to be displayed")); } } if (!exists || isDir || !opened) { mErrorMsgs += isDir ? i18nc("@info", "File is a folder") : exists ? i18nc("@info", "Failed to open file") : i18nc("@info", "File not found"); } break; } case KAEvent::MESSAGE: { // Message label // Using MessageText instead of QLabel allows scrolling and mouse copying MessageText* text = new MessageText(topWidget); text->setAutoFillBackground(true); text->setBackgroundColour(mBgColour); text->setTextColor(mFgColour); text->setCurrentFont(mFont); text->insertPlainText(mMessage); const int lineSpacing = text->fontMetrics().lineSpacing(); const QSize s = text->sizeHint(); const int h = s.height(); text->setMaximumHeight(h + text->scrollBarHeight()); text->setMinimumHeight(qMin(h, lineSpacing*4)); text->setMaximumWidth(s.width() + text->scrollBarWidth()); text->setWhatsThis(i18nc("@info:whatsthis", "The alarm message")); const int vspace = lineSpacing/2; const int hspace = lineSpacing - style()->pixelMetric(QStyle::PM_DefaultChildMargin); topLayout->addSpacing(vspace); topLayout->addStretch(); // Don't include any horizontal margins if message is 2/3 screen width if (text->sizeHint().width() >= KAlarm::desktopWorkArea(mScreenNumber).width()*2/3) topLayout->addWidget(text, 1, Qt::AlignHCenter); else { QHBoxLayout* layout = new QHBoxLayout(); layout->addSpacing(hspace); layout->addWidget(text, 1, Qt::AlignHCenter); layout->addSpacing(hspace); topLayout->addLayout(layout); } if (!reminder) topLayout->addStretch(); break; } case KAEvent::COMMAND: { mCommandText = new MessageText(topWidget); mCommandText->setBackgroundColour(mBgColour); mCommandText->setTextColor(mFgColour); mCommandText->setCurrentFont(mFont); topLayout->addWidget(mCommandText); mCommandText->setWhatsThis(i18nc("@info:whatsthis", "The output of the alarm's command")); theApp()->execCommandAlarm(mEvent, mEvent.alarm(mAlarmType), this, SLOT(readProcessOutput(ShellProcess*))); break; } case KAEvent::EMAIL: default: break; } if (reminder && mEvent.reminderMinutes() > 0) { // Advance reminder: show remaining time until the actual alarm mRemainingText = new QLabel(topWidget); mRemainingText->setFrameStyle(QFrame::Box | QFrame::Raised); mRemainingText->setContentsMargins(leading, leading, leading, leading); mRemainingText->setPalette(labelPalette); mRemainingText->setAutoFillBackground(true); if (mDateTime.isDateOnly() || KADateTime::currentLocalDate().daysTo(mDateTime.date()) > 0) { setRemainingTextDay(); MidnightTimer::connect(this, SLOT(setRemainingTextDay())); // update every day } else { setRemainingTextMinute(); MinuteTimer::connect(this, SLOT(setRemainingTextMinute())); // update every minute } topLayout->addWidget(mRemainingText, 0, Qt::AlignHCenter); topLayout->addSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); topLayout->addStretch(); } } else { // It's an error message switch (mAction) { case KAEvent::EMAIL: { // Display the email addresses and subject. QFrame* frame = new QFrame(topWidget); frame->setFrameStyle(QFrame::Box | QFrame::Raised); frame->setWhatsThis(i18nc("@info:whatsthis", "The email to send")); topLayout->addWidget(frame, 0, Qt::AlignHCenter); QGridLayout* grid = new QGridLayout(frame); grid->setContentsMargins(dcm, dcm, dcm, dcm); grid->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); QLabel* label = new QLabel(i18nc("@info Email addressee", "To:"), frame); label->setFixedSize(label->sizeHint()); grid->addWidget(label, 0, 0, Qt::AlignLeft); label = new QLabel(mEvent.emailAddresses(QStringLiteral("\n")), frame); label->setFixedSize(label->sizeHint()); grid->addWidget(label, 0, 1, Qt::AlignLeft); label = new QLabel(i18nc("@info Email subject", "Subject:"), frame); label->setFixedSize(label->sizeHint()); grid->addWidget(label, 1, 0, Qt::AlignLeft); label = new QLabel(mEvent.emailSubject(), frame); label->setFixedSize(label->sizeHint()); grid->addWidget(label, 1, 1, Qt::AlignLeft); break; } case KAEvent::COMMAND: case KAEvent::FILE: case KAEvent::MESSAGE: default: // Just display the error message strings break; } } if (!mErrorMsgs.count()) { topWidget->setAutoFillBackground(true); QPalette palette = topWidget->palette(); palette.setColor(topWidget->backgroundRole(), mBgColour); topWidget->setPalette(palette); } else { setCaption(i18nc("@title:window", "Error")); QHBoxLayout* layout = new QHBoxLayout(); int m = 2 * dcm; layout->setContentsMargins(m, m, m, m); layout->addStretch(); topLayout->addLayout(layout); QLabel* label = new QLabel(topWidget); label->setPixmap(QIcon::fromTheme(QStringLiteral("dialog-error")).pixmap(IconSize(KIconLoader::Desktop), IconSize(KIconLoader::Desktop))); label->setFixedSize(label->sizeHint()); layout->addWidget(label, 0, Qt::AlignRight); QVBoxLayout* vlayout = new QVBoxLayout(); layout->addLayout(vlayout); for (QStringList::ConstIterator it = mErrorMsgs.constBegin(); it != mErrorMsgs.constEnd(); ++it) { label = new QLabel(*it, topWidget); label->setFixedSize(label->sizeHint()); vlayout->addWidget(label, 0, Qt::AlignLeft); } layout->addStretch(); if (!mDontShowAgain.isEmpty()) { mDontShowAgainCheck = new QCheckBox(i18nc("@option:check", "Do not display this error message again for this alarm"), topWidget); mDontShowAgainCheck->setFixedSize(mDontShowAgainCheck->sizeHint()); topLayout->addWidget(mDontShowAgainCheck, 0, Qt::AlignLeft); } } QGridLayout* grid = new QGridLayout(); grid->setColumnStretch(0, 1); // keep the buttons right-adjusted in the window topLayout->addLayout(grid); int gridIndex = 1; // Close button mOkButton = new PushButton(KStandardGuiItem::close(), topWidget); // Prevent accidental acknowledgement of the message if the user is typing when the window appears mOkButton->clearFocus(); mOkButton->setFocusPolicy(Qt::ClickFocus); // don't allow keyboard selection mOkButton->setFixedSize(mOkButton->sizeHint()); connect(mOkButton, &QAbstractButton::clicked, this, &MessageWin::slotOk); grid->addWidget(mOkButton, 0, gridIndex++, Qt::AlignHCenter); mOkButton->setWhatsThis(i18nc("@info:whatsthis", "Acknowledge the alarm")); if (mShowEdit) { // Edit button mEditButton = new PushButton(i18nc("@action:button", "&Edit..."), topWidget); mEditButton->setFocusPolicy(Qt::ClickFocus); // don't allow keyboard selection mEditButton->setFixedSize(mEditButton->sizeHint()); connect(mEditButton, &QAbstractButton::clicked, this, &MessageWin::slotEdit); grid->addWidget(mEditButton, 0, gridIndex++, Qt::AlignHCenter); mEditButton->setWhatsThis(i18nc("@info:whatsthis", "Edit the alarm.")); } // Defer button mDeferButton = new PushButton(i18nc("@action:button", "&Defer..."), topWidget); mDeferButton->setFocusPolicy(Qt::ClickFocus); // don't allow keyboard selection mDeferButton->setFixedSize(mDeferButton->sizeHint()); connect(mDeferButton, &QAbstractButton::clicked, this, &MessageWin::slotDefer); grid->addWidget(mDeferButton, 0, gridIndex++, Qt::AlignHCenter); mDeferButton->setWhatsThis(xi18nc("@info:whatsthis", "Defer the alarm until later." "You will be prompted to specify when the alarm should be redisplayed.")); if (mNoDefer) mDeferButton->hide(); else setDeferralLimit(mEvent); // ensure that button is disabled when alarm can't be deferred any more KIconLoader iconLoader; if (!mAudioFile.isEmpty() && (mVolume || mFadeVolume > 0)) { // Silence button to stop sound repetition const QPixmap pixmap = iconLoader.loadIcon(QStringLiteral("media-playback-stop"), KIconLoader::MainToolbar); 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); } if (mAkonadiItemId >= 0) { // 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::KeepAbove; if (modal) wstate |= NET::Modal; WId winid = winId(); KWindowSystem::setState(winid, wstate); KWindowSystem::setOnAllDesktops(winid, true); mInitialised = true; // the window's widgets have been created } /****************************************************************************** * Return the number of message windows, optionally excluding always-hidden ones. */ int MessageWin::instanceCount(bool excludeAlwaysHidden) { int count = mWindowList.count(); if (excludeAlwaysHidden) { for (MessageWin* win : qAsConst(mWindowList)) { if (win->mAlwaysHide) --count; } } return count; } bool MessageWin::hasDefer() const { return mDeferButton && mDeferButton->isVisible(); } /****************************************************************************** * Show the Defer button when it was previously hidden. */ void MessageWin::showDefer() { if (mDeferButton) { mNoDefer = false; mDeferButton->show(); setDeferralLimit(mEvent); // ensure that button is disabled when alarm can't be deferred any more resize(sizeHint()); } } /****************************************************************************** * Convert a reminder window into a normal alarm window. */ void MessageWin::cancelReminder(const KAEvent& event, const KAAlarm& alarm) { if (!mInitialised) return; mDateTime = alarm.dateTime(true); mNoPostAction = false; mAlarmType = alarm.type(); if (event.autoClose()) mCloseTime = alarm.dateTime().effectiveKDateTime().toUtc().qDateTime().addSecs(event.lateCancel() * 60); setCaption(i18nc("@title:window", "Message")); mTimeLabel->setText(dateTimeToDisplay()); if (mRemainingText) mRemainingText->hide(); MidnightTimer::disconnect(this, SLOT(setRemainingTextDay())); MinuteTimer::disconnect(this, SLOT(setRemainingTextMinute())); setMinimumHeight(0); centralWidget()->layout()->activate(); setMinimumHeight(sizeHint().height()); resize(sizeHint()); } /****************************************************************************** * Show the alarm's trigger time. * This is assumed to have previously been hidden. */ void MessageWin::showDateTime(const KAEvent& event, const KAAlarm& alarm) { if (!mTimeLabel) return; mDateTime = (alarm.type() & KAAlarm::REMINDER_ALARM) ? event.mainDateTime(true) : alarm.dateTime(true); if (mDateTime.isValid()) { mTimeLabel->setText(dateTimeToDisplay()); mTimeLabel->show(); } } /****************************************************************************** * Get the trigger time to display. */ QString MessageWin::dateTimeToDisplay() { QString tm; if (mDateTime.isValid()) { if (mDateTime.isDateOnly()) tm = QLocale().toString(mDateTime.date(), QLocale::ShortFormat); else { bool showZone = false; if (mDateTime.timeType() == KADateTime::UTC || (mDateTime.timeType() == KADateTime::TimeZone && !mDateTime.isLocalZone())) { // Display time zone abbreviation if it's different from the local // zone. Note that the iCalendar time zone might represent the local // time zone in a slightly different way from the system time zone, // so the zone comparison above might not produce the desired result. const QString tz = mDateTime.kDateTime().toString(QStringLiteral("%Z")); KADateTime local = mDateTime.kDateTime(); local.setTimeSpec(KADateTime::Spec::LocalZone()); showZone = (local.toString(QStringLiteral("%Z")) != tz); } const QDateTime dt = mDateTime.qDateTime(); tm = QLocale().toString(dt, QLocale::ShortFormat); if (showZone) tm += QLatin1Char(' ') + mDateTime.timeZone().displayName(dt, QTimeZone::ShortName, QLocale()); } } return tm; } /****************************************************************************** * Set the remaining time text in a reminder window. * Called at the start of every day (at the user-defined start-of-day time). */ void MessageWin::setRemainingTextDay() { QString text; const int days = KADateTime::currentLocalDate().daysTo(mDateTime.date()); if (days <= 0 && !mDateTime.isDateOnly()) { // The alarm is due today, so start refreshing every minute MidnightTimer::disconnect(this, SLOT(setRemainingTextDay())); setRemainingTextMinute(); MinuteTimer::connect(this, SLOT(setRemainingTextMinute())); // update every minute } else { if (days <= 0) text = i18nc("@info", "Today"); else if (days % 7) text = i18ncp("@info", "Tomorrow", "in %1 days' time", days); else text = i18ncp("@info", "in 1 week's time", "in %1 weeks' time", days/7); } mRemainingText->setText(text); } /****************************************************************************** * Set the remaining time text in a reminder window. * Called on every minute boundary. */ void MessageWin::setRemainingTextMinute() { QString text; const int mins = (KADateTime::currentUtcDateTime().secsTo(mDateTime.effectiveKDateTime()) + 59) / 60; if (mins < 60) text = i18ncp("@info", "in 1 minute's time", "in %1 minutes' time", (mins > 0 ? mins : 0)); else if (mins % 60 == 0) text = i18ncp("@info", "in 1 hour's time", "in %1 hours' time", mins/60); else { QString hourText = i18ncp("@item:intext inserted into 'in ... %1 minute's time' below", "1 hour", "%1 hours", mins/60); text = i18ncp("@info '%2' is the previous message '1 hour'/'%1 hours'", "in %2 1 minute's time", "in %2 %1 minutes' time", mins%60, hourText); } mRemainingText->setText(text); } /****************************************************************************** * Called when output is available from the command which is providing the text * for this window. Add the output and resize the window to show it. */ void MessageWin::readProcessOutput(ShellProcess* proc) { const QByteArray data = proc->readAll(); if (!data.isEmpty()) { // Strip any trailing newline, to avoid showing trailing blank line // in message window. if (mCommandText->newLine()) mCommandText->append(QStringLiteral("\n")); const int nl = data.endsWith('\n') ? 1 : 0; mCommandText->setNewLine(nl); mCommandText->insertPlainText(QString::fromLocal8Bit(data.data(), data.length() - nl)); resize(sizeHint()); } } /****************************************************************************** * Save settings to the session managed config file, for restoration * when the program is restored. */ void MessageWin::saveProperties(KConfigGroup& config) { if (mShown && !mErrorWindow && !mAlwaysHide) { config.writeEntry("EventID", mEventId.eventId()); config.writeEntry("CollectionID", mResource.id()); config.writeEntry("AlarmType", static_cast(mAlarmType)); if (mAlarmType == KAAlarm::INVALID_ALARM) qCCritical(KALARM_LOG) << "MessageWin::saveProperties: Invalid alarm: id=" << mEventId << ", alarm count=" << mEvent.alarmCount(); config.writeEntry("Message", mMessage); config.writeEntry("Type", static_cast(mAction)); config.writeEntry("Font", mFont); config.writeEntry("BgColour", mBgColour); config.writeEntry("FgColour", mFgColour); config.writeEntry("ConfirmAck", mConfirmAck); if (mDateTime.isValid()) { //TODO: Write KADateTime when it becomes possible config.writeEntry("Time", mDateTime.effectiveDateTime()); config.writeEntry("DateOnly", mDateTime.isDateOnly()); QByteArray zone; if (mDateTime.isUtc()) zone = "UTC"; else if (mDateTime.timeType() == KADateTime::TimeZone) { const QTimeZone tz = mDateTime.timeZone(); if (tz.isValid()) zone = tz.id(); } config.writeEntry("TimeZone", zone); } if (mCloseTime.isValid()) config.writeEntry("Expiry", mCloseTime); if (mAudioRepeatPause >= 0 && mSilenceButton && mSilenceButton->isEnabled()) { // Only need to restart sound file playing if it's being repeated config.writePathEntry("AudioFile", mAudioFile); config.writeEntry("Volume", static_cast(mVolume * 100)); config.writeEntry("AudioPause", mAudioRepeatPause); } config.writeEntry("Speak", mSpeak); config.writeEntry("Height", height()); config.writeEntry("DeferMins", mDefaultDeferMinutes); config.writeEntry("NoDefer", mNoDefer); config.writeEntry("NoPostAction", mNoPostAction); config.writeEntry("AkonadiItemId", mAkonadiItemId); 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); const QString eventId = config.readEntry("EventID"); const ResourceId collectionId = config.readEntry("CollectionID", ResourceId(-1)); mAlarmType = static_cast(config.readEntry("AlarmType", 0)); if (mAlarmType == KAAlarm::INVALID_ALARM) { mInvalid = true; qCCritical(KALARM_LOG) << "MessageWin::readProperties: Invalid alarm: id=" << eventId; } mMessage = config.readEntry("Message"); mAction = static_cast(config.readEntry("Type", 0)); mFont = config.readEntry("Font", QFont()); mBgColour = config.readEntry("BgColour", QColor(Qt::white)); mFgColour = config.readEntry("FgColour", QColor(Qt::black)); mConfirmAck = config.readEntry("ConfirmAck", false); QDateTime invalidDateTime; QDateTime dt = config.readEntry("Time", invalidDateTime); const QByteArray zoneId = config.readEntry("TimeZone").toLatin1(); KADateTime::Spec timeSpec; if (zoneId.isEmpty()) timeSpec = KADateTime::LocalZone; else if (zoneId == "UTC") timeSpec = KADateTime::UTC; else timeSpec = QTimeZone(zoneId); mDateTime = KADateTime(dt.date(), dt.time(), timeSpec); const bool dateOnly = config.readEntry("DateOnly", false); if (dateOnly) mDateTime.setDateOnly(true); mCloseTime = config.readEntry("Expiry", invalidDateTime); mCloseTime.setTimeSpec(Qt::UTC); mAudioFile = config.readPathEntry("AudioFile", QString()); mVolume = static_cast(config.readEntry("Volume", 0)) / 100; mFadeVolume = -1; mFadeSeconds = 0; if (!mAudioFile.isEmpty()) // audio file URL was only saved if it repeats mAudioRepeatPause = config.readEntry("AudioPause", 0); mBeep = false; // don't beep after restart (similar to not playing non-repeated sound file) mSpeak = config.readEntry("Speak", false); mRestoreHeight = config.readEntry("Height", 0); mDefaultDeferMinutes = config.readEntry("DeferMins", 0); mNoDefer = config.readEntry("NoDefer", false); mNoPostAction = config.readEntry("NoPostAction", true); mAkonadiItemId = config.readEntry("AkonadiItemId", QVariant(QVariant::LongLong)).toLongLong(); mCommandError = KAEvent::CmdErrType(config.readEntry("CmdErr", static_cast(KAEvent::CMD_NO_ERROR))); mDontShowAgain = config.readEntry("DontShowAgain", QString()); mShowEdit = false; // Temporarily initialise mResource and mEventId - they will be set by redisplayAlarm() mResource = Resources::resource(collectionId); mEventId = EventId(collectionId, 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 (!Resources::allCreated()) { connect(Resources::instance(), &Resources::resourcesCreated, this, &MessageWin::showRestoredAlarm); return; } redisplayAlarm(); } } } /****************************************************************************** * Fetch the restored alarm from the calendar and redisplay it in this window. */ void MessageWin::showRestoredAlarm() { qCDebug(KALARM_LOG) << "MessageWin::showRestoredAlarm:" << mEventId; redisplayAlarm(); show(); } /****************************************************************************** * Fetch the restored alarm from the calendar and redisplay it in this window. */ void MessageWin::redisplayAlarm() { mResource = Resources::resourceForEvent(mEventId.eventId()); mEventId.setCollectionId(mResource.id()); qCDebug(KALARM_LOG) << "MessageWin::redisplayAlarm:" << mEventId; // Delete any already existing window for the same event MessageWin* duplicate = findEvent(mEventId, this); if (duplicate) qCDebug(KALARM_LOG) << "MessageWin::redisplayAlarm: Deleting duplicate window:" << mEventId; delete duplicate; KAEvent* event = AlarmCalendar::resources()->event(mEventId); if (event) { mEvent = *event; mShowEdit = true; } else { // It's not in the active calendar, so try the displaying or archive calendars retrieveEvent(mEvent, mResource, mShowEdit, mNoDefer); mNoDefer = !mNoDefer; } initView(); } /****************************************************************************** * Redisplay alarms which were being shown when the program last exited. * Normally, these alarms will have been displayed by session restoration, but * if the program crashed or was killed, we can redisplay them here so that * they won't be lost. */ void MessageWin::redisplayAlarms() { if (mRedisplayed) return; qCDebug(KALARM_LOG) << "MessageWin::redisplayAlarms"; mRedisplayed = true; AlarmCalendar* cal = AlarmCalendar::displayCalendar(); if (cal && cal->isOpen()) { KAEvent event; Resource resource; const Event::List events = cal->kcalEvents(); for (int i = 0, end = events.count(); i < end; ++i) { bool showDefer, showEdit; reinstateFromDisplaying(events[i], event, resource, showEdit, showDefer); const EventId eventId(event); if (findEvent(eventId)) qCDebug(KALARM_LOG) << "MessageWin::redisplayAlarms: Message window already exists:" << eventId; else { // This event should be displayed, but currently isn't being const KAAlarm alarm = event.convertDisplayingAlarm(); if (alarm.type() == KAAlarm::INVALID_ALARM) { qCCritical(KALARM_LOG) << "MessageWin::redisplayAlarms: Invalid alarm: id=" << eventId; continue; } qCDebug(KALARM_LOG) << "MessageWin::redisplayAlarms:" << eventId; const bool login = alarm.repeatAtLogin(); const int flags = NO_RESCHEDULE | (login ? NO_DEFER : 0) | NO_INIT_VIEW; MessageWin* win = new MessageWin(&event, alarm, flags); win->mResource = resource; const bool rw = resource.isWritable(event.category()); 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, Resource& 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; Resource archiveRes = Resources::getStandard(CalEvent::ARCHIVED); if (archiveRes.isValid()) ev = AlarmCalendar::resources()->event(EventId(archiveRes.id(), CalEvent::uid(mEventId.eventId(), CalEvent::ARCHIVED))); if (!ev) return false; event = *ev; event.setArchive(); // ensure that it gets re-archived if it's saved event.setCategory(CalEvent::ACTIVE); if (mEventId.eventId() != event.id()) qCCritical(KALARM_LOG) << "MessageWin::retrieveEvent: Wrong event ID"; event.setEventId(mEventId.eventId()); resource = Resource(); showEdit = true; showDefer = true; qCDebug(KALARM_LOG) << "MessageWin::retrieveEvent:" << event.id() << ": success"; } return true; } /****************************************************************************** * Retrieves the displayed event from the calendar file, or if not found there, * from the displaying calendar. */ bool MessageWin::reinstateFromDisplaying(const Event::Ptr& kcalEvent, KAEvent& event, Resource& resource, bool& showEdit, bool& showDefer) { if (!kcalEvent) return false; ResourceId resourceId; event.reinstateFromDisplaying(kcalEvent, resourceId, showEdit, showDefer); event.setCollectionId(resourceId); resource = Resources::resource(resourceId); qCDebug(KALARM_LOG) << "MessageWin::reinstateFromDisplaying:" << EventId(event) << ": success"; return true; } /****************************************************************************** * Called when an alarm is currently being displayed, to store a copy of the * alarm in the displaying calendar, and to reschedule it for its next repetition. * If no repetitions remain, cancel it. */ void MessageWin::alarmShowing(KAEvent& event) { qCDebug(KALARM_LOG) << "MessageWin::alarmShowing:" << event.id() << "," << KAAlarm::debugType(mAlarmType); const KAAlarm alarm = event.alarm(mAlarmType); if (!alarm.isValid()) { qCCritical(KALARM_LOG) << "MessageWin::alarmShowing: Alarm type not found:" << event.id() << ":" << mAlarmType; return; } if (!mAlwaysHide) { // Copy the alarm to the displaying calendar in case of a crash, etc. KAEvent dispEvent; const ResourceId id = Resources::resourceForEvent(event.id()).id(); dispEvent.setDisplaying(event, mAlarmType, id, mDateTime.effectiveKDateTime(), mShowEdit, !mNoDefer); AlarmCalendar* cal = AlarmCalendar::displayCalendarOpen(); if (cal) { cal->deleteDisplayEvent(dispEvent.id()); // in case it already exists cal->addEvent(dispEvent); cal->save(); } } theApp()->rescheduleAlarm(event, alarm); } /****************************************************************************** * Spread alarm windows over the screen so that they are all visible, or pile * them on top of each other again. * Reply = true if windows are now scattered, false if piled up. */ bool MessageWin::spread(bool scatter) { if (instanceCount(true) <= 1) // ignore always-hidden windows return false; const QRect desk = KAlarm::desktopWorkArea(); // get the usable area of the desktop if (scatter == isSpread(desk.topLeft())) return scatter; if (scatter) { // Usually there won't be many windows, so a crude // scattering algorithm should suffice. int x = desk.left(); int y = desk.top(); int ynext = y; for (int errmsgs = 0; errmsgs < 2; ++errmsgs) { // Display alarm messages first, then error messages, since most // error messages tend to be the same height. for (int i = 0, end = mWindowList.count(); i < end; ++i) { MessageWin* w = mWindowList[i]; if ((!errmsgs && w->mErrorWindow) || (errmsgs && !w->mErrorWindow)) continue; const QSize sz = w->frameGeometry().size(); if (x + sz.width() > desk.right()) { x = desk.left(); y = ynext; } int ytmp = y; if (y + sz.height() > desk.bottom()) { ytmp = desk.bottom() - sz.height(); if (ytmp < desk.top()) ytmp = desk.top(); } w->move(x, ytmp); x += sz.width(); if (ytmp + sz.height() > ynext) ynext = ytmp + sz.height(); } } } else { // Move all windows to the top left corner for (int i = 0, end = mWindowList.count(); i < end; ++i) mWindowList[i]->move(desk.topLeft()); } return scatter; } /****************************************************************************** * Check whether message windows are all piled up, or are spread out. * Reply = true if windows are currently spread, false if piled up. */ bool MessageWin::isSpread(const QPoint& topLeft) { for (int i = 0, end = mWindowList.count(); i < end; ++i) { if (mWindowList[i]->pos() != topLeft) return true; } return false; } /****************************************************************************** * Returns the existing message window (if any) which is displaying the event * with the specified ID. */ MessageWin* MessageWin::findEvent(const EventId& eventId, MessageWin* exclude) { if (!eventId.isEmpty()) { for (int i = 0, end = mWindowList.count(); i < end; ++i) { MessageWin* w = mWindowList[i]; if (w != exclude && w->mEventId == eventId && !w->mErrorWindow) return w; } } return nullptr; } /****************************************************************************** * Beep and play the audio file, as appropriate. */ void MessageWin::playAudio() { if (mBeep) { // Beep using two methods, in case the sound card/speakers are switched off or not working QApplication::beep(); // beep through the internal speaker KNotification::beep(); // beep through the sound card & speakers } if (!mAudioFile.isEmpty()) { if (!mVolume && mFadeVolume <= 0) return; // ensure zero volume doesn't play anything startAudio(); // play the audio file } else if (mSpeak) { // The message is to be spoken. In case of error messges, // call it on a timer to allow the window to display first. QTimer::singleShot(0, this, &MessageWin::slotSpeak); } } /****************************************************************************** * Speak the message. * Called asynchronously to avoid delaying the display of the message. */ void MessageWin::slotSpeak() { KPIMTextEdit::TextToSpeech *tts = KPIMTextEdit::TextToSpeech::self(); if (!tts->isReady()) { KAMessageBox::detailedError(MainWindow::mainMainWindow(), i18nc("@info", "Unable to speak message"), i18nc("@info", "Text-to-speech subsystem is not available")); clearErrorMessage(ErrMsg_Speak); return; } tts->say(mMessage); } /****************************************************************************** * Called when another window's audio thread has been destructed. * Start playing this window's audio file. Because initialising the sound system * and loading the file may take some time, it is called in a separate thread to * allow the window to display first. */ void MessageWin::startAudio() { if (mAudioThread) { // An audio file is already playing for another message // window, so wait until it has finished. connect(mAudioThread.data(), &QObject::destroyed, this, &MessageWin::audioTerminating); } else { qCDebug(KALARM_LOG) << "MessageWin::startAudio:" << QThread::currentThread(); mAudioThread = new AudioThread(this, mAudioFile, mVolume, mFadeVolume, mFadeSeconds, mAudioRepeatPause); connect(mAudioThread.data(), &AudioThread::readyToPlay, this, &MessageWin::playReady); connect(mAudioThread.data(), &QThread::finished, this, &MessageWin::playFinished); if (mSilenceButton) connect(mSilenceButton, &QAbstractButton::clicked, mAudioThread.data(), &QThread::quit); // Notify after creating mAudioThread, so that isAudioPlaying() will // return the correct value. theApp()->notifyAudioPlaying(true); mAudioThread->start(); } } /****************************************************************************** * Return whether audio playback is currently active. */ bool MessageWin::isAudioPlaying() { return mAudioThread; } /****************************************************************************** * Stop audio playback. */ void MessageWin::stopAudio(bool wait) { qCDebug(KALARM_LOG) << "MessageWin::stopAudio"; if (mAudioThread) mAudioThread->stop(wait); } /****************************************************************************** * Called when another window's audio thread is being destructed. * Wait until the destructor has finished. */ void MessageWin::audioTerminating() { QTimer::singleShot(0, this, &MessageWin::startAudio); } /****************************************************************************** * Called when the audio file is ready to start playing. */ void MessageWin::playReady() { if (mSilenceButton) mSilenceButton->setEnabled(true); } /****************************************************************************** * Called when the audio file thread finishes. */ void MessageWin::playFinished() { if (mSilenceButton) mSilenceButton->setEnabled(false); if (mAudioThread) // mAudioThread can actually be null here! { const QString errmsg = mAudioThread->error(); if (!errmsg.isEmpty() && !haveErrorMessage(ErrMsg_AudioFile)) { KAMessageBox::error(this, errmsg); clearErrorMessage(ErrMsg_AudioFile); } } delete mAudioThread.data(); if (mAlwaysHide) close(); } /****************************************************************************** * Constructor for audio thread. */ AudioThread::AudioThread(MessageWin* parent, const QString& audioFile, float volume, float fadeVolume, int fadeSeconds, int repeatPause) : QThread(parent), mFile(audioFile), mVolume(volume), mFadeVolume(fadeVolume), mFadeSeconds(fadeSeconds), mRepeatPause(repeatPause), mAudioObject(nullptr) { if (mAudioOwner) qCCritical(KALARM_LOG) << "MessageWin::AudioThread: mAudioOwner already set"; mAudioOwner = parent; } /****************************************************************************** * Destructor for audio thread. Waits for thread completion and tidies up. * Note that this destructor is executed in the parent thread. */ AudioThread::~AudioThread() { qCDebug(KALARM_LOG) << "~MessageWin::AudioThread"; stop(true); // stop playing and tidy up (timeout 3 seconds) delete mAudioObject; mAudioObject = nullptr; if (mAudioOwner == parent()) mAudioOwner = nullptr; // Notify after deleting mAudioThread, so that isAudioPlaying() will // return the correct value. QTimer::singleShot(0, theApp(), &KAlarmApp::notifyAudioStopped); } /****************************************************************************** * Quits the thread and waits for thread completion and tidies up. */ void AudioThread::stop(bool waiT) { qCDebug(KALARM_LOG) << "MessageWin::AudioThread::stop"; quit(); // stop playing and tidy up wait(3000); // wait for run() to exit (timeout 3 seconds) if (!isFinished()) { // Something has gone wrong - forcibly kill the thread terminate(); if (waiT) wait(); } } /****************************************************************************** * Kick off the thread to play the audio file. */ void AudioThread::run() { mMutex.lock(); if (mAudioObject) { mMutex.unlock(); return; } qCDebug(KALARM_LOG) << "MessageWin::AudioThread::run:" << QThread::currentThread() << mFile; const QString audioFile = mFile; const QUrl url = QUrl::fromUserInput(mFile); mFile = url.isLocalFile() ? url.toLocalFile() : url.toString(); Phonon::MediaSource source(url); if (source.type() == Phonon::MediaSource::Invalid) { mError = xi18nc("@info", "Cannot open audio file: %1", audioFile); mMutex.unlock(); qCCritical(KALARM_LOG) << "MessageWin::AudioThread::run: Open failure:" << audioFile; return; } mAudioObject = new Phonon::MediaObject(); mAudioObject->setCurrentSource(source); mAudioObject->setTransitionTime(100); // workaround to prevent clipping of end of files in Xine backend Phonon::AudioOutput* output = new Phonon::AudioOutput(Phonon::NotificationCategory, mAudioObject); mPath = Phonon::createPath(mAudioObject, output); if (mVolume >= 0 || mFadeVolume >= 0) { const float vol = (mVolume >= 0) ? mVolume : output->volume(); const float maxvol = qMax(vol, mFadeVolume); output->setVolume(maxvol); if (mFadeVolume >= 0 && mFadeSeconds > 0) { Phonon::VolumeFaderEffect* fader = new Phonon::VolumeFaderEffect(mAudioObject); fader->setVolume(mFadeVolume / maxvol); fader->fadeTo(mVolume / maxvol, mFadeSeconds * 1000); mPath.insertEffect(fader); } } connect(mAudioObject, &Phonon::MediaObject::stateChanged, this, &AudioThread::playStateChanged, Qt::DirectConnection); connect(mAudioObject, &Phonon::MediaObject::finished, this, &AudioThread::checkAudioPlay, Qt::DirectConnection); mPlayedOnce = false; mPausing = false; mMutex.unlock(); Q_EMIT readyToPlay(); checkAudioPlay(); // Start an event loop. // The function will exit once exit() or quit() is called. // First, ensure that the thread object is deleted once it has completed. connect(this, &QThread::finished, this, &QObject::deleteLater); exec(); stopPlay(); } /****************************************************************************** * Called when the audio file has loaded and is ready to play, or when play * has completed. * If it is ready to play, start playing it (for the first time or repeated). * If play has not yet completed, wait a bit longer. */ void AudioThread::checkAudioPlay() { mMutex.lock(); if (!mAudioObject) { mMutex.unlock(); return; } if (mPausing) mPausing = false; else { // The file has loaded and is ready to play, or play has completed if (mPlayedOnce) { if (mRepeatPause < 0) { // Play has completed mMutex.unlock(); stopPlay(); return; } if (mRepeatPause > 0) { // Pause before playing the file again mPausing = true; QTimer::singleShot(mRepeatPause * 1000, this, &AudioThread::checkAudioPlay); mMutex.unlock(); return; } } mPlayedOnce = true; } // Start playing the file, either for the first time or again qCDebug(KALARM_LOG) << "MessageWin::AudioThread::checkAudioPlay: start"; mAudioObject->play(); mMutex.unlock(); } /****************************************************************************** * Called when the playback object changes state. * If an error has occurred, quit and return the error to the caller. */ void AudioThread::playStateChanged(Phonon::State newState) { if (newState == Phonon::ErrorState) { QMutexLocker locker(&mMutex); const QString err = mAudioObject->errorString(); if (!err.isEmpty()) { qCCritical(KALARM_LOG) << "MessageWin::AudioThread::playStateChanged: Play failure:" << mFile << ":" << err; mError = xi18nc("@info", "Error playing audio file: %1%2", mFile, err); exit(1); } } } /****************************************************************************** * Called when play completes, the Silence button is clicked, or the window is * closed, to terminate audio access. */ void AudioThread::stopPlay() { mMutex.lock(); if (mAudioObject) { mAudioObject->stop(); const QList effects = mPath.effects(); for (int i = 0; i < effects.count(); ++i) { mPath.removeEffect(effects[i]); delete effects[i]; } delete mAudioObject; mAudioObject = nullptr; } mMutex.unlock(); quit(); // exit the event loop, if it's still running } QString AudioThread::error() const { QMutexLocker locker(&mMutex); return mError; } /****************************************************************************** * Raise the alarm window, re-output any required audio notification, and * reschedule the alarm in the calendar file. */ void MessageWin::repeat(const KAAlarm& alarm) { if (!mInitialised) return; if (mDeferDlg) { // Cancel any deferral dialog so that the user notices something's going on, // and also because the deferral time limit will have changed. delete mDeferDlg; mDeferDlg = nullptr; } KAEvent* event = mEventId.isEmpty() ? nullptr : AlarmCalendar::resources()->event(mEventId); if (event) { mAlarmType = alarm.type(); // store new alarm type for use if it is later deferred if (mAlwaysHide) playAudio(); else { if (!mDeferDlg || Preferences::modalMessages()) { raise(); playAudio(); } if (mDeferButton->isVisible()) { mDeferButton->setEnabled(true); setDeferralLimit(*event); // ensure that button is disabled when alarm can't be deferred any more } } alarmShowing(*event); } } /****************************************************************************** * Display the window. * If windows are being positioned away from the mouse cursor, it is initially * positioned at the top left to slightly reduce the number of times the * windows need to be moved in showEvent(). */ void MessageWin::show() { if (mCloseTime.isValid()) { // Set a timer to auto-close the window int delay = QDateTime::currentDateTimeUtc().secsTo(mCloseTime); if (delay < 0) delay = 0; QTimer::singleShot(delay * 1000, this, &QWidget::close); if (!delay) return; // don't show the window if auto-closing is already due } if (Preferences::messageButtonDelay() == 0) move(0, 0); MainWindowBase::show(); } /****************************************************************************** * Returns the window's recommended size exclusive of its frame. */ QSize MessageWin::sizeHint() const { QSize desired; switch (mAction) { case KAEvent::MESSAGE: desired = MainWindowBase::sizeHint(); break; case KAEvent::COMMAND: if (mShown) { // For command output, expand the window to accommodate the text const QSize texthint = mCommandText->sizeHint(); int w = texthint.width() + 2 * style()->pixelMetric(QStyle::PM_DefaultChildMargin); if (w < width()) w = width(); const int ypadding = height() - mCommandText->height(); desired = QSize(w, texthint.height() + ypadding); break; } // fall through to default Q_FALLTHROUGH(); default: return MainWindowBase::sizeHint(); } // Limit the size to fit inside the working area of the desktop const QSize desktop = KAlarm::desktopWorkArea(mScreenNumber).size(); const QSize frameThickness = frameGeometry().size() - geometry().size(); // title bar & window frame return desired.boundedTo(desktop - frameThickness); } /****************************************************************************** * Called when the window is shown. * The first time, output any required audio notification, and reschedule or * delete the event from the calendar file. */ void MessageWin::showEvent(QShowEvent* se) { MainWindowBase::showEvent(se); if (mShown || !mInitialised) return; if (mErrorWindow || mAlarmType == KAAlarm::INVALID_ALARM) { // Don't bother repositioning error messages, // and invalid alarms should be deleted anyway. enableButtons(); } else { /* Set the window size. * Note that the frame thickness is not yet known when this * method is called, so for large windows the size needs to be * set again later. */ bool execComplete = true; QSize s = sizeHint(); // fit the window round the message if (mAction == KAEvent::FILE && !mErrorMsgs.count()) KAlarm::readConfigWindowSize("FileMessage", s); resize(s); const QRect desk = KAlarm::desktopWorkArea(mScreenNumber); const QRect frame = frameGeometry(); mButtonDelay = Preferences::messageButtonDelay() * 1000; if (mButtonDelay) { // Position the window in the middle of the screen, and // delay enabling the buttons. mPositioning = true; move((desk.width() - frame.width())/2, (desk.height() - frame.height())/2); execComplete = false; } else { /* Try to ensure that the window can't accidentally be acknowledged * by the user clicking the mouse just as it appears. * To achieve this, move the window so that the OK button is as far away * from the cursor as possible. If the buttons are still too close to the * cursor, disable the buttons for a short time. * N.B. This can't be done in show(), since the geometry of the window * is not known until it is displayed. Unfortunately by moving the * window in showEvent(), a flicker is unavoidable. * See the Qt documentation on window geometry for more details. */ // PROBLEM: The frame size is not known yet! const QPoint cursor = QCursor::pos(); const QRect rect = geometry(); // Find the offsets from the outside of the frame to the edges of the OK button const QRect button(mOkButton->mapToParent(QPoint(0, 0)), mOkButton->mapToParent(mOkButton->rect().bottomRight())); const int buttonLeft = button.left() + rect.left() - frame.left(); const int buttonRight = width() - button.right() + frame.right() - rect.right(); const int buttonTop = button.top() + rect.top() - frame.top(); const int buttonBottom = height() - button.bottom() + frame.bottom() - rect.bottom(); const int centrex = (desk.width() + buttonLeft - buttonRight) / 2; const int centrey = (desk.height() + buttonTop - buttonBottom) / 2; const int x = (cursor.x() < centrex) ? desk.right() - frame.width() : desk.left(); const int y = (cursor.y() < centrey) ? desk.bottom() - frame.height() : desk.top(); // Find the enclosing rectangle for the new button positions // and check if the cursor is too near QRect buttons = mOkButton->geometry().united(mKAlarmButton->geometry()); buttons.translate(rect.left() + x - frame.left(), rect.top() + y - frame.top()); const int minDistance = proximityMultiple * mOkButton->height(); if ((abs(cursor.x() - buttons.left()) < minDistance || abs(cursor.x() - buttons.right()) < minDistance) && (abs(cursor.y() - buttons.top()) < minDistance || abs(cursor.y() - buttons.bottom()) < minDistance)) mButtonDelay = proximityButtonDelay; // too near - disable buttons initially if (x != frame.left() || y != frame.top()) { mPositioning = true; move(x, y); execComplete = false; } } if (execComplete) displayComplete(); // play audio, etc. } // Set the window size etc. once the frame size is known QTimer::singleShot(0, this, &MessageWin::frameDrawn); mShown = true; } /****************************************************************************** * Called when the window has been moved. */ void MessageWin::moveEvent(QMoveEvent* e) { MainWindowBase::moveEvent(e); theApp()->setSpreadWindowsState(isSpread(KAlarm::desktopWorkArea(mScreenNumber).topLeft())); if (mPositioning) { // The window has just been initially positioned mPositioning = false; displayComplete(); // play audio, etc. } } /****************************************************************************** * Called after (hopefully) the window frame size is known. * Reset the initial window size if it exceeds the working area of the desktop. * Set the 'spread windows' menu item status. */ void MessageWin::frameDrawn() { if (!mErrorWindow && mAction == KAEvent::MESSAGE) { const QSize s = sizeHint(); if (width() > s.width() || height() > s.height()) resize(s); } theApp()->setSpreadWindowsState(isSpread(KAlarm::desktopWorkArea(mScreenNumber).topLeft())); } /****************************************************************************** * Called when the window has been displayed properly (in its correct position), * to play sounds and reschedule the event. */ void MessageWin::displayComplete() { delete mTempFile; mTempFile = nullptr; playAudio(); if (mRescheduleEvent) alarmShowing(mEvent); if (!mAlwaysHide) { // Enable the window's buttons either now or after the configured delay if (mButtonDelay > 0) QTimer::singleShot(mButtonDelay, this, &MessageWin::enableButtons); else enableButtons(); } } /****************************************************************************** * Enable the window's buttons. */ void MessageWin::enableButtons() { mOkButton->setEnabled(true); mKAlarmButton->setEnabled(true); if (mDeferButton->isVisible() && !mDisableDeferral) mDeferButton->setEnabled(true); if (mEditButton) mEditButton->setEnabled(true); if (mKMailButton) mKMailButton->setEnabled(true); } /****************************************************************************** * Called when the window's size has changed (before it is painted). */ void MessageWin::resizeEvent(QResizeEvent* re) { if (mRestoreHeight) { // Restore the window height on session restoration if (mRestoreHeight != re->size().height()) { QSize size = re->size(); size.setHeight(mRestoreHeight); resize(size); } else if (isVisible()) mRestoreHeight = 0; } else { if (mShown && mAction == KAEvent::FILE && !mErrorMsgs.count()) KAlarm::writeConfigWindowSize("FileMessage", re->size()); MainWindowBase::resizeEvent(re); } } /****************************************************************************** * Called when a close event is received. * Only quits the application if there is no system tray icon displayed. */ void MessageWin::closeEvent(QCloseEvent* ce) { // Don't prompt or delete the alarm from the display calendar if the session is closing if (!mErrorWindow && !qApp->isSavingSession()) { if (mConfirmAck && !mNoCloseConfirm) { // Ask for confirmation of acknowledgement. Use warningYesNo() because its default is No. if (KAMessageBox::warningYesNo(this, i18nc("@info", "Do you really want to acknowledge this alarm?"), i18nc("@action:button", "Acknowledge Alarm"), KGuiItem(i18nc("@action:button", "Acknowledge")), KStandardGuiItem::cancel()) != KMessageBox::Yes) { ce->ignore(); return; } } if (!mEventId.isEmpty()) { // Delete from the display calendar KAlarm::deleteDisplayEvent(CalEvent::uid(mEventId.eventId(), CalEvent::DISPLAYING)); } } MainWindowBase::closeEvent(ce); } /****************************************************************************** * Called when the OK button is clicked. */ void MessageWin::slotOk() { if (mDontShowAgainCheck && mDontShowAgainCheck->isChecked()) KAlarm::setDontShowErrors(mEventId, mDontShowAgain); close(); } /****************************************************************************** * Called when the KMail button is clicked. * Tells KMail to display the email message displayed in this message window. */ void MessageWin::slotShowKMailMessage() { qCDebug(KALARM_LOG) << "MessageWin::slotShowKMailMessage"; if (mAkonadiItemId < 0) return; const QString err = KAlarm::runKMail(); if (!err.isNull()) { KAMessageBox::sorry(this, err); return; } org::kde::kmail::kmail kmail(KMAIL_DBUS_SERVICE, KMAIL_DBUS_PATH, QDBusConnection::sessionBus()); // Display the message contents QDBusReply reply = kmail.showMail(mAkonadiItemId); bool failed1 = true; bool failed2 = true; if (!reply.isValid()) qCCritical(KALARM_LOG) << "kmail 'showMail' D-Bus call failed:" << reply.error().message(); else if (reply.value()) failed1 = false; // Select the mail folder containing the message Akonadi::ItemFetchJob* job = new Akonadi::ItemFetchJob(Akonadi::Item(mAkonadiItemId)); job->fetchScope().setAncestorRetrieval(Akonadi::ItemFetchScope::Parent); Akonadi::Item::List items; if (job->exec()) items = job->items(); if (items.isEmpty() || !items.at(0).isValid()) qCWarning(KALARM_LOG) << "MessageWin::slotShowKMailMessage: No parent found for item" << mAkonadiItemId; else { const Akonadi::Item& it = items.at(0); const Akonadi::Collection::Id colId = it.parentCollection().id(); reply = kmail.selectFolder(QString::number(colId)); if (!reply.isValid()) qCCritical(KALARM_LOG) << "kmail 'selectFolder' D-Bus call failed:" << reply.error().message(); else if (reply.value()) failed2 = false; } if (failed1 || failed2) KAMessageBox::sorry(this, xi18nc("@info", "Unable to locate this email in KMail")); } /****************************************************************************** * Called when the Edit... button is clicked. * Displays the alarm edit dialog. * * NOTE: The alarm edit dialog is made a child of the main window, not this * window, so that if this window closes before the dialog (e.g. on * auto-close), KAlarm doesn't crash. The dialog is set non-modal so that * the main window is unaffected, but modal mode is simulated so that * this window is inactive while the dialog is open. */ void MessageWin::slotEdit() { qCDebug(KALARM_LOG) << "MessageWin::slotEdit"; MainWindow* mainWin = MainWindow::mainMainWindow(); mEditDlg = EditAlarmDlg::create(false, &mOriginalEvent, false, mainWin, EditAlarmDlg::RES_IGNORE); mEditDlg->setAttribute(Qt::WA_NativeWindow, true); KWindowSystem::setMainWindow(mEditDlg->windowHandle(), winId()); KWindowSystem::setOnAllDesktops(mEditDlg->winId(), false); setButtonsReadOnly(true); connect(mEditDlg, &QDialog::accepted, this, &MessageWin::editCloseOk); connect(mEditDlg, &QDialog::rejected, this, &MessageWin::editCloseCancel); connect(mEditDlg, &QObject::destroyed, this, &MessageWin::editCloseCancel); connect(KWindowSystem::self(), &KWindowSystem::activeWindowChanged, this, &MessageWin::activeWindowChanged); mainWin->editAlarm(mEditDlg, mOriginalEvent); } /****************************************************************************** * Called when OK is clicked in the alarm edit dialog invoked by the Edit button. * Closes the window. */ void MessageWin::editCloseOk() { mEditDlg = nullptr; mNoCloseConfirm = true; // allow window to close without confirmation prompt close(); } /****************************************************************************** * Called when Cancel is clicked in the alarm edit dialog invoked by the Edit * button, or when the dialog is deleted. */ void MessageWin::editCloseCancel() { mEditDlg = nullptr; setButtonsReadOnly(false); } /****************************************************************************** * Called when the active window has changed. If this window has become the * active window and there is an alarm edit dialog, simulate a modal dialog by * making the alarm edit dialog the active window instead. */ void MessageWin::activeWindowChanged(WId win) { if (mEditDlg && win == winId()) KWindowSystem::activateWindow(mEditDlg->winId()); } /****************************************************************************** * Set or clear the read-only state of the dialog buttons. */ void MessageWin::setButtonsReadOnly(bool ro) { mOkButton->setReadOnly(ro, true); mDeferButton->setReadOnly(ro, true); mEditButton->setReadOnly(ro, true); if (mSilenceButton) mSilenceButton->setReadOnly(ro, true); if (mKMailButton) mKMailButton->setReadOnly(ro, true); mKAlarmButton->setReadOnly(ro, true); } /****************************************************************************** * Set up to disable the defer button when the deferral limit is reached. */ void MessageWin::setDeferralLimit(const KAEvent& event) { mDeferLimit = event.deferralLimit().effectiveKDateTime().toUtc().qDateTime(); MidnightTimer::connect(this, SLOT(checkDeferralLimit())); // check every day mDisableDeferral = false; checkDeferralLimit(); } /****************************************************************************** * Check whether the deferral limit has been reached. * If so, disable the Defer button. * N.B. Ideally, just a single QTimer::singleShot() call would be made to disable * the defer button at the corret time. But for a 32-bit integer, the * milliseconds parameter overflows in about 25 days, so instead a daily * check is done until the day when the deferral limit is reached, followed * by a non-overflowing QTimer::singleShot() call. */ void MessageWin::checkDeferralLimit() { if (!mDeferButton->isEnabled() || !mDeferLimit.isValid()) return; int n = KADateTime::currentLocalDate().daysTo(KADateTime(mDeferLimit, KADateTime::LocalZone).date()); if (n > 0) return; MidnightTimer::disconnect(this, SLOT(checkDeferralLimit())); if (n == 0) { // The deferral limit will be reached today n = QDateTime::currentDateTimeUtc().secsTo(mDeferLimit); if (n > 0) { QTimer::singleShot(n * 1000, this, &MessageWin::checkDeferralLimit); return; } } mDeferButton->setEnabled(false); mDisableDeferral = true; } /****************************************************************************** * Called when the Defer... button is clicked. * Displays the defer message dialog. */ void MessageWin::slotDefer() { mDeferDlg = new DeferAlarmDlg(KADateTime::currentDateTime(Preferences::timeSpec()).addSecs(60), mDateTime.isDateOnly(), false, this); mDeferDlg->setObjectName(QStringLiteral("DeferDlg")); // used by LikeBack mDeferDlg->setDeferMinutes(mDefaultDeferMinutes > 0 ? mDefaultDeferMinutes : Preferences::defaultDeferTime()); mDeferDlg->setLimit(mEvent); if (!Preferences::modalMessages()) lower(); if (mDeferDlg->exec() == QDialog::Accepted) { const DateTime dateTime = mDeferDlg->getDateTime(); const int delayMins = mDeferDlg->deferMinutes(); // Fetch the up-to-date alarm from the calendar. Note that it could have // changed since it was displayed. const KAEvent* event = mEventId.isEmpty() ? nullptr : AlarmCalendar::resources()->event(mEventId); if (event) { // The event still exists in the active calendar qCDebug(KALARM_LOG) << "MessageWin::slotDefer: Deferring event" << mEventId; KAEvent newev(*event); newev.defer(dateTime, (mAlarmType & KAAlarm::REMINDER_ALARM), true); newev.setDeferDefaultMinutes(delayMins); KAlarm::updateEvent(newev, mDeferDlg, true); if (newev.deferred()) mNoPostAction = true; } else { // Try to retrieve the event from the displaying or archive calendars Resource resource; KAEvent event; bool showEdit, showDefer; if (!retrieveEvent(event, resource, showEdit, showDefer)) { // The event doesn't exist any more !?!, so recurrence data, // flags, and more, have been lost. KAMessageBox::error(this, xi18nc("@info", "Cannot defer alarm:Alarm not found.")); raise(); delete mDeferDlg; mDeferDlg = nullptr; mDeferButton->setEnabled(false); mEditButton->setEnabled(false); return; } qCDebug(KALARM_LOG) << "MessageWin::slotDefer: Deferring retrieved event" << mEventId; event.defer(dateTime, (mAlarmType & KAAlarm::REMINDER_ALARM), true); event.setDeferDefaultMinutes(delayMins); event.setCommandError(mCommandError); // Add the event back into the calendar file, retaining its ID // and not updating KOrganizer. KAlarm::addEvent(event, &resource, 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(mEventId.eventId()); } /****************************************************************************** * 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 QList screens = QGuiApplication::screens(); const int numScreens = screens.count(); if (numScreens > 1) { // There are multiple screens. // Check for any full screen windows, even if they are not the active // window, and try not to show the alarm message their screens. mScreenNumber = QApplication::desktop()->screenNumber(MainWindow::mainMainWindow()); // default = KAlarm's screen if (QGuiApplication::primaryScreen()->virtualSiblings().size() > 1) { // The screens form a single virtual desktop. // Xinerama, for example, uses this scheme. QVector screenTypes(numScreens); QVector screenRects(numScreens); for (int s = 0; s < numScreens; ++s) screenRects[s] = screens[s]->geometry(); const FullScreenType full = findFullScreenWindows(screenRects, screenTypes); if (full == NoFullScreen || screenTypes[mScreenNumber] == NoFullScreen) return modal; for (int s = 0; s < numScreens; ++s) { if (screenTypes[s] == NoFullScreen) { // There is no full screen window on this screen mScreenNumber = s; return modal; } } // All screens contain a full screen window: use one without // an active full screen window. for (int s = 0; s < numScreens; ++s) { if (screenTypes[s] == FullScreen) { mScreenNumber = s; return modal; } } } else { // The screens are completely separate from each other. int inactiveScreen = -1; FullScreenType full = haveFullScreenWindow(mScreenNumber); //qCDebug(KALARM_LOG)<<"full="<& screenRects, QVector& screenTypes) { FullScreenType result = NoFullScreen; screenTypes.fill(NoFullScreen); xcb_connection_t* connection = QX11Info::connection(); const NETRootInfo rootInfo(connection, NET::ClientList | NET::ActiveWindow, NET::Properties2()); const xcb_window_t rootWindow = rootInfo.rootWindow(); const xcb_window_t activeWindow = rootInfo.activeWindow(); const xcb_window_t* windows = rootInfo.clientList(); const int windowCount = rootInfo.clientListCount(); //qCDebug(KALARM_LOG)<<"Virtual desktops: Window count="< * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef NEWALARMACTION_H #define NEWALARMACTION_H #include "editdlg.h" -#include +#include #include #include class KActionCollection; class TemplateMenuAction; class NewAlarmAction : public KActionMenu { Q_OBJECT public: NewAlarmAction(bool templates, const QString& label, QObject* parent, KActionCollection* collection = nullptr); virtual ~NewAlarmAction() {} QAction* displayAlarmAction(const QString& name); QAction* commandAlarmAction(const QString& name); QAction* emailAlarmAction(const QString& name); QAction* audioAlarmAction(const QString& name); TemplateMenuAction* fromTemplateAlarmAction(const QString& name); Q_SIGNALS: void selected(EditAlarmDlg::Type); private Q_SLOTS: void slotSelected(QAction*); void slotInitMenu(); void slotCalendarStatusChanged(); private: QAction* mDisplayAction; QAction* mCommandAction; QAction* mEmailAction; QAction* mAudioAction; TemplateMenuAction* mTemplateAction{nullptr}; // New From Template action, for non-template menu only KActionCollection* mActionCollection; QMap mTypes; }; #endif // NEWALARMACTION_H // vim: et sw=4: diff --git a/src/prefdlg.cpp b/src/prefdlg.cpp index b6297c24..7e2f4b69 100644 --- a/src/prefdlg.cpp +++ b/src/prefdlg.cpp @@ -1,2000 +1,2000 @@ /* * prefdlg.cpp - program preferences dialog * Program: kalarm * Copyright © 2001-2019 David Jarvie * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "prefdlg.h" #include "prefdlg_p.h" #include "alarmcalendar.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 "resources/resources.h" #include "config-kalarm.h" #include "kalarm_debug.h" #include #include using namespace KHolidays; #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 using namespace KCalendarCore; 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() { 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) << "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) << "KAlarmPrefDlg::slotOk"; mValid = true; slotApply(); if (mValid) QDialog::accept(); } // Discard the current preferences and close the dialog void KAlarmPrefDlg::slotCancel() { 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) << "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) { QFrame* topWidget = new QFrame(this); setWidget(topWidget); mTopLayout = new QVBoxLayout(topWidget); mTopLayout->setContentsMargins(0, 0, 0, 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; const int end = mLabels.count(); QList xpos; for (i = 0; i < end; ++i) { const int x = mLabels[i]->mapTo(this, QPoint(0, 0)).x(); xpos += x; const 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); const int dcm = style()->pixelMetric(QStyle::PM_DefaultChildMargin); vlayout->setContentsMargins(dcm, dcm, dcm, dcm); 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->setContentsMargins(dcm, dcm, dcm, dcm); 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]; const 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); const 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 { const 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()) { const 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->setContentsMargins(0, 0, 0, 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->setContentsMargins(0, 0, 0, 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->setContentsMargins(0, 0, 0, 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->setContentsMargins(0, 0, 0, 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; for (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->setContentsMargins(0, 0, 0, 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->setContentsMargins(0, 0, 0, 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); const int dcm = style()->pixelMetric(QStyle::PM_DefaultChildMargin); layout->setContentsMargins(dcm, dcm, dcm, dcm); 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->setContentsMargins(0, 0, 0, 0); layout->addLayout(itemBox); widget = new QWidget; // this is to control the QWhatsThis text display area itemBox->addWidget(widget); box = new QHBoxLayout(widget); box->setContentsMargins(0, 0, 0, 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->setContentsMargins(0, 0, 0, 0); layout->addLayout(itemBox); widget = new QWidget; // this is to control the QWhatsThis text display area itemBox->addWidget(widget); box = new QHBoxLayout(widget); box->setContentsMargins(0, 0, 0, 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->setContentsMargins(dcm, dcm, dcm, dcm); 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->setContentsMargins(0, 0, 0, 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()); const 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) { const 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()); const QString hol = mHolidays->itemData(mHolidays->currentIndex()).toString(); if (hol != Preferences::holidays().regionCode()) Preferences::setHolidayRegion(hol); int t = mStartOfDay->value(); const 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) { // 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); const int dcm = style()->pixelMetric(QStyle::PM_DefaultChildMargin); layout->setContentsMargins(dcm, dcm, dcm, dcm); 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->setContentsMargins(dcm, dcm, dcm, dcm); 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->setContentsMargins(0, 0, 0, 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((Resources::enabledResources(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); const int keepDays = Preferences::archivedKeepDays(); if (!defaults) mOldKeepArchived = keepDays; setArchivedControls(keepDays); mCheckKeepChanges = true; } void StorePrefTab::apply(bool syncToDisc) { const bool b = mAskResource->isChecked(); if (b != Preferences::askResource()) Preferences::setAskResource(mAskResource->isChecked()); const 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) { const bool keep = mKeepArchived->isChecked(); if (keep && !mOldKeepArchived && mCheckKeepChanges && !Resources::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() { const bool single = Resources::enabledResources(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) { 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); const QString kmailOption = i18nc("@option:radio", "KMail"); const 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); const int dcm = style()->pixelMetric(QStyle::PM_DefaultChildMargin); grid->setContentsMargins(dcm, dcm, dcm, dcm); 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) { const 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->setContentsMargins(0, 0, 0, 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->setContentsMargins(0, 0, 0, 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->setContentsMargins(0, 0, 0, 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->setContentsMargins(0, 0, 0, 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); const int dcm = style()->pixelMetric(QStyle::PM_DefaultChildMargin); vlayout->setContentsMargins(dcm, dcm, dcm, dcm); 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->setContentsMargins(0, 0, 0, 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->setContentsMargins(dcm, dcm, dcm, dcm); vlayout->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); QHBoxLayout* hlayout = new QHBoxLayout; hlayout->setContentsMargins(0, 0, 0, 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->setContentsMargins(0, 0, 0, 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->setContentsMargins(dcm, dcm, dcm, dcm); vlayout->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); hlayout = new QHBoxLayout(); hlayout->setContentsMargins(0, 0, 0, 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->setContentsMargins(dcm, dcm, dcm, dcm); 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{}; 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); const 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); const 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); const QFont font = mFontChooser->font(); if (font != Preferences::messageFont()) Preferences::setMessageFont(font); PrefsTabBase::apply(syncToDisc); } void EditPrefTab::slotBrowseSoundFile() { QString defaultDir; QString file; if (SoundPicker::browseFile(file, defaultDir, mSoundFile->text())) { if (!file.isEmpty()) mSoundFile->setText(file); } } 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) { mTabs = new QTabWidget(); topLayout()->addWidget(mTabs); QWidget* widget = new QWidget; QVBoxLayout* topGeneral = new QVBoxLayout(widget); const int dcm = style()->pixelMetric(QStyle::PM_DefaultChildMargin); const int m = dcm / 2; topGeneral->setContentsMargins(m, m, m, m); topGeneral->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); mTabGeneral = mTabs->addTab(widget, i18nc("@title:tab", "General")); widget = new QWidget; QVBoxLayout* topWindows = new QVBoxLayout(widget); topWindows->setContentsMargins(m, m, m, m); 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->setContentsMargins(dcm, dcm, dcm, dcm); 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->setContentsMargins(dcm, dcm, dcm, dcm); 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->setContentsMargins(0, 0, 0, 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(i18nc("@option:check", "Show alarm time"), 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(i18nc("@option:check", "Show time until alarm"), 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->setContentsMargins(0, 0, 0, 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->setContentsMargins(dcm, dcm, dcm, dcm); QVBoxLayout* colourLayout = new QVBoxLayout(); colourLayout->setContentsMargins(0, 0, 0, 0); hlayout->addLayout(colourLayout); widget = new QWidget; // to group widgets for QWhatsThis text box = new QHBoxLayout(widget); box->setContentsMargins(0, 0, 0, 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->setContentsMargins(0, 0, 0, 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->setContentsMargins(dcm, dcm, dcm, dcm); 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); const 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->setContentsMargins(0, 0, 0, 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; const 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; const TimePeriod::Units units = secs ? TimePeriod::HoursMinutes : (days % 7) ? TimePeriod::Days : TimePeriod::Weeks; const 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) { const 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/prefdlg.h b/src/prefdlg.h index 71a90f35..31bd8fdf 100644 --- a/src/prefdlg.h +++ b/src/prefdlg.h @@ -1,81 +1,81 @@ /* * prefdlg.h - program preferences dialog * Program: kalarm * Copyright © 2001-2013 David Jarvie * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef PREFDLG_H #define PREFDLG_H -#include +#include class EditPrefTab; class EmailPrefTab; class ViewPrefTab; class StorePrefTab; class TimePrefTab; class MiscPrefTab; class StackedScrollGroup; // The Preferences dialog class KAlarmPrefDlg : public KPageDialog { Q_OBJECT public: static void display(); ~KAlarmPrefDlg(); QSize minimumSizeHint() const override; MiscPrefTab* mMiscPage; TimePrefTab* mTimePage; StorePrefTab* mStorePage; EditPrefTab* mEditPage; EmailPrefTab* mEmailPage; ViewPrefTab* mViewPage; KPageWidgetItem* mMiscPageItem; KPageWidgetItem* mTimePageItem; KPageWidgetItem* mStorePageItem; KPageWidgetItem* mEditPageItem; KPageWidgetItem* mEmailPageItem; KPageWidgetItem* mViewPageItem; protected: void showEvent(QShowEvent*) override; void resizeEvent(QResizeEvent*) override; protected Q_SLOTS: virtual void slotOk(); virtual void slotApply(); virtual void slotHelp(); virtual void slotDefault(); virtual void slotCancel(); private: KAlarmPrefDlg(); void restore(bool defaults); static KAlarmPrefDlg* mInstance; StackedScrollGroup* mTabScrollGroup; bool mShown{false}; bool mValid; }; #endif // PREFDLG_H // vim: et sw=4: diff --git a/src/preferences.cpp b/src/preferences.cpp index 27d84bbf..fef18e2e 100644 --- a/src/preferences.cpp +++ b/src/preferences.cpp @@ -1,525 +1,525 @@ /* * preferences.cpp - program preference settings * Program: kalarm * Copyright © 2001-2019 David Jarvie * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "preferences.h" #include "kalarm.h" #include "functions.h" #include "kamail.h" #include "messagebox.h" #include "kalarm_debug.h" #include #include #include #include #include -#include -#include +#include +#include #include #include #include using namespace KHolidays; using namespace KAlarmCal; // Config file entry names static const char* GENERAL_SECTION = "General"; // Config file entry name for temporary use static const char* TEMP = "Temp"; static const QString AUTOSTART_FILE(QStringLiteral("kalarm.autostart.desktop")); // Values for EmailFrom entry static const QString FROM_SYS_SETTINGS(QStringLiteral("@SystemSettings")); static const QString FROM_KMAIL(QStringLiteral("@KMail")); // Config file entry names for notification messages const QLatin1String Preferences::QUIT_WARN("QuitWarn"); const QLatin1String Preferences::ASK_AUTO_START("AskAutoStart"); const QLatin1String Preferences::CONFIRM_ALARM_DELETION("ConfirmAlarmDeletion"); const QLatin1String Preferences::EMAIL_QUEUED_NOTIFY("EmailQueuedNotify"); const bool default_quitWarn = true; const bool default_emailQueuedNotify = false; const bool default_confirmAlarmDeletion = true; static QString translateXTermPath(const QString& cmdline, bool write); Preferences* Preferences::mInstance = nullptr; bool Preferences::mUsingDefaults = false; HolidayRegion* Preferences::mHolidays = nullptr; // always non-null after Preferences initialisation QString Preferences::mPreviousVersion; Preferences::Backend Preferences::mPreviousBackend; // Change tracking bool Preferences::mAutoStartChangedByUser = false; Preferences* Preferences::self() { if (!mInstance) { // Set the default button for the Quit warning message box to Cancel KAMessageBox::setContinueDefault(QUIT_WARN, KMessageBox::Cancel); KAMessageBox::setDefaultShouldBeShownContinue(QUIT_WARN, default_quitWarn); KAMessageBox::setDefaultShouldBeShownContinue(EMAIL_QUEUED_NOTIFY, default_emailQueuedNotify); KAMessageBox::setDefaultShouldBeShownContinue(CONFIRM_ALARM_DELETION, default_confirmAlarmDeletion); mInstance = new Preferences; } return mInstance; } Preferences::Preferences() { QObject::connect(this, &Preferences::base_StartOfDayChanged, this, &Preferences::startDayChange); QObject::connect(this, &Preferences::base_TimeZoneChanged, this, &Preferences::timeZoneChange); QObject::connect(this, &Preferences::base_HolidayRegionChanged, this, &Preferences::holidaysChange); QObject::connect(this, &Preferences::base_WorkTimeChanged, this, &Preferences::workTimeChange); load(); // Fetch the KAlarm version and backend which wrote the previous config file mPreviousVersion = version(); mPreviousBackend = backend(); // Update the KAlarm version in the config file, but don't call // writeConfig() here - leave it to be written only if the config file // is updated with other data. setVersion(QStringLiteral(KALARM_VERSION)); } /****************************************************************************** * Auto hiding of the system tray icon is only allowed on desktops which provide * GUI controls to show hidden icons. */ int Preferences::autoHideSystemTray() { if(noAutoHideSystemTrayDesktops().contains(KAlarm::currentDesktopIdentityName())) return 0; // never hide return self()->mBase_AutoHideSystemTray; } /****************************************************************************** * Auto hiding of the system tray icon is only allowed on desktops which provide * GUI controls to show hidden icons, so while KAlarm is running on such a * desktop, don't allow changes to the setting. */ void Preferences::setAutoHideSystemTray(int timeout) { if(noAutoHideSystemTrayDesktops().contains(KAlarm::currentDesktopIdentityName())) return; self()->setBase_AutoHideSystemTray(timeout); } void Preferences::setAskAutoStart(bool yes) { KAMessageBox::saveDontShowAgainYesNo(ASK_AUTO_START, !yes); } /****************************************************************************** * Set the NoAutoStart condition. * On KDE desktops, the "X-KDE-autostart-condition" entry in * kalarm.autostart.desktop references this to determine whether to autostart KAlarm. * On non-KDE desktops, the "X-KDE-autostart-condition" entry in * kalarm.autostart.desktop doesn't have any effect, so that KAlarm will be * autostarted even if it is set not to autostart. Adding a "Hidden" entry to, * and removing the "OnlyShowIn=KDE" entry from, a user-modifiable copy of the * file fixes this. */ void Preferences::setNoAutoStart(bool yes) { // Find the existing kalarm.autostart.desktop file, and whether it's writable. bool existingRO = true; // whether the existing file is read-only QString autostartFile; const QStringList autostartDirs = QStandardPaths::standardLocations(QStandardPaths::GenericConfigLocation); for (const QString& dir : autostartDirs) { const QString file = dir + QLatin1String("/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; QString configDirRW; if (existingRO) { configDirRW = QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation); autostartFileRW = configDirRW + QLatin1String("/autostart/") + AUTOSTART_FILE; if (configDirRW.isEmpty()) { qCWarning(KALARM_LOG) << "Preferences::setNoAutoStart: No writable autostart file path"; return; } if (QFile::exists(autostartFileRW)) { QFileInfo info(autostartFileRW); if (!info.isReadable() || !info.isWritable()) { qCWarning(KALARM_LOG) << "Preferences::setNoAutoStart: Autostart file is not read/write:" << autostartFileRW; return; } } } // Read the existing file and remove any "Hidden=" and "OnlyShowIn=" entries bool update = false; QStringList lines; { QFile file(autostartFile); if (!file.open(QIODevice::ReadOnly)) { qCWarning(KALARM_LOG) << "Preferences::setNoAutoStart: Error reading autostart file:" << autostartFile; return; } QTextStream stream(&file); stream.setCodec("UTF-8"); stream.setAutoDetectUnicode(true); lines = stream.readAll().split(QLatin1Char('\n')); for (int i = 0; i < lines.size(); ++i) { const QString line = lines.at(i).trimmed(); if (line.isEmpty()) { lines.removeAt(i); --i; } else if (line.startsWith(QLatin1String("Hidden=")) || line.startsWith(QLatin1String("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 QFileInfo info(configDirRW + QLatin1String("/autostart")); if (!info.exists()) { // First, create the directory for it. if (!QDir(configDirRW).mkdir(QStringLiteral("autostart"))) { qCWarning(KALARM_LOG) << "Preferences::setNoAutoStart: Error creating autostart file directory:" << info.filePath(); return; } } QFile file(autostartFileRW); if (!file.open(QIODevice::WriteOnly)) { qCWarning(KALARM_LOG) << "Preferences::setNoAutoStart: Error writing autostart file:" << autostartFileRW; return; } QTextStream stream(&file); stream.setCodec("UTF-8"); stream << lines.join(QLatin1Char('\n')) << "\n"; qCDebug(KALARM_LOG) << "Preferences::setNoAutoStart: Written" << autostartFileRW; } self()->setBase_NoAutoStart(yes); } /****************************************************************************** * Get the user's time zone, or if none has been chosen, the system time zone. * Reply = time zone, or invalid to use the local time zone. */ KADateTime::Spec Preferences::timeSpec() { const QByteArray zoneId = self()->mBase_TimeZone.toLatin1(); return zoneId.isEmpty() ? KADateTime::LocalZone : KADateTime::Spec(QTimeZone(zoneId)); } QTimeZone Preferences::timeSpecAsZone() { const QByteArray zoneId = self()->mBase_TimeZone.toLatin1(); return zoneId.isEmpty() ? QTimeZone::systemTimeZone() : QTimeZone(zoneId); } void Preferences::setTimeSpec(const KADateTime::Spec& spec) { self()->setBase_TimeZone(spec.type() == KADateTime::TimeZone ? QString::fromLatin1(spec.timeZone().id()) : QString()); } void Preferences::timeZoneChange(const QString& zone) { Q_UNUSED(zone); Q_EMIT mInstance->timeZoneChanged(timeSpec()); } const HolidayRegion& Preferences::holidays() { QString regionCode = self()->mBase_HolidayRegion; if (!mHolidays || mHolidays->regionCode() != regionCode) { delete mHolidays; mHolidays = new HolidayRegion(regionCode); } return *mHolidays; } void Preferences::setHolidayRegion(const QString& regionCode) { self()->setBase_HolidayRegion(regionCode); } void Preferences::holidaysChange(const QString& regionCode) { Q_UNUSED(regionCode); Q_EMIT mInstance->holidaysChanged(holidays()); } void Preferences::setStartOfDay(const QTime& t) { if (t != self()->mBase_StartOfDay.time()) { self()->setBase_StartOfDay(QDateTime(QDate(1900,1,1), t)); Q_EMIT mInstance->startOfDayChanged(t); } } // Called when the start of day value has changed in the config file void Preferences::startDayChange(const QDateTime& dt) { Q_EMIT mInstance->startOfDayChanged(dt.time()); } QBitArray Preferences::workDays() { unsigned days = self()->base_WorkDays(); QBitArray dayBits(7); for (int i = 0; i < 7; ++i) dayBits.setBit(i, days & (1 << i)); return dayBits; } void Preferences::setWorkDays(const QBitArray& dayBits) { if (dayBits.size() != 7) { qCWarning(KALARM_LOG) << "Preferences::setWorkDays: Error! 'dayBits' parameter must have 7 elements: actual size" << dayBits.size(); return; } unsigned days = 0; for (int i = 0; i < 7; ++i) if (dayBits.testBit(i)) days |= 1 << i; self()->setBase_WorkDays(days); } void Preferences::workTimeChange(const QDateTime& start, const QDateTime& end, int days) { QBitArray dayBits(7); for (int i = 0; i < 7; ++i) if (days & (1 << i)) dayBits.setBit(i); Q_EMIT mInstance->workTimeChanged(start.time(), end.time(), dayBits); } Preferences::MailFrom Preferences::emailFrom() { const QString from = self()->mBase_EmailFrom; if (from == FROM_KMAIL) return MAIL_FROM_KMAIL; if (from == FROM_SYS_SETTINGS) return MAIL_FROM_SYS_SETTINGS; return MAIL_FROM_ADDR; } /****************************************************************************** * Get user's default 'From' email address. */ QString Preferences::emailAddress() { const QString from = self()->mBase_EmailFrom; if (from == FROM_KMAIL) return Identities::identityManager()->defaultIdentity().fullEmailAddr(); if (from == FROM_SYS_SETTINGS) return KAMail::controlCentreAddress(); return from; } void Preferences::setEmailAddress(Preferences::MailFrom from, const QString& address) { QString out; switch (from) { case MAIL_FROM_KMAIL: out = FROM_KMAIL; break; case MAIL_FROM_SYS_SETTINGS: out = FROM_SYS_SETTINGS; break; case MAIL_FROM_ADDR: out = address; break; default: return; } self()->setBase_EmailFrom(out); } Preferences::MailFrom Preferences::emailBccFrom() { const QString from = self()->mBase_EmailBccAddress; if (from == FROM_SYS_SETTINGS) return MAIL_FROM_SYS_SETTINGS; return MAIL_FROM_ADDR; } QString Preferences::emailBccAddress() { const QString from = self()->mBase_EmailBccAddress; if (from == FROM_SYS_SETTINGS) return KAMail::controlCentreAddress(); return from; } bool Preferences::emailBccUseSystemSettings() { return self()->mBase_EmailBccAddress == FROM_SYS_SETTINGS; } void Preferences::setEmailBccAddress(bool useSystemSettings, const QString& address) { QString out; if (useSystemSettings) out = FROM_SYS_SETTINGS; else out = address; self()->setBase_EmailBccAddress(out); } QString Preferences::cmdXTermCommand() { return translateXTermPath(self()->mBase_CmdXTermCommand, false); } void Preferences::setCmdXTermCommand(const QString& cmd) { self()->setBase_CmdXTermCommand(translateXTermPath(cmd, true)); } void Preferences::connect(const char* signal, const QObject* receiver, const char* member) { QObject::connect(self(), signal, receiver, member); } /****************************************************************************** * Called to allow or suppress output of the specified message dialog, where the * dialog has a checkbox to turn notification off. */ void Preferences::setNotify(const QString& messageID, bool notify) { KAMessageBox::saveDontShowAgainContinue(messageID, !notify); } /****************************************************************************** * Return whether the specified message dialog is output, where the dialog has * a checkbox to turn notification off. * Reply = false if message has been suppressed (by preferences or by selecting * "don't ask again") * = true in all other cases. */ bool Preferences::notifying(const QString& messageID) { return KAMessageBox::shouldBeShownContinue(messageID); } /****************************************************************************** * Translate an X terminal command path to/from config file format. * Note that only a home directory specification at the start of the path is * translated, so there's no need to worry about missing out some of the * executable's path due to quotes etc. * N.B. Calling KConfig::read/writePathEntry() on the entire command line * causes a crash on some systems, so it's necessary to extract the * executable path first before processing. */ QString translateXTermPath(const QString& cmdline, bool write) { QString params; QString cmd = cmdline; if (cmdline.isEmpty()) return cmdline; // Strip any leading quote const QChar quote = cmdline[0]; const char q = quote.toLatin1(); const bool quoted = (q == '"' || q == '\''); if (quoted) cmd = cmdline.mid(1); // Split the command at the first non-escaped space for (int i = 0, count = cmd.length(); i < count; ++i) { switch (cmd.at(i).toLatin1()) { case '\\': ++i; continue; case '"': case '\'': if (cmd.at(i) != quote) continue; // fall through to ' ' Q_FALLTHROUGH(); case ' ': params = cmd.mid(i); cmd.truncate(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 e538b990..71fc5737 100644 --- a/src/recurrenceedit.cpp +++ b/src/recurrenceedit.cpp @@ -1,1733 +1,1733 @@ /* * recurrenceedit.cpp - widget to edit the event's recurrence definition * Program: kalarm * Copyright © 2002-2019 David Jarvie * * Based originally on KOrganizer module koeditorrecurrence.cpp, * Copyright (c) 2000,2001 Cornelius Schumacher * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "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 "kalarm_debug.h" #include #include #include using namespace KCalendarCore; #include -#include -#include +#include +#include #include #include #include #include #include #include #include #include #include #include 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) , mReadOnly(readOnly) { qCDebug(KALARM_LOG) << "RecurrenceEdit:"; QVBoxLayout* topLayout = new QVBoxLayout(this); topLayout->setContentsMargins(0, 0, 0, 0); topLayout->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); /* Create the recurrence rule Group box which holds the recurrence period * selection buttons, and the weekly, monthly and yearly recurrence rule * frames which specify options individual to each of these distinct * sections of the recurrence rule. Each frame is made visible by the * selection of its corresponding radio button. */ QGroupBox* recurGroup = new QGroupBox(i18nc("@title:group", "Recurrence Rule"), this); topLayout->addWidget(recurGroup); QHBoxLayout* hlayout = new QHBoxLayout(recurGroup); int dcm = style()->pixelMetric(QStyle::PM_DefaultChildMargin); hlayout->setContentsMargins(dcm, dcm, dcm, dcm); hlayout->setSpacing(style()->pixelMetric(QStyle::PM_DefaultChildMargin)); // use margin spacing due to vertical divider line // Recurrence period radio buttons QVBoxLayout* vlayout = new QVBoxLayout(); vlayout->setSpacing(0); vlayout->setContentsMargins(0, 0, 0, 0); hlayout->addLayout(vlayout); mRuleButtonGroup = new ButtonGroup(recurGroup); connect(mRuleButtonGroup, &ButtonGroup::buttonSet, this, &RecurrenceEdit::periodClicked); connect(mRuleButtonGroup, &ButtonGroup::buttonSet, this, &RecurrenceEdit::contentsChanged); mNoneButton = new RadioButton(i18n_combo_NoRecur(), recurGroup); mNoneButton->setFixedSize(mNoneButton->sizeHint()); mNoneButton->setReadOnly(mReadOnly); mNoneButton->setWhatsThis(i18nc("@info:whatsthis", "Do not repeat the alarm")); mRuleButtonGroup->addButton(mNoneButton); vlayout->addWidget(mNoneButton); mAtLoginButton = new RadioButton(i18n_combo_AtLogin(), recurGroup); mAtLoginButton->setFixedSize(mAtLoginButton->sizeHint()); mAtLoginButton->setReadOnly(mReadOnly); mAtLoginButton->setWhatsThis(xi18nc("@info:whatsthis", "Trigger the alarm at the specified date/time and at every login until then." "Note that it will also be triggered any time KAlarm is restarted.")); mRuleButtonGroup->addButton(mAtLoginButton); vlayout->addWidget(mAtLoginButton); mSubDailyButton = new RadioButton(i18n_combo_HourlyMinutely(), recurGroup); mSubDailyButton->setFixedSize(mSubDailyButton->sizeHint()); mSubDailyButton->setReadOnly(mReadOnly); mSubDailyButton->setWhatsThis(i18nc("@info:whatsthis", "Repeat the alarm at hourly/minutely intervals")); mRuleButtonGroup->addButton(mSubDailyButton); vlayout->addWidget(mSubDailyButton); mDailyButton = new RadioButton(i18n_combo_Daily(), recurGroup); mDailyButton->setFixedSize(mDailyButton->sizeHint()); mDailyButton->setReadOnly(mReadOnly); mDailyButton->setWhatsThis(i18nc("@info:whatsthis", "Repeat the alarm at daily intervals")); mRuleButtonGroup->addButton(mDailyButton); vlayout->addWidget(mDailyButton); mWeeklyButton = new RadioButton(i18n_combo_Weekly(), recurGroup); mWeeklyButton->setFixedSize(mWeeklyButton->sizeHint()); mWeeklyButton->setReadOnly(mReadOnly); mWeeklyButton->setWhatsThis(i18nc("@info:whatsthis", "Repeat the alarm at weekly intervals")); mRuleButtonGroup->addButton(mWeeklyButton); vlayout->addWidget(mWeeklyButton); mMonthlyButton = new RadioButton(i18n_combo_Monthly(), recurGroup); mMonthlyButton->setFixedSize(mMonthlyButton->sizeHint()); mMonthlyButton->setReadOnly(mReadOnly); mMonthlyButton->setWhatsThis(i18nc("@info:whatsthis", "Repeat the alarm at monthly intervals")); mRuleButtonGroup->addButton(mMonthlyButton); vlayout->addWidget(mMonthlyButton); mYearlyButton = new RadioButton(i18n_combo_Yearly(), recurGroup); mYearlyButton->setFixedSize(mYearlyButton->sizeHint()); mYearlyButton->setReadOnly(mReadOnly); mYearlyButton->setWhatsThis(i18nc("@info:whatsthis", "Repeat the alarm at annual intervals")); mRuleButtonGroup->addButton(mYearlyButton); vlayout->addWidget(mYearlyButton); vlayout->addStretch(); // top-adjust the interval radio buttons // Sub-repetition button mSubRepetition = new RepetitionButton(i18nc("@action:button", "Sub-Repetition"), true, recurGroup); mSubRepetition->setFixedSize(mSubRepetition->sizeHint()); mSubRepetition->setReadOnly(mReadOnly); mSubRepetition->setWhatsThis(i18nc("@info:whatsthis", "Set up a repetition within the recurrence, to trigger the alarm multiple times each time the recurrence is due.")); connect(mSubRepetition, &RepetitionButton::needsInitialisation, this, &RecurrenceEdit::repeatNeedsInitialisation); connect(mSubRepetition, &RepetitionButton::changed, this, &RecurrenceEdit::frequencyChanged); connect(mSubRepetition, &RepetitionButton::changed, this, &RecurrenceEdit::contentsChanged); vlayout->addSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); vlayout->addWidget(mSubRepetition); // Vertical divider line vlayout = new QVBoxLayout(); vlayout->setContentsMargins(0, 0, 0, 0); hlayout->addLayout(vlayout); QFrame* divider = new QFrame(recurGroup); divider->setFrameStyle(QFrame::VLine | QFrame::Sunken); vlayout->addWidget(divider, 1); // Rule definition stack mRuleStack = new QStackedWidget(recurGroup); hlayout->addWidget(mRuleStack); hlayout->addStretch(1); mNoRule = new NoRule(mRuleStack); mSubDailyRule = new SubDailyRule(mReadOnly, mRuleStack); mDailyRule = new DailyRule(mReadOnly, mRuleStack); mWeeklyRule = new WeeklyRule(mReadOnly, mRuleStack); mMonthlyRule = new MonthlyRule(mReadOnly, mRuleStack); mYearlyRule = new YearlyRule(mReadOnly, mRuleStack); connect(mSubDailyRule, &SubDailyRule::frequencyChanged, this, &RecurrenceEdit::frequencyChanged); connect(mDailyRule, &DailyRule::frequencyChanged, this, &RecurrenceEdit::frequencyChanged); connect(mWeeklyRule, &WeeklyRule::frequencyChanged, this, &RecurrenceEdit::frequencyChanged); connect(mMonthlyRule, &MonthlyRule::frequencyChanged, this, &RecurrenceEdit::frequencyChanged); connect(mYearlyRule, &YearlyRule::frequencyChanged, this, &RecurrenceEdit::frequencyChanged); connect(mSubDailyRule, &SubDailyRule::changed, this, &RecurrenceEdit::contentsChanged); connect(mDailyRule, &DailyRule::changed, this, &RecurrenceEdit::contentsChanged); connect(mWeeklyRule, &WeeklyRule::changed, this, &RecurrenceEdit::contentsChanged); connect(mMonthlyRule, &MonthlyRule::changed, this, &RecurrenceEdit::contentsChanged); connect(mYearlyRule, &YearlyRule::changed, this, &RecurrenceEdit::contentsChanged); mRuleStack->addWidget(mNoRule); mRuleStack->addWidget(mSubDailyRule); mRuleStack->addWidget(mDailyRule); mRuleStack->addWidget(mWeeklyRule); mRuleStack->addWidget(mMonthlyRule); mRuleStack->addWidget(mYearlyRule); hlayout->addSpacing(style()->pixelMetric(QStyle::PM_DefaultChildMargin)); // Create the recurrence range group which contains the controls // which specify how long the recurrence is to last. mRangeButtonBox = new QGroupBox(i18nc("@title:group", "Recurrence End"), this); topLayout->addWidget(mRangeButtonBox); mRangeButtonGroup = new ButtonGroup(mRangeButtonBox); connect(mRangeButtonGroup, &ButtonGroup::buttonSet, this, &RecurrenceEdit::rangeTypeClicked); connect(mRangeButtonGroup, &ButtonGroup::buttonSet, this, &RecurrenceEdit::contentsChanged); vlayout = new QVBoxLayout(mRangeButtonBox); vlayout->setContentsMargins(dcm, dcm, dcm, dcm); vlayout->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); mNoEndDateButton = new RadioButton(i18nc("@option:radio", "No end"), mRangeButtonBox); mNoEndDateButton->setFixedSize(mNoEndDateButton->sizeHint()); mNoEndDateButton->setReadOnly(mReadOnly); mNoEndDateButton->setWhatsThis(i18nc("@info:whatsthis", "Repeat the alarm indefinitely")); mRangeButtonGroup->addButton(mNoEndDateButton); vlayout->addWidget(mNoEndDateButton, 1, Qt::AlignLeft); QSize size = mNoEndDateButton->size(); hlayout = new QHBoxLayout(); hlayout->setContentsMargins(0, 0, 0, 0); vlayout->addLayout(hlayout); mRepeatCountButton = new RadioButton(i18nc("@option:radio", "End after:"), mRangeButtonBox); mRepeatCountButton->setReadOnly(mReadOnly); mRepeatCountButton->setWhatsThis(i18nc("@info:whatsthis", "Repeat the alarm for the number of times specified")); mRangeButtonGroup->addButton(mRepeatCountButton); mRepeatCountEntry = new SpinBox(1, 9999, mRangeButtonBox); mRepeatCountEntry->setFixedSize(mRepeatCountEntry->sizeHint()); mRepeatCountEntry->setSingleShiftStep(10); mRepeatCountEntry->setSelectOnStep(false); mRepeatCountEntry->setReadOnly(mReadOnly); mRepeatCountEntry->setWhatsThis(i18nc("@info:whatsthis", "Enter the total number of times to trigger the alarm")); connect(mRepeatCountEntry, static_cast(&SpinBox::valueChanged), this, &RecurrenceEdit::repeatCountChanged); connect(mRepeatCountEntry, static_cast(&SpinBox::valueChanged), this, &RecurrenceEdit::contentsChanged); mRepeatCountButton->setFocusWidget(mRepeatCountEntry); mRepeatCountLabel = new QLabel(i18nc("@label", "occurrence(s)"), mRangeButtonBox); mRepeatCountLabel->setFixedSize(mRepeatCountLabel->sizeHint()); hlayout->addWidget(mRepeatCountButton); hlayout->addSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); hlayout->addWidget(mRepeatCountEntry); hlayout->addWidget(mRepeatCountLabel); hlayout->addStretch(); size = size.expandedTo(mRepeatCountButton->sizeHint()); hlayout = new QHBoxLayout(); hlayout->setContentsMargins(0, 0, 0, 0); vlayout->addLayout(hlayout); mEndDateButton = new RadioButton(i18nc("@option:radio", "End by:"), mRangeButtonBox); mEndDateButton->setReadOnly(mReadOnly); mEndDateButton->setWhatsThis( xi18nc("@info:whatsthis", "Repeat the alarm until the date/time specified." "This applies to the main recurrence only. It does not limit any sub-repetition which will occur regardless after the last main recurrence.")); mRangeButtonGroup->addButton(mEndDateButton); mEndDateEdit = new KDateComboBox(mRangeButtonBox); mEndDateEdit->setOptions(mReadOnly ? KDateComboBox::Options{} : KDateComboBox::EditDate | KDateComboBox::SelectDate | KDateComboBox::DatePicker); static const QString tzText = i18nc("@info", "This uses the same time zone as the start time."); mEndDateEdit->setWhatsThis(xi18nc("@info:whatsthis", "Enter the last date to repeat the alarm.%1", tzText)); connect(mEndDateEdit, &KDateComboBox::dateEdited, this, &RecurrenceEdit::contentsChanged); mEndDateButton->setFocusWidget(mEndDateEdit); mEndTimeEdit = new TimeEdit(mRangeButtonBox); mEndTimeEdit->setFixedSize(mEndTimeEdit->sizeHint()); mEndTimeEdit->setReadOnly(mReadOnly); mEndTimeEdit->setWhatsThis(xi18nc("@info:whatsthis", "Enter the last time to repeat the alarm.%1%2", tzText, TimeSpinBox::shiftWhatsThis())); connect(mEndTimeEdit, &TimeEdit::valueChanged, this, &RecurrenceEdit::contentsChanged); mEndAnyTimeCheckBox = new CheckBox(i18nc("@option:check", "Any time"), mRangeButtonBox); mEndAnyTimeCheckBox->setFixedSize(mEndAnyTimeCheckBox->sizeHint()); mEndAnyTimeCheckBox->setReadOnly(mReadOnly); mEndAnyTimeCheckBox->setWhatsThis(i18nc("@info:whatsthis", "Stop repeating the alarm after your first login on or after the specified end date")); connect(mEndAnyTimeCheckBox, &CheckBox::toggled, this, &RecurrenceEdit::slotAnyTimeToggled); connect(mEndAnyTimeCheckBox, &CheckBox::toggled, this, &RecurrenceEdit::contentsChanged); hlayout->addWidget(mEndDateButton); hlayout->addSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); hlayout->addWidget(mEndDateEdit); hlayout->addWidget(mEndTimeEdit); hlayout->addWidget(mEndAnyTimeCheckBox); hlayout->addStretch(); size = size.expandedTo(mEndDateButton->sizeHint()); // Line up the widgets to the right of the radio buttons mRepeatCountButton->setFixedSize(size); mEndDateButton->setFixedSize(size); // Create the exceptions group which specifies dates to be excluded // from the recurrence. mExceptionGroup = new QGroupBox(i18nc("@title:group", "Exceptions"), this); topLayout->addWidget(mExceptionGroup); topLayout->setStretchFactor(mExceptionGroup, 2); hlayout = new QHBoxLayout(mExceptionGroup); hlayout->setContentsMargins(dcm, dcm, dcm, dcm); hlayout->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); vlayout = new QVBoxLayout(); vlayout->setContentsMargins(0, 0, 0, 0); hlayout->addLayout(vlayout); mExceptionDateList = new ListWidget(mExceptionGroup); mExceptionDateList->setWhatsThis(i18nc("@info:whatsthis", "The list of exceptions, i.e. dates/times excluded from the recurrence")); connect(mExceptionDateList, &QListWidget::currentRowChanged, this, &RecurrenceEdit::enableExceptionButtons); vlayout->addWidget(mExceptionDateList); if (mReadOnly) { mExceptionDateEdit = nullptr; mChangeExceptionButton = nullptr; mDeleteExceptionButton = nullptr; } else { vlayout = new QVBoxLayout(); vlayout->setContentsMargins(0, 0, 0, 0); hlayout->addLayout(vlayout); mExceptionDateEdit = new KDateComboBox(mExceptionGroup); mExceptionDateEdit->setOptions(mReadOnly ? KDateComboBox::Options{} : KDateComboBox::EditDate | KDateComboBox::SelectDate | KDateComboBox::DatePicker); mExceptionDateEdit->setDate(KADateTime::currentLocalDate()); mExceptionDateEdit->setWhatsThis(i18nc("@info:whatsthis", "Enter a date to insert in the exceptions list. " "Use in conjunction with the Add or Change button below.")); vlayout->addWidget(mExceptionDateEdit, 0, Qt::AlignLeft); hlayout = new QHBoxLayout(); hlayout->setContentsMargins(0, 0, 0, 0); vlayout->addLayout(hlayout); QPushButton* button = new QPushButton(i18nc("@action:button", "Add"), mExceptionGroup); button->setWhatsThis(i18nc("@info:whatsthis", "Add the date entered above to the exceptions list")); connect(button, &QPushButton::clicked, this, &RecurrenceEdit::addException); hlayout->addWidget(button); mChangeExceptionButton = new QPushButton(i18nc("@action:button", "Change"), mExceptionGroup); mChangeExceptionButton->setWhatsThis(i18nc("@info:whatsthis", "Replace the currently highlighted item in the exceptions list with the date entered above")); connect(mChangeExceptionButton, &QPushButton::clicked, this, &RecurrenceEdit::changeException); hlayout->addWidget(mChangeExceptionButton); mDeleteExceptionButton = new QPushButton(i18nc("@action:button", "Delete"), mExceptionGroup); mDeleteExceptionButton->setWhatsThis(i18nc("@info:whatsthis", "Remove the currently highlighted item from the exceptions list")); connect(mDeleteExceptionButton, &QPushButton::clicked, this, &RecurrenceEdit::deleteException); hlayout->addWidget(mDeleteExceptionButton); } vlayout->addStretch(); mExcludeHolidays = new CheckBox(i18nc("@option:check", "Exclude holidays"), mExceptionGroup); mExcludeHolidays->setReadOnly(mReadOnly); mExcludeHolidays->setWhatsThis(xi18nc("@info:whatsthis", "Do not trigger the alarm on holidays." "You can specify your holiday region in the Configuration dialog.")); connect(mExcludeHolidays, &CheckBox::toggled, this, &RecurrenceEdit::contentsChanged); vlayout->addWidget(mExcludeHolidays); mWorkTimeOnly = new CheckBox(i18nc("@option:check", "Only during working time"), mExceptionGroup); mWorkTimeOnly->setReadOnly(mReadOnly); mWorkTimeOnly->setWhatsThis(xi18nc("@info:whatsthis", "Only execute the alarm during working hours, on working days." "You can specify working days and hours in the Configuration dialog.")); connect(mWorkTimeOnly, &CheckBox::toggled, this, &RecurrenceEdit::contentsChanged); vlayout->addWidget(mWorkTimeOnly); topLayout->addStretch(); mNoEmitTypeChanged = false; } /****************************************************************************** * Show or hide the exception controls. */ void RecurrenceEdit::showMoreOptions(bool more) { if (more) mExceptionGroup->show(); else mExceptionGroup->hide(); updateGeometry(); } /****************************************************************************** * Verify the consistency of the entered data. * Reply = widget to receive focus on error, or 0 if no error. */ QWidget* RecurrenceEdit::checkData(const KADateTime& startDateTime, QString& errorMessage) const { if (mAtLoginButton->isChecked()) return nullptr; const_cast(this)->mCurrStartDateTime = startDateTime; if (mEndDateButton->isChecked()) { // N.B. End date/time takes the same time spec as start date/time QWidget* errWidget = nullptr; bool noTime = !mEndTimeEdit->isEnabled(); QDate endDate = mEndDateEdit->date(); if (endDate < startDateTime.date()) errWidget = mEndDateEdit; else if (!noTime && KADateTime(endDate, mEndTimeEdit->time(), startDateTime.timeSpec()) < startDateTime) errWidget = mEndTimeEdit; if (errWidget) { errorMessage = noTime ? i18nc("@info", "End date is earlier than start date") : i18nc("@info", "End date/time is earlier than start date/time"); return errWidget; } } if (!mRule) return nullptr; return mRule->validate(errorMessage); } /****************************************************************************** * Called when a recurrence period radio button is clicked. */ void RecurrenceEdit::periodClicked(QAbstractButton* button) { const RepeatType oldType = mRuleButtonType; const bool none = (button == mNoneButton); const bool atLogin = (button == mAtLoginButton); const bool subdaily = (button == mSubDailyButton); if (none) { mRule = nullptr; mRuleButtonType = NO_RECUR; } else if (atLogin) { mRule = nullptr; mRuleButtonType = AT_LOGIN; mEndDateButton->setChecked(true); } else if (subdaily) { mRule = mSubDailyRule; mRuleButtonType = SUBDAILY; } else if (button == mDailyButton) { mRule = mDailyRule; mRuleButtonType = DAILY; mDailyShown = true; } else if (button == mWeeklyButton) { mRule = mWeeklyRule; mRuleButtonType = WEEKLY; mWeeklyShown = true; } else if (button == mMonthlyButton) { mRule = mMonthlyRule; mRuleButtonType = MONTHLY; mMonthlyShown = true; } else if (button == mYearlyButton) { mRule = mYearlyRule; mRuleButtonType = ANNUAL; mYearlyShown = true; } else return; if (mRuleButtonType != oldType) { mRuleStack->setCurrentWidget(mRule ? mRule : mNoRule); if (oldType == NO_RECUR || none) mRangeButtonBox->setEnabled(!none); mExceptionGroup->setEnabled(!(none || atLogin)); mEndAnyTimeCheckBox->setEnabled(atLogin); if (!none) { mNoEndDateButton->setEnabled(!atLogin); mRepeatCountButton->setEnabled(!atLogin); } rangeTypeClicked(); mSubRepetition->setEnabled(!(none || atLogin)); if (!mNoEmitTypeChanged) Q_EMIT typeChanged(mRuleButtonType); } } void RecurrenceEdit::slotAnyTimeToggled(bool on) { QAbstractButton* button = mRuleButtonGroup->checkedButton(); mEndTimeEdit->setEnabled((button == mAtLoginButton && !on) || (button == mSubDailyButton && mEndDateButton->isChecked())); } /****************************************************************************** * Called when a recurrence range type radio button is clicked. */ void RecurrenceEdit::rangeTypeClicked() { const bool endDate = mEndDateButton->isChecked(); mEndDateEdit->setEnabled(endDate); mEndTimeEdit->setEnabled(endDate && ((mAtLoginButton->isChecked() && !mEndAnyTimeCheckBox->isChecked()) || mSubDailyButton->isChecked())); const bool repeatCount = mRepeatCountButton->isChecked(); mRepeatCountEntry->setEnabled(repeatCount); mRepeatCountLabel->setEnabled(repeatCount); } void RecurrenceEdit::showEvent(QShowEvent*) { if (mRule) mRule->setFrequencyFocus(); else mRuleButtonGroup->checkedButton()->setFocus(); Q_EMIT shown(); } /****************************************************************************** * Return the sub-repetition interval and count within the recurrence, i.e. the * number of repetitions after the main recurrence. */ Repetition RecurrenceEdit::subRepetition() const { return (mRuleButtonType >= SUBDAILY) ? mSubRepetition->repetition() : Repetition(); } /****************************************************************************** * Called when the Sub-Repetition button has been pressed to display the * sub-repetition dialog. * Alarm repetition has the following restrictions: * 1) Not allowed for a repeat-at-login alarm * 2) For a date-only alarm, the repeat interval must be a whole number of days. * 3) The overall repeat duration must be less than the recurrence interval. */ void RecurrenceEdit::setSubRepetition(int reminderMinutes, bool dateOnly) { int maxDuration; switch (mRuleButtonType) { case RecurrenceEdit::NO_RECUR: case RecurrenceEdit::AT_LOGIN: // alarm repeat not allowed maxDuration = 0; break; default: // repeat duration must be less than recurrence interval { KAEvent event; updateEvent(event, false); maxDuration = event.longestRecurrenceInterval().asSeconds()/60 - reminderMinutes - 1; break; } } mSubRepetition->initialise(mSubRepetition->repetition(), dateOnly, maxDuration); mSubRepetition->setEnabled(mRuleButtonType >= SUBDAILY && maxDuration); } /****************************************************************************** * Activate the sub-repetition dialog. */ void RecurrenceEdit::activateSubRepetition() { mSubRepetition->activate(); } /****************************************************************************** * Called when the value of the repeat count field changes, to reset the * minimum value to 1 if the value was 0. */ void RecurrenceEdit::repeatCountChanged(int value) { if (value > 0 && mRepeatCountEntry->minimum() == 0) mRepeatCountEntry->setMinimum(1); } /****************************************************************************** * Add the date entered in the exception date edit control to the list of * exception dates. */ void RecurrenceEdit::addException() { if (!mExceptionDateEdit || !mExceptionDateEdit->date().isValid()) return; const QDate date = mExceptionDateEdit->date(); DateList::Iterator it; int index = 0; bool insert = true; for (it = mExceptionDates.begin(); it != mExceptionDates.end(); ++index, ++it) { if (date <= *it) { insert = (date != *it); break; } } if (insert) { mExceptionDates.insert(it, date); mExceptionDateList->insertItem(index, new QListWidgetItem(QLocale().toString(date, QLocale::LongFormat))); Q_EMIT contentsChanged(); } mExceptionDateList->setCurrentItem(mExceptionDateList->item(index)); enableExceptionButtons(); } /****************************************************************************** * Change the currently highlighted exception date to that entered in the * exception date edit control. */ void RecurrenceEdit::changeException() { if (!mExceptionDateEdit || !mExceptionDateEdit->date().isValid()) return; QListWidgetItem* item = mExceptionDateList->currentItem(); if (item && item->isSelected()) { const int index = mExceptionDateList->row(item); const QDate olddate = mExceptionDates.at(index); const QDate newdate = mExceptionDateEdit->date(); if (newdate != olddate) { mExceptionDates.removeAt(index); mExceptionDateList->takeItem(index); Q_EMIT contentsChanged(); addException(); } } } /****************************************************************************** * Delete the currently highlighted exception date. */ void RecurrenceEdit::deleteException() { QListWidgetItem* item = mExceptionDateList->currentItem(); if (item && item->isSelected()) { const int index = mExceptionDateList->row(item); mExceptionDates.removeAt(index); mExceptionDateList->takeItem(index); Q_EMIT contentsChanged(); enableExceptionButtons(); } } /****************************************************************************** * Enable/disable the exception group buttons according to whether any item is * selected in the exceptions listbox. */ void RecurrenceEdit::enableExceptionButtons() { QListWidgetItem* item = mExceptionDateList->currentItem(); const bool enable = item; if (mDeleteExceptionButton) mDeleteExceptionButton->setEnabled(enable); if (mChangeExceptionButton) mChangeExceptionButton->setEnabled(enable); // Prevent the exceptions list box receiving keyboard focus is it's empty mExceptionDateList->setFocusPolicy(mExceptionDateList->count() ? Qt::WheelFocus : Qt::NoFocus); } /****************************************************************************** * Notify this instance of a change in the alarm start date. */ void RecurrenceEdit::setStartDate(const QDate& start, const QDate& today) { if (!mReadOnly) { setRuleDefaults(start); if (start < today) { mEndDateEdit->setMinimumDate(today); if (mExceptionDateEdit) mExceptionDateEdit->setMinimumDate(today); } else { const QString startString = i18nc("@info", "Date cannot be earlier than start date"); mEndDateEdit->setMinimumDate(start, startString); if (mExceptionDateEdit) mExceptionDateEdit->setMinimumDate(start, startString); } } } /****************************************************************************** * Specify the default recurrence end date. */ void RecurrenceEdit::setDefaultEndDate(const QDate& end) { if (!mEndDateButton->isChecked()) mEndDateEdit->setDate(end); } void RecurrenceEdit::setEndDateTime(const KADateTime& end) { const KADateTime edt = end.toTimeSpec(mCurrStartDateTime.timeSpec()); mEndDateEdit->setDate(edt.date()); mEndTimeEdit->setValue(edt.time()); mEndTimeEdit->setEnabled(!end.isDateOnly()); mEndAnyTimeCheckBox->setChecked(end.isDateOnly()); } KADateTime RecurrenceEdit::endDateTime() const { if (mRuleButtonGroup->checkedButton() == mAtLoginButton && mEndAnyTimeCheckBox->isChecked()) return KADateTime(mEndDateEdit->date(), mCurrStartDateTime.timeSpec()); return KADateTime(mEndDateEdit->date(), mEndTimeEdit->time(), mCurrStartDateTime.timeSpec()); } /****************************************************************************** * Set all controls to their default values. */ void RecurrenceEdit::setDefaults(const KADateTime& from) { mCurrStartDateTime = from; const QDate fromDate = from.date(); mNoEndDateButton->setChecked(true); mSubDailyRule->setFrequency(1); mDailyRule->setFrequency(1); mWeeklyRule->setFrequency(1); mMonthlyRule->setFrequency(1); mYearlyRule->setFrequency(1); setRuleDefaults(fromDate); mMonthlyRule->setType(MonthYearRule::DATE); // date in month mYearlyRule->setType(MonthYearRule::DATE); // date in year mEndDateEdit->setDate(fromDate); mNoEmitTypeChanged = true; RadioButton* button; switch (Preferences::defaultRecurPeriod()) { case Preferences::Recur_Login: button = mAtLoginButton; break; case Preferences::Recur_Yearly: button = mYearlyButton; break; case Preferences::Recur_Monthly: button = mMonthlyButton; break; case Preferences::Recur_Weekly: button = mWeeklyButton; break; case Preferences::Recur_Daily: button = mDailyButton; break; case Preferences::Recur_SubDaily: button = mSubDailyButton; break; case Preferences::Recur_None: default: button = mNoneButton; break; } button->setChecked(true); mNoEmitTypeChanged = false; rangeTypeClicked(); enableExceptionButtons(); saveState(); } /****************************************************************************** * Set the controls for weekly, monthly and yearly rules which have not so far * been shown, to their default values, depending on the recurrence start date. */ void RecurrenceEdit::setRuleDefaults(const QDate& fromDate) { const int day = fromDate.day(); const int dayOfWeek = fromDate.dayOfWeek(); const int month = fromDate.month(); if (!mDailyShown) mDailyRule->setDays(true); if (!mWeeklyShown) mWeeklyRule->setDay(dayOfWeek); if (!mMonthlyShown) mMonthlyRule->setDefaultValues(day, dayOfWeek); if (!mYearlyShown) mYearlyRule->setDefaultValues(day, dayOfWeek, month); } /****************************************************************************** * Initialise the recurrence to select repeat-at-login. * This function and set() are mutually exclusive: call one or the other, not both. */ void RecurrenceEdit::setRepeatAtLogin() { mAtLoginButton->setChecked(true); mEndDateButton->setChecked(true); } /****************************************************************************** * Set the state of all controls to reflect the data in the specified event. */ void RecurrenceEdit::set(const KAEvent& event) { setDefaults(event.mainDateTime().kDateTime()); if (event.repeatAtLogin()) { mAtLoginButton->setChecked(true); mEndDateButton->setChecked(true); return; } mNoneButton->setChecked(true); KARecurrence* recurrence = event.recurrence(); if (!recurrence) return; const KARecurrence::Type rtype = recurrence->type(); switch (rtype) { case KARecurrence::MINUTELY: mSubDailyButton->setChecked(true); break; case KARecurrence::DAILY: { mDailyButton->setChecked(true); const QBitArray rDays = recurrence->days(); if (rDays.count(true)) mDailyRule->setDays(rDays); else mDailyRule->setDays(true); break; } case KARecurrence::WEEKLY: { mWeeklyButton->setChecked(true); const QBitArray rDays = recurrence->days(); mWeeklyRule->setDays(rDays); break; } case KARecurrence::MONTHLY_POS: // on nth (Tuesday) of the month { const QList posns = recurrence->monthPositions(); int i = posns.first().pos(); if (!i) { // It's every (Tuesday) of the month. Convert to a weekly recurrence // (but ignoring any non-every xxxDay positions). mWeeklyButton->setChecked(true); mWeeklyRule->setFrequency(recurrence->frequency()); QBitArray rDays(7); for (const RecurrenceRule::WDayPos& posn : posns) { if (!posn.pos()) rDays.setBit(posn.day() - 1, 1); } mWeeklyRule->setDays(rDays); break; } mMonthlyButton->setChecked(true); mMonthlyRule->setPosition(i, posns.first().day()); break; } case KARecurrence::MONTHLY_DAY: // on nth day of the month { mMonthlyButton->setChecked(true); const QList rmd = recurrence->monthDays(); const int day = (rmd.isEmpty()) ? event.mainDateTime().date().day() : rmd.first(); mMonthlyRule->setDate(day); break; } case KARecurrence::ANNUAL_DATE: // on the nth day of (months...) in the year case KARecurrence::ANNUAL_POS: // on the nth (Tuesday) of (months...) in the year { if (rtype == KARecurrence::ANNUAL_DATE) { mYearlyButton->setChecked(true); const QList rmd = recurrence->monthDays(); const int day = (rmd.isEmpty()) ? event.mainDateTime().date().day() : rmd.first(); mYearlyRule->setDate(day); mYearlyRule->setFeb29Type(recurrence->feb29Type()); } else if (rtype == KARecurrence::ANNUAL_POS) { mYearlyButton->setChecked(true); const QList posns = recurrence->yearPositions(); mYearlyRule->setPosition(posns.first().pos(), posns.first().day()); } mYearlyRule->setMonths(recurrence->yearMonths()); break; } default: return; } mRule->setFrequency(recurrence->frequency()); // Get range information KADateTime endtime = mCurrStartDateTime; const int duration = recurrence->duration(); if (duration == -1) mNoEndDateButton->setChecked(true); else if (duration) { mRepeatCountButton->setChecked(true); mRepeatCountEntry->setValue(duration); } else { mEndDateButton->setChecked(true); endtime = recurrence->endDateTime(); mEndTimeEdit->setValue(endtime.time()); } mEndDateEdit->setDate(endtime.date()); // Get exception information mExceptionDates = event.recurrence()->exDates(); std::sort(mExceptionDates.begin(), mExceptionDates.end()); mExceptionDateList->clear(); for (const QDate& exceptionDate : mExceptionDates) new QListWidgetItem(QLocale().toString(exceptionDate, QLocale::LongFormat), mExceptionDateList); enableExceptionButtons(); mExcludeHolidays->setChecked(event.holidaysExcluded()); mWorkTimeOnly->setChecked(event.workTimeOnly()); // Get repetition within recurrence mSubRepetition->set(event.repetition()); rangeTypeClicked(); saveState(); } /****************************************************************************** * Update the specified KAEvent with the entered recurrence data. * If 'adjustStart' is true, the start date/time will be adjusted if necessary * to be the first date/time which recurs on or after the original start. */ void RecurrenceEdit::updateEvent(KAEvent& event, bool adjustStart) { // Get end date and repeat count, common to all types of recurring events QDate endDate; QTime endTime; int repeatCount; if (mNoEndDateButton->isChecked()) repeatCount = -1; else if (mRepeatCountButton->isChecked()) repeatCount = mRepeatCountEntry->value(); else { repeatCount = 0; endDate = mEndDateEdit->date(); endTime = mEndTimeEdit->time(); } // Set up the recurrence according to the type selected event.startChanges(); QAbstractButton* button = mRuleButtonGroup->checkedButton(); event.setRepeatAtLogin(button == mAtLoginButton); const int frequency = mRule ? mRule->frequency() : 0; if (button == mSubDailyButton) { const KADateTime endDateTime(endDate, endTime, mCurrStartDateTime.timeSpec()); event.setRecurMinutely(frequency, repeatCount, endDateTime); } else if (button == mDailyButton) { event.setRecurDaily(frequency, mDailyRule->days(), repeatCount, endDate); } else if (button == mWeeklyButton) { event.setRecurWeekly(frequency, mWeeklyRule->days(), repeatCount, endDate); } else if (button == mMonthlyButton) { if (mMonthlyRule->type() == MonthlyRule::POS) { // It's by position KAEvent::MonthPos pos; pos.days.fill(false); pos.days.setBit(mMonthlyRule->dayOfWeek() - 1); pos.weeknum = mMonthlyRule->week(); QVector poses(1, pos); event.setRecurMonthlyByPos(frequency, poses, repeatCount, endDate); } else { // It's by day const int daynum = mMonthlyRule->date(); QVector daynums(1, daynum); event.setRecurMonthlyByDate(frequency, daynums, repeatCount, endDate); } } else if (button == mYearlyButton) { const QVector months = mYearlyRule->months(); if (mYearlyRule->type() == YearlyRule::POS) { // It's by position KAEvent::MonthPos pos; pos.days.fill(false); pos.days.setBit(mYearlyRule->dayOfWeek() - 1); pos.weeknum = mYearlyRule->week(); QVector poses(1, pos); event.setRecurAnnualByPos(frequency, poses, months, repeatCount, endDate); } else { // It's by date in month event.setRecurAnnualByDate(frequency, months, mYearlyRule->date(), mYearlyRule->feb29Type(), repeatCount, endDate); } } else { event.setNoRecur(); event.endChanges(); return; } if (!event.recurs()) { event.endChanges(); return; // an error occurred setting up the recurrence } if (adjustStart) event.setFirstRecurrence(); // Set up repetition within the recurrence // N.B. This requires the main recurrence to be set up first. event.setRepetition((mRuleButtonType < SUBDAILY) ? Repetition() : mSubRepetition->repetition()); // Set up exceptions event.recurrence()->setExDates(mExceptionDates); event.setWorkTimeOnly(mWorkTimeOnly->isChecked()); event.setExcludeHolidays(mExcludeHolidays->isChecked()); event.endChanges(); } /****************************************************************************** * Save the state of all controls. */ void RecurrenceEdit::saveState() { mSavedRuleButton = mRuleButtonGroup->checkedButton(); if (mRule) mRule->saveState(); mSavedRangeButton = mRangeButtonGroup->checkedButton(); if (mSavedRangeButton == mRepeatCountButton) mSavedRecurCount = mRepeatCountEntry->value(); else if (mSavedRangeButton == mEndDateButton) { mSavedEndDateTime = KADateTime(mEndDateEdit->date(), mEndTimeEdit->time(), mCurrStartDateTime.timeSpec()); mSavedEndDateTime.setDateOnly(mEndAnyTimeCheckBox->isChecked()); } mSavedExceptionDates = mExceptionDates; mSavedWorkTimeOnly = mWorkTimeOnly->isChecked(); mSavedExclHolidays = mExcludeHolidays->isChecked(); mSavedRepetition = mSubRepetition->repetition(); } /****************************************************************************** * Check whether any of the controls have changed state since initialisation. */ bool RecurrenceEdit::stateChanged() const { if (mSavedRuleButton != mRuleButtonGroup->checkedButton() || mSavedRangeButton != mRangeButtonGroup->checkedButton() || (mRule && mRule->stateChanged())) return true; if (mSavedRangeButton == mRepeatCountButton && mSavedRecurCount != mRepeatCountEntry->value()) return true; if (mSavedRangeButton == mEndDateButton) { KADateTime edt(mEndDateEdit->date(), mEndTimeEdit->time(), mCurrStartDateTime.timeSpec()); edt.setDateOnly(mEndAnyTimeCheckBox->isChecked()); if (mSavedEndDateTime != edt) return true; } if (mSavedExceptionDates != mExceptionDates || mSavedWorkTimeOnly != mWorkTimeOnly->isChecked() || mSavedExclHolidays != mExcludeHolidays->isChecked() || mSavedRepetition != mSubRepetition->repetition()) return true; return false; } /*============================================================================= = Class Rule = Base class for rule widgets, including recurrence frequency. =============================================================================*/ Rule::Rule(const QString& freqText, const QString& freqWhatsThis, bool time, bool readOnly, QWidget* parent) : NoRule(parent) { mLayout = new QVBoxLayout(this); mLayout->setContentsMargins(0, 0, 0, 0); mLayout->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); QHBoxLayout* freqLayout = new QHBoxLayout(); freqLayout->setContentsMargins(0, 0, 0, 0); freqLayout->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); mLayout->addLayout(freqLayout); QWidget* box = new QWidget(this); // this is to control the QWhatsThis text display area freqLayout->addWidget(box, 0, Qt::AlignLeft); QHBoxLayout* boxLayout = new QHBoxLayout(box); boxLayout->setContentsMargins(0, 0, 0, 0); QLabel* label = new QLabel(i18nc("@label:spinbox", "Recur e&very"), box); label->setFixedSize(label->sizeHint()); boxLayout->addWidget(label, 0, Qt::AlignLeft); if (time) { mIntSpinBox = nullptr; mSpinBox = mTimeSpinBox = new TimeSpinBox(1, 5999, box); mTimeSpinBox->setFixedSize(mTimeSpinBox->sizeHint()); mTimeSpinBox->setReadOnly(readOnly); boxLayout->addWidget(mSpinBox, 0, Qt::AlignLeft); } else { mTimeSpinBox = nullptr; mSpinBox = mIntSpinBox = new SpinBox(1, 999, box); mIntSpinBox->setFixedSize(mIntSpinBox->sizeHint()); mIntSpinBox->setReadOnly(readOnly); boxLayout->addWidget(mSpinBox, 0, Qt::AlignLeft); } connect(mSpinBox, SIGNAL(valueChanged(int)), SIGNAL(frequencyChanged())); connect(mSpinBox, SIGNAL(valueChanged(int)), SIGNAL(changed())); label->setBuddy(mSpinBox); label = new QLabel(freqText, box); label->setFixedSize(label->sizeHint()); boxLayout->addWidget(label, 0, Qt::AlignLeft); box->setFixedSize(sizeHint()); box->setWhatsThis(freqWhatsThis); } int Rule::frequency() const { if (mIntSpinBox) return mIntSpinBox->value(); if (mTimeSpinBox) return mTimeSpinBox->value(); return 0; } void Rule::setFrequency(int n) { if (mIntSpinBox) mIntSpinBox->setValue(n); if (mTimeSpinBox) mTimeSpinBox->setValue(n); } /****************************************************************************** * Save the state of all controls. */ void Rule::saveState() { mSavedFrequency = frequency(); } /****************************************************************************** * Check whether any of the controls have changed state since initialisation. */ bool Rule::stateChanged() const { return (mSavedFrequency != frequency()); } /*============================================================================= = Class SubDailyRule = Sub-daily rule widget. =============================================================================*/ SubDailyRule::SubDailyRule(bool readOnly, QWidget* parent) : Rule(i18nc("@label Time units for user-entered numbers", "hours:minutes"), i18nc("@info:whatsthis", "Enter the number of hours and minutes between repetitions of the alarm"), true, readOnly, parent) { } /*============================================================================= = Class DayWeekRule = Daily/weekly rule widget base class. =============================================================================*/ DayWeekRule::DayWeekRule(const QString& freqText, const QString& freqWhatsThis, const QString& daysWhatsThis, bool readOnly, QWidget* parent) : Rule(freqText, freqWhatsThis, false, readOnly, parent) , mSavedDays(7) { QGridLayout* grid = new QGridLayout(); grid->setContentsMargins(0, 0, 0, 0); grid->setRowStretch(0, 1); layout()->addLayout(grid); QLabel* label = new QLabel(i18nc("@label On: Tuesday", "O&n:"), this); label->setFixedSize(label->sizeHint()); grid->addWidget(label, 0, 0, Qt::AlignRight | Qt::AlignTop); grid->setColumnMinimumWidth(1, style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); // List the days of the week starting at the user's start day of the week. // Save the first day of the week, just in case it changes while the dialog is open. QWidget* box = new QWidget(this); // this is to control the QWhatsThis text display area QGridLayout* dgrid = new QGridLayout(box); dgrid->setContentsMargins(0, 0, 0, 0); dgrid->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); QLocale locale; for (int i = 0; i < 7; ++i) { const int day = KAlarm::localeDayInWeek_to_weekDay(i); mDayBox[i] = new CheckBox(locale.dayName(day), box); mDayBox[i]->setFixedSize(mDayBox[i]->sizeHint()); mDayBox[i]->setReadOnly(readOnly); connect(mDayBox[i], &QAbstractButton::toggled, this, &Rule::changed); dgrid->addWidget(mDayBox[i], i%4, i/4, Qt::AlignLeft); } box->setFixedSize(box->sizeHint()); box->setWhatsThis(daysWhatsThis); grid->addWidget(box, 0, 2, Qt::AlignLeft); label->setBuddy(mDayBox[0]); grid->setColumnStretch(3, 1); } /****************************************************************************** * Fetch which days of the week have been ticked. */ QBitArray DayWeekRule::days() const { QBitArray ds(7); ds.fill(false); for (int i = 0; i < 7; ++i) if (mDayBox[i]->isChecked()) ds.setBit(KAlarm::localeDayInWeek_to_weekDay(i) - 1, 1); return ds; } /****************************************************************************** * Tick/untick every day of the week. */ void DayWeekRule::setDays(bool tick) { for (int i = 0; i < 7; ++i) mDayBox[i]->setChecked(tick); } /****************************************************************************** * Tick/untick each day of the week according to the specified bits. */ void DayWeekRule::setDays(const QBitArray& days) { if (days.size() != 7) { qCWarning(KALARM_LOG) << "DayWeekRule::setDays: Error! 'days' parameter must have 7 elements: actual size" << days.size(); return; } for (int i = 0; i < 7; ++i) { bool x = days.testBit(KAlarm::localeDayInWeek_to_weekDay(i) - 1); mDayBox[i]->setChecked(x); } } /****************************************************************************** * Tick the specified day of the week, and untick all other days. */ void DayWeekRule::setDay(int dayOfWeek) { for (int i = 0; i < 7; ++i) mDayBox[i]->setChecked(false); if (dayOfWeek > 0 && dayOfWeek <= 7) mDayBox[KAlarm::weekDay_to_localeDayInWeek(dayOfWeek)]->setChecked(true); } /****************************************************************************** * Validate: check that at least one day is selected. */ QWidget* DayWeekRule::validate(QString& errorMessage) { for (int i = 0; i < 7; ++i) if (mDayBox[i]->isChecked()) return nullptr; errorMessage = i18nc("@info", "No day selected"); return mDayBox[0]; } /****************************************************************************** * Save the state of all controls. */ void DayWeekRule::saveState() { Rule::saveState(); mSavedDays = days(); } /****************************************************************************** * Check whether any of the controls have changed state since initialisation. */ bool DayWeekRule::stateChanged() const { return (Rule::stateChanged() || mSavedDays != days()); } /*============================================================================= = Class DailyRule = Daily rule widget. =============================================================================*/ DailyRule::DailyRule(bool readOnly, QWidget* parent) : DayWeekRule(i18nc("@label Time unit for user-entered number", "day(s)"), i18nc("@info:whatsthis", "Enter the number of days between repetitions of the alarm"), i18nc("@info:whatsthis", "Select the days of the week on which the alarm is allowed to occur"), readOnly, parent) { } /*============================================================================= = Class WeeklyRule = Weekly rule widget. =============================================================================*/ WeeklyRule::WeeklyRule(bool readOnly, QWidget* parent) : DayWeekRule(i18nc("@label Time unit for user-entered number", "week(s)"), i18nc("@info:whatsthis", "Enter the number of weeks between repetitions of the alarm"), i18nc("@info:whatsthis", "Select the days of the week on which to repeat the alarm"), readOnly, parent) { } /*============================================================================= = Class MonthYearRule = Monthly/yearly rule widget base class. =============================================================================*/ MonthYearRule::MonthYearRule(const QString& freqText, const QString& freqWhatsThis, bool allowEveryWeek, bool readOnly, QWidget* parent) : Rule(freqText, freqWhatsThis, false, readOnly, parent) , mEveryWeek(allowEveryWeek) { mButtonGroup = new ButtonGroup(this); // Month day selector QGridLayout* boxLayout = new QGridLayout(); boxLayout->setContentsMargins(0, 0, 0, 0); boxLayout->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); layout()->addLayout(boxLayout); mDayButton = new RadioButton(i18nc("@option:radio On day number in the month", "O&n day"), this); mDayButton->setFixedSize(mDayButton->sizeHint()); mDayButton->setReadOnly(readOnly); mButtonGroup->addButton(mDayButton); mDayButton->setWhatsThis(i18nc("@info:whatsthis", "Repeat the alarm on the selected day of the month")); boxLayout->addWidget(mDayButton, 0, 0); mDayCombo = new ComboBox(this); mDayCombo->setEditable(false); mDayCombo->setMaxVisibleItems(11); for (int i = 0; i < 31; ++i) mDayCombo->addItem(QString::number(i + 1)); mDayCombo->addItem(i18nc("@item:inlistbox Last day of month", "Last")); mDayCombo->setFixedSize(mDayCombo->sizeHint()); mDayCombo->setReadOnly(readOnly); mDayCombo->setWhatsThis(i18nc("@info:whatsthis", "Select the day of the month on which to repeat the alarm")); mDayButton->setFocusWidget(mDayCombo); connect(mDayCombo, static_cast(&ComboBox::activated), this, &MonthYearRule::slotDaySelected); connect(mDayCombo, static_cast(&ComboBox::currentIndexChanged), this, &MonthYearRule::changed); boxLayout->addWidget(mDayCombo, 0, 1, 1, 2, Qt::AlignLeft); // Month position selector mPosButton = new RadioButton(i18nc("@option:radio On the 1st Tuesday", "On t&he"), this); mPosButton->setFixedSize(mPosButton->sizeHint()); mPosButton->setReadOnly(readOnly); mButtonGroup->addButton(mPosButton); mPosButton->setWhatsThis(i18nc("@info:whatsthis", "Repeat the alarm on one day of the week, in the selected week of the month")); boxLayout->addWidget(mPosButton, 1, 0); mWeekCombo = new ComboBox(this); mWeekCombo->setEditable(false); mWeekCombo->addItem(i18nc("@item:inlistbox", "1st")); mWeekCombo->addItem(i18nc("@item:inlistbox", "2nd")); mWeekCombo->addItem(i18nc("@item:inlistbox", "3rd")); mWeekCombo->addItem(i18nc("@item:inlistbox", "4th")); mWeekCombo->addItem(i18nc("@item:inlistbox", "5th")); mWeekCombo->addItem(i18nc("@item:inlistbox Last Monday in March", "Last")); mWeekCombo->addItem(i18nc("@item:inlistbox", "2nd Last")); mWeekCombo->addItem(i18nc("@item:inlistbox", "3rd Last")); mWeekCombo->addItem(i18nc("@item:inlistbox", "4th Last")); mWeekCombo->addItem(i18nc("@item:inlistbox", "5th Last")); if (mEveryWeek) { mWeekCombo->addItem(i18nc("@item:inlistbox Every (Monday...) in month", "Every")); mWeekCombo->setMaxVisibleItems(11); } mWeekCombo->setWhatsThis(i18nc("@info:whatsthis", "Select the week of the month in which to repeat the alarm")); mWeekCombo->setFixedSize(mWeekCombo->sizeHint()); mWeekCombo->setReadOnly(readOnly); mPosButton->setFocusWidget(mWeekCombo); connect(mWeekCombo, static_cast(&ComboBox::currentIndexChanged), this, &MonthYearRule::changed); boxLayout->addWidget(mWeekCombo, 1, 1); mDayOfWeekCombo = new ComboBox(this); mDayOfWeekCombo->setEditable(false); QLocale locale; for (int i = 0; i < 7; ++i) { int day = KAlarm::localeDayInWeek_to_weekDay(i); mDayOfWeekCombo->addItem(locale.dayName(day)); } mDayOfWeekCombo->setReadOnly(readOnly); mDayOfWeekCombo->setWhatsThis(i18nc("@info:whatsthis", "Select the day of the week on which to repeat the alarm")); connect(mDayOfWeekCombo, static_cast(&ComboBox::currentIndexChanged), this, &MonthYearRule::changed); boxLayout->addWidget(mDayOfWeekCombo, 1, 2, Qt::AlignLeft); connect(mButtonGroup, &ButtonGroup::buttonSet, this, &MonthYearRule::clicked); connect(mButtonGroup, &ButtonGroup::buttonSet, this, &MonthYearRule::changed); } MonthYearRule::DayPosType MonthYearRule::type() const { return (mButtonGroup->checkedButton() == mDayButton) ? DATE : POS; } void MonthYearRule::setType(MonthYearRule::DayPosType type) { if (type == DATE) mDayButton->setChecked(true); else mPosButton->setChecked(true); } void MonthYearRule::setDefaultValues(int dayOfMonth, int dayOfWeek) { --dayOfMonth; mDayCombo->setCurrentIndex(dayOfMonth); mWeekCombo->setCurrentIndex(dayOfMonth / 7); mDayOfWeekCombo->setCurrentIndex(KAlarm::weekDay_to_localeDayInWeek(dayOfWeek)); } int MonthYearRule::date() const { 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) { const bool date = (type == DATE); mDayCombo->setEnabled(date); mWeekCombo->setEnabled(!date); mDayOfWeekCombo->setEnabled(!date); } void MonthYearRule::clicked(QAbstractButton* button) { enableSelection(button == mDayButton ? DATE : POS); } void MonthYearRule::slotDaySelected(int index) { daySelected(index <= 30 ? index + 1 : 30 - index); } /****************************************************************************** * Save the state of all controls. */ void MonthYearRule::saveState() { Rule::saveState(); mSavedType = type(); if (mSavedType == DATE) mSavedDay = date(); else { mSavedWeek = week(); mSavedWeekDay = dayOfWeek(); } } /****************************************************************************** * Check whether any of the controls have changed state since initialisation. */ bool MonthYearRule::stateChanged() const { if (Rule::stateChanged() || mSavedType != type()) return true; if (mSavedType == DATE) { if (mSavedDay != date()) return true; } else { if (mSavedWeek != week() || mSavedWeekDay != dayOfWeek()) return true; } return false; } /*============================================================================= = Class MonthlyRule = Monthly rule widget. =============================================================================*/ MonthlyRule::MonthlyRule(bool readOnly, QWidget* parent) : MonthYearRule(i18nc("@label Time unit for user-entered number", "month(s)"), i18nc("@info:whatsthis", "Enter the number of months between repetitions of the alarm"), false, readOnly, parent) { } /*============================================================================= = Class YearlyRule = Yearly rule widget. =============================================================================*/ YearlyRule::YearlyRule(bool readOnly, QWidget* parent) : MonthYearRule(i18nc("@label Time unit for user-entered number", "year(s)"), i18nc("@info:whatsthis", "Enter the number of years between repetitions of the alarm"), true, readOnly, parent) { // Set up the month selection widgets QHBoxLayout* hlayout = new QHBoxLayout(); hlayout->setContentsMargins(0, 0, 0, 0); layout()->addLayout(hlayout); QLabel* label = new QLabel(i18nc("@label List of months to select", "Months:"), this); label->setFixedSize(label->sizeHint()); hlayout->addWidget(label, 0, Qt::AlignLeft | Qt::AlignTop); // List the months of the year. QWidget* w = new QWidget(this); // this is to control the QWhatsThis text display area hlayout->addWidget(w, 1, Qt::AlignLeft); QGridLayout* grid = new QGridLayout(w); grid->setContentsMargins(0, 0, 0, 0); grid->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); QLocale locale; for (int i = 0; i < 12; ++i) { mMonthBox[i] = new CheckBox(locale.monthName(i + 1, QLocale::ShortFormat), w); mMonthBox[i]->setFixedSize(mMonthBox[i]->sizeHint()); mMonthBox[i]->setReadOnly(readOnly); connect(mMonthBox[i], &QAbstractButton::toggled, this, &Rule::changed); grid->addWidget(mMonthBox[i], i%3, i/3, Qt::AlignLeft); } connect(mMonthBox[1], &QAbstractButton::toggled, this, &YearlyRule::enableFeb29); w->setFixedHeight(w->sizeHint().height()); w->setWhatsThis(i18nc("@info:whatsthis", "Select the months of the year in which to repeat the alarm")); // February 29th handling option QHBoxLayout* f29box = new QHBoxLayout; layout()->addLayout(f29box); w = new QWidget(this); // this is to control the QWhatsThis text display area f29box->addWidget(w, 0, Qt::AlignLeft); QHBoxLayout* boxLayout = new QHBoxLayout(w); boxLayout->setContentsMargins(0, 0, 0, 0); boxLayout->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); mFeb29Label = new QLabel(i18nc("@label:listbox", "February 2&9th alarm in non-leap years:")); mFeb29Label->setFixedSize(mFeb29Label->sizeHint()); boxLayout->addWidget(mFeb29Label); mFeb29Combo = new ComboBox(); mFeb29Combo->setEditable(false); mFeb29Combo->addItem(i18nc("@item:inlistbox No date", "None")); mFeb29Combo->addItem(i18nc("@item:inlistbox 1st March (short form)", "1 Mar")); mFeb29Combo->addItem(i18nc("@item:inlistbox 28th February (short form)", "28 Feb")); mFeb29Combo->setFixedSize(mFeb29Combo->sizeHint()); mFeb29Combo->setReadOnly(readOnly); connect(mFeb29Combo, static_cast(&ComboBox::currentIndexChanged), this, &YearlyRule::changed); mFeb29Label->setBuddy(mFeb29Combo); boxLayout->addWidget(mFeb29Combo); w->setFixedSize(w->sizeHint()); w->setWhatsThis(i18nc("@info:whatsthis", "Select which date, if any, the February 29th alarm should trigger in non-leap years")); } void YearlyRule::setDefaultValues(int dayOfMonth, int dayOfWeek, int month) { MonthYearRule::setDefaultValues(dayOfMonth, dayOfWeek); --month; for (int i = 0; i < 12; ++i) mMonthBox[i]->setChecked(i == month); setFeb29Type(KARecurrence::defaultFeb29Type()); daySelected(dayOfMonth); // enable/disable month checkboxes as appropriate } /****************************************************************************** * Fetch which months have been checked (1 - 12). * Reply = true if February has been checked. */ QVector YearlyRule::months() const { QVector mnths; for (int i = 0; i < 12; ++i) if (mMonthBox[i]->isChecked() && mMonthBox[i]->isEnabled()) mnths.append(i + 1); return mnths; } /****************************************************************************** * Check/uncheck each month of the year according to the specified list. */ void YearlyRule::setMonths(const QList& mnths) { bool checked[12]; for (int i = 0; i < 12; ++i) checked[i] = false; for (int i = 0, end = mnths.count(); i < end; ++i) checked[mnths[i] - 1] = true; for (int i = 0; i < 12; ++i) mMonthBox[i]->setChecked(checked[i]); enableFeb29(); } /****************************************************************************** * Return the date for February 29th alarms in non-leap years. */ KARecurrence::Feb29Type YearlyRule::feb29Type() const { if (mFeb29Combo->isEnabled()) { switch (mFeb29Combo->currentIndex()) { case 1: return KARecurrence::Feb29_Mar1; case 2: return KARecurrence::Feb29_Feb28; default: break; } } return KARecurrence::Feb29_None; } /****************************************************************************** * Set the date for February 29th alarms to trigger in non-leap years. */ void YearlyRule::setFeb29Type(KARecurrence::Feb29Type type) { int index; switch (type) { default: case KARecurrence::Feb29_None: index = 0; break; case KARecurrence::Feb29_Mar1: index = 1; break; case KARecurrence::Feb29_Feb28: index = 2; break; } mFeb29Combo->setCurrentIndex(index); } /****************************************************************************** * Validate: check that at least one month is selected. */ QWidget* YearlyRule::validate(QString& errorMessage) { for (int i = 0; i < 12; ++i) if (mMonthBox[i]->isChecked() && mMonthBox[i]->isEnabled()) return nullptr; errorMessage = i18nc("@info", "No month selected"); return mMonthBox[0]; } /****************************************************************************** * Called when a yearly recurrence type radio button is clicked, * to enable/disable month checkboxes as appropriate for the date selected. */ void YearlyRule::clicked(QAbstractButton* button) { MonthYearRule::clicked(button); daySelected(buttonType(button) == DATE ? date() : 1); } /****************************************************************************** * Called when a day of the month is selected in a yearly recurrence, to * disable months for which the day is out of range. */ void YearlyRule::daySelected(int day) { mMonthBox[1]->setEnabled(day <= 29); // February const bool enable = (day != 31); mMonthBox[3]->setEnabled(enable); // April mMonthBox[5]->setEnabled(enable); // June mMonthBox[8]->setEnabled(enable); // September mMonthBox[10]->setEnabled(enable); // November enableFeb29(); } /****************************************************************************** * Enable/disable the February 29th combo box depending on whether February * 29th is selected. */ void YearlyRule::enableFeb29() { const bool enable = (type() == DATE && date() == 29 && mMonthBox[1]->isChecked() && mMonthBox[1]->isEnabled()); mFeb29Label->setEnabled(enable); mFeb29Combo->setEnabled(enable); } /****************************************************************************** * Save the state of all controls. */ void YearlyRule::saveState() { MonthYearRule::saveState(); mSavedMonths = months(); mSavedFeb29Type = feb29Type(); } /****************************************************************************** * Check whether any of the controls have changed state since initialisation. */ bool YearlyRule::stateChanged() const { return (MonthYearRule::stateChanged() || mSavedMonths != months() || mSavedFeb29Type != feb29Type()); } // vim: et sw=4: diff --git a/src/resources/resourcedatamodelbase.cpp b/src/resources/resourcedatamodelbase.cpp index 59f1df2c..e7776935 100644 --- a/src/resources/resourcedatamodelbase.cpp +++ b/src/resources/resourcedatamodelbase.cpp @@ -1,605 +1,605 @@ /* * resourcedatamodelbase.cpp - base for models containing calendars and events * Program: kalarm * Copyright © 2007-2019 David Jarvie * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "resourcedatamodelbase.h" #include "alarmtime.h" #include "mainwindow.h" #include "messagebox.h" #include "preferences.h" #include "resources/resource.h" #include "kalarm_debug.h" #include #include #include -#include +#include #include #include #include #include /*============================================================================= = Class: ResourceDataModelBase =============================================================================*/ QPixmap* ResourceDataModelBase::mTextIcon = nullptr; QPixmap* ResourceDataModelBase::mFileIcon = nullptr; QPixmap* ResourceDataModelBase::mCommandIcon = nullptr; QPixmap* ResourceDataModelBase::mEmailIcon = nullptr; QPixmap* ResourceDataModelBase::mAudioIcon = nullptr; QSize ResourceDataModelBase::mIconSize; /****************************************************************************** * Constructor. */ ResourceDataModelBase::ResourceDataModelBase() { if (!mTextIcon) { mTextIcon = new QPixmap(QIcon::fromTheme(QStringLiteral("dialog-information")).pixmap(16, 16)); mFileIcon = new QPixmap(QIcon::fromTheme(QStringLiteral("document-open")).pixmap(16, 16)); mCommandIcon = new QPixmap(QIcon::fromTheme(QStringLiteral("system-run")).pixmap(16, 16)); mEmailIcon = new QPixmap(QIcon::fromTheme(QStringLiteral("mail-unread")).pixmap(16, 16)); mAudioIcon = new QPixmap(QIcon::fromTheme(QStringLiteral("audio-x-generic")).pixmap(16, 16)); mIconSize = mTextIcon->size().expandedTo(mFileIcon->size()).expandedTo(mCommandIcon->size()).expandedTo(mEmailIcon->size()).expandedTo(mAudioIcon->size()); } } ResourceDataModelBase::~ResourceDataModelBase() { } /****************************************************************************** * Create a bulleted list of alarm types for insertion into .... */ QString ResourceDataModelBase::typeListForDisplay(CalEvent::Types alarmTypes) { QString list; if (alarmTypes & CalEvent::ACTIVE) list += QLatin1String("") + i18nc("@info", "Active Alarms") + QLatin1String(""); if (alarmTypes & CalEvent::ARCHIVED) list += QLatin1String("") + i18nc("@info", "Archived Alarms") + QLatin1String(""); if (alarmTypes & CalEvent::TEMPLATE) list += QLatin1String("") + i18nc("@info", "Alarm Templates") + QLatin1String(""); if (!list.isEmpty()) list = QLatin1String("") + list + QLatin1String(""); return list; } /****************************************************************************** * Return the read-only status tooltip for a collection, determined by the * read-write permissions and the KAlarm calendar format compatibility. * A null string is returned if the collection is read-write and compatible. */ QString ResourceDataModelBase::readOnlyTooltip(const Resource& resource) { switch (resource.compatibility()) { case KACalendar::Current: return resource.readOnly() ? i18nc("@info", "Read-only") : QString(); case KACalendar::Converted: case KACalendar::Convertible: return i18nc("@info", "Read-only (old format)"); case KACalendar::Incompatible: default: return i18nc("@info", "Read-only (other format)"); } } /****************************************************************************** * Return data for a column heading. */ QVariant ResourceDataModelBase::headerData(int section, Qt::Orientation orientation, int role, bool eventHeaders, bool& handled) { if (orientation == Qt::Horizontal) { handled = true; if (eventHeaders) { // Event column headers if (section < 0 || section >= ColumnCount) return QVariant(); if (role == Qt::DisplayRole || role == ColumnTitleRole) { switch (section) { case TimeColumn: return i18nc("@title:column", "Time"); case TimeToColumn: return i18nc("@title:column", "Time To"); case RepeatColumn: return i18nc("@title:column", "Repeat"); case ColourColumn: return (role == Qt::DisplayRole) ? QString() : i18nc("@title:column", "Color"); case TypeColumn: return (role == Qt::DisplayRole) ? QString() : i18nc("@title:column", "Type"); case TextColumn: return i18nc("@title:column", "Message, File or Command"); case TemplateNameColumn: return i18nc("@title:column Template name", "Name"); } } else if (role == Qt::WhatsThisRole) return whatsThisText(section); } else { // Calendar column headers if (section != 0) return QVariant(); if (role == Qt::DisplayRole) return i18nc("@title:column", "Calendars"); } } handled = false; return QVariant(); } bool ResourceDataModelBase::roleHandled(int role) const { switch (role) { case Qt::WhatsThisRole: case Qt::ForegroundRole: case Qt::BackgroundRole: case Qt::DisplayRole: case Qt::TextAlignmentRole: case Qt::DecorationRole: case Qt::SizeHintRole: case Qt::AccessibleTextRole: case Qt::ToolTipRole: case BaseColourRole: case TimeDisplayRole: case SortRole: case StatusRole: case ValueRole: case EventIdRole: case ParentResourceIdRole: case EnabledRole: case AlarmActionsRole: case AlarmSubActionRole: case CommandErrorRole: return true; default: return false; } } /****************************************************************************** * Return the data for a given role, for a specified resource. */ QVariant ResourceDataModelBase::resourceData(int& role, const Resource& resource, bool& handled) const { if (roleHandled(role)) // Ensure that resourceDataHandles() is coded correctly { handled = true; switch (role) { case Qt::DisplayRole: return resource.displayName(); case BaseColourRole: role = Qt::BackgroundRole; // use base model background colour break; case Qt::BackgroundRole: { const QColor colour = resource.backgroundColour(); if (colour.isValid()) return colour; break; } case Qt::ForegroundRole: return resource.foregroundColour(); case Qt::ToolTipRole: return tooltip(resource, CalEvent::ACTIVE | CalEvent::ARCHIVED | CalEvent::TEMPLATE); default: break; } } handled = false; return QVariant(); } /****************************************************************************** * Return the data for a given role, for a specified event. */ QVariant ResourceDataModelBase::eventData(int role, int column, const KAEvent& event, const Resource& resource, bool& handled) const { if (roleHandled(role)) // Ensure that eventDataHandles() is coded correctly { handled = true; bool calendarColour = false; if (role == Qt::WhatsThisRole) return whatsThisText(column); if (!event.isValid()) return QVariant(); switch (role) { case EventIdRole: return event.id(); case StatusRole: return event.category(); case AlarmActionsRole: return event.actionTypes(); case AlarmSubActionRole: return event.actionSubType(); case CommandErrorRole: return event.commandError(); default: break; } switch (column) { case TimeColumn: switch (role) { case Qt::BackgroundRole: calendarColour = true; break; case Qt::DisplayRole: if (event.expired()) return AlarmTime::alarmTimeText(event.startDateTime(), '0'); return AlarmTime::alarmTimeText(event.nextTrigger(KAEvent::DISPLAY_TRIGGER), '0'); case TimeDisplayRole: if (event.expired()) return AlarmTime::alarmTimeText(event.startDateTime(), '~'); return AlarmTime::alarmTimeText(event.nextTrigger(KAEvent::DISPLAY_TRIGGER), '~'); case Qt::TextAlignmentRole: return Qt::AlignRight; case SortRole: { DateTime due; if (event.expired()) due = event.startDateTime(); else due = event.nextTrigger(KAEvent::DISPLAY_TRIGGER); return due.isValid() ? due.effectiveKDateTime().toUtc().qDateTime() : QDateTime(QDate(9999,12,31), QTime(0,0,0)); } default: break; } break; case TimeToColumn: switch (role) { case Qt::BackgroundRole: calendarColour = true; break; case Qt::DisplayRole: if (event.expired()) return QString(); return AlarmTime::timeToAlarmText(event.nextTrigger(KAEvent::DISPLAY_TRIGGER)); case Qt::TextAlignmentRole: return Qt::AlignRight; case SortRole: { if (event.expired()) return -1; const DateTime due = event.nextTrigger(KAEvent::DISPLAY_TRIGGER); const KADateTime now = KADateTime::currentUtcDateTime(); if (due.isDateOnly()) return now.date().daysTo(due.date()) * 1440; return (now.secsTo(due.effectiveKDateTime()) + 59) / 60; } } break; case RepeatColumn: switch (role) { case Qt::BackgroundRole: calendarColour = true; break; case Qt::DisplayRole: return repeatText(event); case Qt::TextAlignmentRole: return Qt::AlignHCenter; case SortRole: return repeatOrder(event); } break; case ColourColumn: switch (role) { case Qt::BackgroundRole: { const KAEvent::Actions type = event.actionTypes(); if (type & KAEvent::ACT_DISPLAY) return event.bgColour(); if (type == KAEvent::ACT_COMMAND) { if (event.commandError() != KAEvent::CMD_NO_ERROR) return QColor(Qt::red); } break; } case Qt::ForegroundRole: if (event.commandError() != KAEvent::CMD_NO_ERROR) { if (event.actionTypes() == KAEvent::ACT_COMMAND) return QColor(Qt::white); QColor colour = Qt::red; int r, g, b; event.bgColour().getRgb(&r, &g, &b); if (r > 128 && g <= 128 && b <= 128) colour = QColor(Qt::white); return colour; } break; case Qt::DisplayRole: if (event.commandError() != KAEvent::CMD_NO_ERROR) return QLatin1String("!"); break; case SortRole: { const unsigned i = (event.actionTypes() == KAEvent::ACT_DISPLAY) ? event.bgColour().rgb() : 0; return QStringLiteral("%1").arg(i, 6, 10, QLatin1Char('0')); } default: break; } break; case TypeColumn: switch (role) { case Qt::BackgroundRole: calendarColour = true; break; case Qt::DecorationRole: { QVariant v; v.setValue(*eventIcon(event)); return v; } case Qt::TextAlignmentRole: return Qt::AlignHCenter; case Qt::SizeHintRole: return mIconSize; case Qt::AccessibleTextRole: //TODO: Implement accessibility return QString(); case ValueRole: return static_cast(event.actionSubType()); case SortRole: return QStringLiteral("%1").arg(event.actionSubType(), 2, 10, QLatin1Char('0')); } break; case TextColumn: switch (role) { case Qt::BackgroundRole: calendarColour = true; break; case Qt::DisplayRole: case SortRole: return AlarmText::summary(event, 1); case Qt::ToolTipRole: return AlarmText::summary(event, 10); default: break; } break; case TemplateNameColumn: switch (role) { case Qt::BackgroundRole: calendarColour = true; break; case Qt::DisplayRole: return event.templateName(); case SortRole: return event.templateName().toUpper(); } break; default: break; } if (calendarColour) { const QColor colour = resource.backgroundColour(); if (colour.isValid()) return colour; } switch (role) { case Qt::ForegroundRole: if (!event.enabled()) return Preferences::disabledColour(); if (event.expired()) return Preferences::archivedColour(); break; // use the default for normal active alarms case Qt::ToolTipRole: // Show the last command execution error message switch (event.commandError()) { case KAEvent::CMD_ERROR: return i18nc("@info:tooltip", "Command execution failed"); case KAEvent::CMD_ERROR_PRE: return i18nc("@info:tooltip", "Pre-alarm action execution failed"); case KAEvent::CMD_ERROR_POST: return i18nc("@info:tooltip", "Post-alarm action execution failed"); case KAEvent::CMD_ERROR_PRE_POST: return i18nc("@info:tooltip", "Pre- and post-alarm action execution failed"); default: case KAEvent::CMD_NO_ERROR: break; } break; case EnabledRole: return event.enabled(); default: break; } } handled = false; return QVariant(); } /****************************************************************************** * Return a resource's tooltip text. The resource's enabled status is * evaluated for specified alarm types. */ QString ResourceDataModelBase::tooltip(const Resource& resource, CalEvent::Types types) const { const QString name = QLatin1Char('@') + resource.displayName(); // insert markers for stripping out name const QString type = QLatin1Char('@') + resource.storageTypeString(false); // file/directory/URL etc. const QString locn = resource.displayLocation(); const bool inactive = !(resource.enabledTypes() & types); const QString readonly = readOnlyTooltip(resource); const bool writable = readonly.isEmpty(); //TODO: should the above line be = resource.isWritable() ? const QString disabled = i18nc("@info", "Disabled"); if (inactive && !writable) return xi18nc("@info:tooltip", "%1" "%2: %3" "%4, %5", name, type, locn, disabled, readonly); if (inactive || !writable) return xi18nc("@info:tooltip", "%1" "%2: %3" "%4", name, type, locn, (inactive ? disabled : readonly)); return xi18nc("@info:tooltip", "%1" "%2: %3", name, type, locn); } /****************************************************************************** * Return the repetition text. */ QString ResourceDataModelBase::repeatText(const KAEvent& event) { const QString repText = event.recurrenceText(true); return repText.isEmpty() ? event.repetitionText(true) : repText; } /****************************************************************************** * Return a string for sorting the repetition column. */ QString ResourceDataModelBase::repeatOrder(const KAEvent& event) { int repOrder = 0; int repInterval = 0; if (event.repeatAtLogin()) repOrder = 1; else { repInterval = event.recurInterval(); switch (event.recurType()) { case KARecurrence::MINUTELY: repOrder = 2; break; case KARecurrence::DAILY: repOrder = 3; break; case KARecurrence::WEEKLY: repOrder = 4; break; case KARecurrence::MONTHLY_DAY: case KARecurrence::MONTHLY_POS: repOrder = 5; break; case KARecurrence::ANNUAL_DATE: case KARecurrence::ANNUAL_POS: repOrder = 6; break; case KARecurrence::NO_RECUR: default: break; } } return QStringLiteral("%1%2").arg(static_cast('0' + repOrder)).arg(repInterval, 8, 10, QLatin1Char('0')); } /****************************************************************************** * Returns the QWhatsThis text for a specified column. */ QString ResourceDataModelBase::whatsThisText(int column) { switch (column) { case TimeColumn: return i18nc("@info:whatsthis", "Next scheduled date and time of the alarm"); case TimeToColumn: return i18nc("@info:whatsthis", "How long until the next scheduled trigger of the alarm"); case RepeatColumn: return i18nc("@info:whatsthis", "How often the alarm recurs"); case ColourColumn: return i18nc("@info:whatsthis", "Background color of alarm message"); case TypeColumn: return i18nc("@info:whatsthis", "Alarm type (message, file, command or email)"); case TextColumn: return i18nc("@info:whatsthis", "Alarm message text, URL of text file to display, command to execute, or email subject line"); case TemplateNameColumn: return i18nc("@info:whatsthis", "Name of the alarm template"); default: return QString(); } } /****************************************************************************** * Return the icon associated with an event's action. */ QPixmap* ResourceDataModelBase::eventIcon(const KAEvent& event) { switch (event.actionTypes()) { case KAEvent::ACT_EMAIL: return mEmailIcon; case KAEvent::ACT_AUDIO: return mAudioIcon; case KAEvent::ACT_COMMAND: return mCommandIcon; case KAEvent::ACT_DISPLAY: if (event.actionSubType() == KAEvent::FILE) return mFileIcon; Q_FALLTHROUGH(); // fall through to ACT_DISPLAY_COMMAND case KAEvent::ACT_DISPLAY_COMMAND: default: return mTextIcon; } } /****************************************************************************** * Display a message to the user. */ void ResourceDataModelBase::handleResourceMessage(ResourceType::MessageType type, const QString& message, const QString& details) { if (type == ResourceType::MessageType::Error) KAMessageBox::detailedError(MainWindow::mainMainWindow(), message, details); else if (type == ResourceType::MessageType::Info) KAMessageBox::informationList(MainWindow::mainMainWindow(), message, {details}); } // vim: et sw=4: diff --git a/src/resourceselector.cpp b/src/resourceselector.cpp index d1790eda..ac0a63f6 100644 --- a/src/resourceselector.cpp +++ b/src/resourceselector.cpp @@ -1,574 +1,574 @@ /* * resourceselector.cpp - calendar resource selection widget * Program: kalarm * Copyright © 2006-2019 David Jarvie * Based on KOrganizer's ResourceView class and KAddressBook's ResourceSelection class, * Copyright (C) 2003,2004 Cornelius Schumacher * Copyright (C) 2003-2004 Reinhold Kainhofer * Copyright (c) 2004 Tobias Koenig * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "resourceselector.h" #include "alarmcalendar.h" #include "autoqpointer.h" #include "akonadiresourcecreator.h" #include "calendarmigrator.h" #include "kalarmapp.h" #include "messagebox.h" #include "packedlayout.h" #include "preferences.h" #include "resources/resources.h" #include "resources/resourcemodel.h" #include "kalarm_debug.h" #include -#include -#include +#include +#include #include #include #include #include #include #include #include #include #include using namespace KCalendarCore; ResourceSelector::ResourceSelector(QWidget* parent) : QFrame(parent) { QBoxLayout* topLayout = new QVBoxLayout(this); QLabel* label = new QLabel(i18nc("@title:group", "Calendars"), this); topLayout->addWidget(label, 0, Qt::AlignHCenter); mAlarmType = new QComboBox(this); mAlarmType->addItem(i18nc("@item:inlistbox", "Active Alarms")); mAlarmType->addItem(i18nc("@item:inlistbox", "Archived Alarms")); mAlarmType->addItem(i18nc("@item:inlistbox", "Alarm Templates")); mAlarmType->setFixedHeight(mAlarmType->sizeHint().height()); mAlarmType->setWhatsThis(i18nc("@info:whatsthis", "Choose which type of data to show alarm calendars for")); topLayout->addWidget(mAlarmType); // No spacing between combo box and listview. ResourceFilterCheckListModel* model = ResourceFilterCheckListModel::create(this); mListView = new ResourceView(model, this); connect(mListView->selectionModel(), &QItemSelectionModel::selectionChanged, this, &ResourceSelector::selectionChanged); mListView->setContextMenuPolicy(Qt::CustomContextMenu); connect(mListView, &ResourceView::customContextMenuRequested, this, &ResourceSelector::contextMenuRequested); mListView->setWhatsThis(i18nc("@info:whatsthis", "List of available calendars of the selected type. The checked state shows whether a calendar " "is enabled (checked) or disabled (unchecked). The default calendar is shown in bold.")); topLayout->addWidget(mListView, 1); PackedLayout* blayout = new PackedLayout(Qt::AlignHCenter); blayout->setContentsMargins(0, 0, 0, 0); topLayout->addLayout(blayout); mAddButton = new QPushButton(i18nc("@action:button", "Add..."), this); mEditButton = new QPushButton(i18nc("@action:button", "Edit..."), this); mDeleteButton = new QPushButton(i18nc("@action:button", "Remove"), this); blayout->addWidget(mAddButton); blayout->addWidget(mEditButton); blayout->addWidget(mDeleteButton); mEditButton->setWhatsThis(i18nc("@info:whatsthis", "Edit the highlighted calendar")); mDeleteButton->setWhatsThis(xi18nc("@info:whatsthis", "Remove the highlighted calendar from the list." "The calendar itself is left intact, and may subsequently be reinstated in the list if desired.")); mEditButton->setDisabled(true); mDeleteButton->setDisabled(true); connect(mAddButton, &QPushButton::clicked, this, &ResourceSelector::addResource); connect(mEditButton, &QPushButton::clicked, this, &ResourceSelector::editResource); connect(mDeleteButton, &QPushButton::clicked, this, &ResourceSelector::removeResource); connect(Resources::instance(), &Resources::resourceRemoved, this, &ResourceSelector::selectionChanged); connect(mAlarmType, static_cast(&QComboBox::activated), this, &ResourceSelector::alarmTypeSelected); QTimer::singleShot(0, this, SLOT(alarmTypeSelected())); Preferences::connect(SIGNAL(archivedKeepDaysChanged(int)), this, SLOT(archiveDaysChanged(int))); } /****************************************************************************** * Called when an alarm type has been selected. * Filter the resource list to show resources of the selected alarm type, and * add appropriate whatsThis texts to the list and to the Add button. */ void ResourceSelector::alarmTypeSelected() { QString addTip; switch (mAlarmType->currentIndex()) { case 0: mCurrentAlarmType = CalEvent::ACTIVE; addTip = i18nc("@info:tooltip", "Add a new active alarm calendar"); break; case 1: mCurrentAlarmType = CalEvent::ARCHIVED; addTip = i18nc("@info:tooltip", "Add a new archived alarm calendar"); break; case 2: mCurrentAlarmType = CalEvent::TEMPLATE; addTip = i18nc("@info:tooltip", "Add a new alarm template calendar"); break; } // WORKAROUND: Switch scroll bars off to avoid crash (see explanation // in reinstateAlarmTypeScrollBars() description). mListView->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); mListView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); mListView->resourceModel()->setEventTypeFilter(mCurrentAlarmType); mAddButton->setWhatsThis(addTip); mAddButton->setToolTip(addTip); // WORKAROUND: Switch scroll bars back on after allowing geometry to update ... QTimer::singleShot(0, this, &ResourceSelector::reinstateAlarmTypeScrollBars); selectionChanged(); // enable/disable buttons } /****************************************************************************** * WORKAROUND for crash due to presumed Qt bug. * Switch scroll bars off. This is to avoid a crash which can very occasionally * happen when changing from a list of calendars which requires vertical scroll * bars, to a list whose text is very slightly wider but which doesn't require * scroll bars at all. (The suspicion is that the width is such that it would * require horizontal scroll bars if the vertical scroll bars were still * present.) Presumably due to a Qt bug, this can result in a recursive call to * ResourceView::viewportEvent() with a Resize event. * * The crash only occurs if the ResourceSelector happens to have exactly (within * one pixel) the "right" width to create the crash. */ void ResourceSelector::reinstateAlarmTypeScrollBars() { mListView->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); mListView->setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded); } /****************************************************************************** * Prompt the user for a new resource to add to the list. */ void ResourceSelector::addResource() { AkonadiResourceCreator* creator = new AkonadiResourceCreator(mCurrentAlarmType, this); connect(creator, &AkonadiResourceCreator::resourceAdded, this, &ResourceSelector::slotResourceAdded); creator->createResource(); } /****************************************************************************** * Called when a collection is added to the AkonadiModel, after being created * by addResource(). */ void ResourceSelector::slotResourceAdded(Resource& resource, CalEvent::Type alarmType) { const CalEvent::Types types = resource.alarmTypes(); resource.setEnabled(types); if (!(types & alarmType)) { // The user has selected alarm types for the resource // which don't include the currently displayed type. // Show a collection list which includes a selected type. int index = -1; if (types & CalEvent::ACTIVE) index = 0; else if (types & CalEvent::ARCHIVED) index = 1; else if (types & CalEvent::TEMPLATE) index = 2; if (index >= 0) { mAlarmType->setCurrentIndex(index); alarmTypeSelected(); } } } /****************************************************************************** * Edit the currently selected resource. */ void ResourceSelector::editResource() { currentResource().editResource(this); } /****************************************************************************** * Update the backend storage format for the currently selected resource in the * displayed list. */ void ResourceSelector::updateResource() { const Resource resource = currentResource(); if (!resource.isValid()) return; CalendarMigrator::updateToCurrentFormat(resource, true, this); } /****************************************************************************** * Remove the currently selected resource from the displayed list. */ void ResourceSelector::removeResource() { Resource resource = currentResource(); if (!resource.isValid()) return; const QString name = resource.configName(); // Check if it's the standard or only resource for at least one type. const CalEvent::Types allTypes = resource.alarmTypes(); const CalEvent::Types standardTypes = Resources::standardTypes(resource, true); const CalEvent::Type currentType = currentResourceType(); const CalEvent::Type stdType = (standardTypes & CalEvent::ACTIVE) ? CalEvent::ACTIVE : (standardTypes & CalEvent::ARCHIVED) ? CalEvent::ARCHIVED : CalEvent::EMPTY; if (stdType == CalEvent::ACTIVE) { KAMessageBox::sorry(this, i18nc("@info", "You cannot remove your default active alarm calendar.")); return; } if (stdType == CalEvent::ARCHIVED && Preferences::archivedKeepDays()) { // Only allow the archived alarms standard resource to be removed if // we're not saving archived alarms. KAMessageBox::sorry(this, i18nc("@info", "You cannot remove your default archived alarm calendar " "while expired alarms are configured to be kept.")); return; } QString text; if (standardTypes) { // It's a standard resource for at least one alarm type if (allTypes != currentType) { // It also contains alarm types other than the currently displayed type const QString stdTypes = ResourceDataModelBase::typeListForDisplay(standardTypes); QString otherTypes; const CalEvent::Types nonStandardTypes(allTypes & ~standardTypes); if (nonStandardTypes != currentType) otherTypes = xi18nc("@info", "It also contains:%1", ResourceDataModelBase::typeListForDisplay(nonStandardTypes)); text = xi18nc("@info", "%1 is the default calendar for:%2%3" "Do you really want to remove it from all calendar lists?", name, stdTypes, otherTypes); } else text = xi18nc("@info", "Do you really want to remove your default calendar (%1) from the list?", name); } else if (allTypes != currentType) text = xi18nc("@info", "%1 contains:%2Do you really want to remove it from all calendar lists?", name, ResourceDataModelBase::typeListForDisplay(allTypes)); else text = xi18nc("@info", "Do you really want to remove the calendar %1 from the list?", name); if (KAMessageBox::warningContinueCancel(this, text, QString(), KStandardGuiItem::remove()) == KMessageBox::Cancel) return; resource.removeResource(); } /****************************************************************************** * Called when the current selection changes, to enable/disable the * Delete and Edit buttons accordingly. */ void ResourceSelector::selectionChanged() { bool state = mListView->selectionModel()->selectedRows().count(); mDeleteButton->setEnabled(state); mEditButton->setEnabled(state); } /****************************************************************************** * Initialise the button and context menu actions. */ void ResourceSelector::initActions(KActionCollection* actions) { mActionReload = new QAction(QIcon::fromTheme(QStringLiteral("view-refresh")), i18nc("@action Reload calendar", "Re&load"), this); actions->addAction(QStringLiteral("resReload"), mActionReload); connect(mActionReload, &QAction::triggered, this, &ResourceSelector::reloadResource); mActionShowDetails = new QAction(QIcon::fromTheme(QStringLiteral("help-about")), i18nc("@action", "Show &Details"), this); actions->addAction(QStringLiteral("resDetails"), mActionShowDetails); connect(mActionShowDetails, &QAction::triggered, this, &ResourceSelector::showInfo); mActionSetColour = new QAction(QIcon::fromTheme(QStringLiteral("color-picker")), i18nc("@action", "Set &Color..."), this); actions->addAction(QStringLiteral("resSetColour"), mActionSetColour); connect(mActionSetColour, &QAction::triggered, this, &ResourceSelector::setColour); mActionClearColour = new QAction(i18nc("@action", "Clear C&olor"), this); actions->addAction(QStringLiteral("resClearColour"), mActionClearColour); connect(mActionClearColour, &QAction::triggered, this, &ResourceSelector::clearColour); mActionEdit = new QAction(QIcon::fromTheme(QStringLiteral("document-properties")), i18nc("@action", "&Edit..."), this); actions->addAction(QStringLiteral("resEdit"), mActionEdit); connect(mActionEdit, &QAction::triggered, this, &ResourceSelector::editResource); mActionUpdate = new QAction(i18nc("@action", "&Update Calendar Format"), this); actions->addAction(QStringLiteral("resUpdate"), mActionUpdate); connect(mActionUpdate, &QAction::triggered, this, &ResourceSelector::updateResource); mActionRemove = new QAction(QIcon::fromTheme(QStringLiteral("edit-delete")), i18nc("@action", "&Remove"), this); actions->addAction(QStringLiteral("resRemove"), mActionRemove); connect(mActionRemove, &QAction::triggered, this, &ResourceSelector::removeResource); mActionSetDefault = new KToggleAction(this); actions->addAction(QStringLiteral("resDefault"), mActionSetDefault); connect(mActionSetDefault, &KToggleAction::triggered, this, &ResourceSelector::setStandard); QAction* action = new QAction(QIcon::fromTheme(QStringLiteral("document-new")), i18nc("@action", "&Add..."), this); actions->addAction(QStringLiteral("resAdd"), action); connect(action, &QAction::triggered, this, &ResourceSelector::addResource); mActionImport = new QAction(i18nc("@action", "Im&port..."), this); actions->addAction(QStringLiteral("resImport"), mActionImport); connect(mActionImport, &QAction::triggered, this, &ResourceSelector::importCalendar); mActionExport = new QAction(i18nc("@action", "E&xport..."), this); actions->addAction(QStringLiteral("resExport"), mActionExport); connect(mActionExport, &QAction::triggered, this, &ResourceSelector::exportCalendar); } void ResourceSelector::setContextMenu(QMenu* menu) { mContextMenu = menu; } /****************************************************************************** * Display the context menu for the selected calendar. */ void ResourceSelector::contextMenuRequested(const QPoint& viewportPos) { if (!mContextMenu) return; bool active = false; bool writable = false; bool updatable = false; Resource resource; if (mListView->selectionModel()->hasSelection()) { const QModelIndex index = mListView->indexAt(viewportPos); if (index.isValid()) resource = mListView->resourceModel()->resource(index); else mListView->clearSelection(); } CalEvent::Type type = currentResourceType(); bool haveCalendar = resource.isValid(); if (haveCalendar) { active = resource.isEnabled(type); const int rw = resource.writableStatus(type); writable = (rw > 0); const KACalendar::Compat compatibility = resource.compatibility(); if (!rw && (compatibility & ~KACalendar::Converted) && !(compatibility & ~(KACalendar::Convertible | KACalendar::Converted))) updatable = true; // the calendar format is convertible to the current KAlarm format if (!(resource.alarmTypes() & type)) type = CalEvent::EMPTY; } mActionReload->setEnabled(active); mActionShowDetails->setEnabled(haveCalendar); mActionSetColour->setEnabled(haveCalendar); mActionClearColour->setEnabled(haveCalendar); mActionClearColour->setVisible(resource.backgroundColour().isValid()); mActionEdit->setEnabled(haveCalendar); mActionUpdate->setEnabled(updatable); mActionRemove->setEnabled(haveCalendar); mActionImport->setEnabled(active && writable); mActionExport->setEnabled(active); QString text; switch (type) { case CalEvent::ACTIVE: text = i18nc("@action", "Use as &Default for Active Alarms"); break; case CalEvent::ARCHIVED: text = i18nc("@action", "Use as &Default for Archived Alarms"); break; case CalEvent::TEMPLATE: text = i18nc("@action", "Use as &Default for Alarm Templates"); break; default: break; } mActionSetDefault->setText(text); bool standard = Resources::isStandard(resource, type); mActionSetDefault->setChecked(active && writable && standard); mActionSetDefault->setEnabled(active && writable); mContextMenu->popup(mListView->viewport()->mapToGlobal(viewportPos)); } /****************************************************************************** * Called from the context menu to reload the selected resource. */ void ResourceSelector::reloadResource() { const Resource resource = currentResource(); if (resource.isValid()) AkonadiModel::instance()->reloadResource(resource); } /****************************************************************************** * Called from the context menu to save the selected resource. */ void ResourceSelector::saveResource() { // Save resource is not applicable to Akonadi } /****************************************************************************** * Called when the length of time archived alarms are to be stored changes. * If expired alarms are now to be stored, this also sets any single archived * alarm resource to be the default. */ void ResourceSelector::archiveDaysChanged(int days) { if (days) { const Resource resource = Resources::getStandard(CalEvent::ARCHIVED); if (resource.isValid()) theApp()->purgeNewArchivedDefault(resource); } } /****************************************************************************** * Called from the context menu to set the selected resource as the default * for its alarm type. The resource is automatically made active. */ void ResourceSelector::setStandard() { Resource resource = currentResource(); if (resource.isValid()) { CalEvent::Type alarmType = currentResourceType(); bool standard = mActionSetDefault->isChecked(); if (standard) resource.setEnabled(alarmType, true); Resources::setStandard(resource, alarmType, standard); if (alarmType == CalEvent::ARCHIVED) theApp()->purgeNewArchivedDefault(resource); } } /****************************************************************************** * Called from the context menu to merge alarms from an external calendar into * the selected resource (if any). */ void ResourceSelector::importCalendar() { Resource resource = currentResource(); AlarmCalendar::resources()->importAlarms(this, &resource); } /****************************************************************************** * Called from the context menu to copy the selected resource's alarms to an * external calendar. */ void ResourceSelector::exportCalendar() { const Resource resource = currentResource(); if (resource.isValid()) AlarmCalendar::exportAlarms(AlarmCalendar::resources()->events(resource), this); } /****************************************************************************** * Called from the context menu to set a colour for the selected resource. */ void ResourceSelector::setColour() { Resource resource = currentResource(); if (resource.isValid()) { QColor colour = resource.backgroundColour(); if (!colour.isValid()) colour = QApplication::palette().color(QPalette::Base); colour = QColorDialog::getColor(colour, this); if (colour.isValid()) resource.setBackgroundColour(colour); } } /****************************************************************************** * Called from the context menu to clear the display colour for the selected * resource. */ void ResourceSelector::clearColour() { Resource resource = currentResource(); if (resource.isValid()) resource.setBackgroundColour(QColor()); } /****************************************************************************** * Called from the context menu to display information for the selected resource. */ void ResourceSelector::showInfo() { const Resource resource = currentResource(); if (resource.isValid()) { const QString name = resource.displayName(); const QString id = resource.configName(); // resource name const QString calType = resource.storageTypeString(true); const CalEvent::Type alarmType = currentResourceType(); const QString storage = resource.storageTypeString(false); const QString location = resource.displayLocation(); const CalEvent::Types altypes = resource.alarmTypes(); QStringList alarmTypes; if (altypes & CalEvent::ACTIVE) alarmTypes << i18nc("@info", "Active alarms"); if (altypes & CalEvent::ARCHIVED) alarmTypes << i18nc("@info", "Archived alarms"); if (altypes & CalEvent::TEMPLATE) alarmTypes << i18nc("@info", "Alarm templates"); const QString alarmTypeString = alarmTypes.join(i18nc("@info List separator", ", ")); QString perms = ResourceDataModelBase::readOnlyTooltip(resource); if (perms.isEmpty()) perms = i18nc("@info", "Read-write"); const QString enabled = resource.isEnabled(alarmType) ? i18nc("@info", "Enabled") : i18nc("@info", "Disabled"); const QString std = Resources::isStandard(resource, alarmType) ? i18nc("@info Parameter in 'Default calendar: Yes/No'", "Yes") : i18nc("@info Parameter in 'Default calendar: Yes/No'", "No"); const QString text = xi18nc("@info", "%1" "ID: %2" "Calendar type: %3" "Contents: %4" "%5: %6" "Permissions: %7" "Status: %8" "Default calendar: %9", name, id, calType, alarmTypeString, storage, location, perms, enabled, std); // Display the resource information. Because the user requested // the information, don't raise a KNotify event. KAMessageBox::information(this, text, QString(), QString(), KMessageBox::Options()); } } /****************************************************************************** * Return the currently selected resource in the list. */ Resource ResourceSelector::currentResource() const { return mListView->resource(mListView->selectionModel()->currentIndex()); } /****************************************************************************** * Return the currently selected resource type. */ CalEvent::Type ResourceSelector::currentResourceType() const { switch (mAlarmType->currentIndex()) { case 0: return CalEvent::ACTIVE; case 1: return CalEvent::ARCHIVED; case 2: return CalEvent::TEMPLATE; default: return CalEvent::EMPTY; } } void ResourceSelector::resizeEvent(QResizeEvent* re) { Q_EMIT resized(re->oldSize(), re->size()); } // vim: et sw=4: diff --git a/src/templatedlg.cpp b/src/templatedlg.cpp index 67a6a895..69c7a694 100644 --- a/src/templatedlg.cpp +++ b/src/templatedlg.cpp @@ -1,241 +1,241 @@ /* * templatedlg.cpp - dialog to create, edit and delete alarm templates * Program: kalarm * Copyright © 2004-2019 David Jarvie * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "templatedlg.h" #include "editdlg.h" #include "alarmcalendar.h" #include "functions.h" #include "messagebox.h" #include "newalarmaction.h" #include "shellprocess.h" #include "templatelistview.h" #include "undo.h" #include "resources/resources.h" #include "resources/eventmodel.h" #include "kalarm_debug.h" #include -#include -#include -#include +#include +#include +#include #include #include #include #include #include #include #include using namespace KCal; static const char TMPL_DIALOG_NAME[] = "TemplateDialog"; TemplateDlg* TemplateDlg::mInstance = nullptr; TemplateDlg::TemplateDlg(QWidget* parent) : QDialog(parent) { setModal(false); setWindowTitle(i18nc("@title:window", "Alarm Templates")); QBoxLayout* topLayout = new QVBoxLayout(this); QBoxLayout* hlayout = new QHBoxLayout(this); topLayout->addLayout(hlayout); QBoxLayout* layout = new QVBoxLayout(); layout->setContentsMargins(0, 0, 0, 0); hlayout->addLayout(layout); mListFilterModel = TemplateListModel::create(this); if (!ShellProcess::authorised()) mListFilterModel->setAlarmActionFilter(static_cast(KAEvent::ACT_ALL & ~KAEvent::ACT_COMMAND)); mListView = new TemplateListView(this); mListView->setModel(mListFilterModel); mListView->sortByColumn(TemplateListModel::TemplateNameColumn, Qt::AscendingOrder); mListView->setSizePolicy(QSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding)); mListView->setWhatsThis(i18nc("@info:whatsthis", "The list of alarm templates")); mListView->setItemDelegate(new TemplateListDelegate(mListView)); connect(mListView->selectionModel(), &QItemSelectionModel::selectionChanged, this, &TemplateDlg::slotSelectionChanged); layout->addWidget(mListView); layout = new QVBoxLayout(); layout->setContentsMargins(0, 0, 0, 0); hlayout->addLayout(layout); QPushButton* button = new QPushButton(i18nc("@action:button", "New")); mNewAction = new NewAlarmAction(true, i18nc("@action", "New"), this); button->setMenu(mNewAction->menu()); connect(mNewAction, &NewAlarmAction::selected, this, &TemplateDlg::slotNew); button->setWhatsThis(i18nc("@info:whatsthis", "Create a new alarm template")); layout->addWidget(button); mEditButton = new QPushButton(i18nc("@action:button", "Edit...")); connect(mEditButton, &QPushButton::clicked, this, &TemplateDlg::slotEdit); mEditButton->setWhatsThis(i18nc("@info:whatsthis", "Edit the currently highlighted alarm template")); layout->addWidget(mEditButton); mCopyButton = new QPushButton(i18nc("@action:button", "Copy")); connect(mCopyButton, &QPushButton::clicked, this, &TemplateDlg::slotCopy); mCopyButton->setWhatsThis(i18nc("@info:whatsthis", "Create a new alarm template based on a copy of the currently highlighted template")); layout->addWidget(mCopyButton); mDeleteButton = new QPushButton(i18nc("@action:button", "Delete")); connect(mDeleteButton, &QPushButton::clicked, this, &TemplateDlg::slotDelete); mDeleteButton->setWhatsThis(i18nc("@info:whatsthis", "Delete the currently highlighted alarm template")); layout->addWidget(mDeleteButton); layout->addStretch(); topLayout->addWidget(new KSeparator(Qt::Horizontal, this)); QDialogButtonBox* buttonBox = new QDialogButtonBox(this); buttonBox->addButton(QDialogButtonBox::Close); connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::close); topLayout->addWidget(buttonBox); KActionCollection* actions = new KActionCollection(this); QAction* act = KStandardAction::selectAll(mListView, SLOT(selectAll()), actions); topLevelWidget()->addAction(act); act = KStandardAction::deselect(mListView, SLOT(clearSelection()), actions); topLevelWidget()->addAction(act); slotSelectionChanged(); // enable/disable buttons as appropriate QSize s; if (KAlarm::readConfigWindowSize(TMPL_DIALOG_NAME, s)) resize(s); } /****************************************************************************** * Destructor. */ TemplateDlg::~TemplateDlg() { mInstance = nullptr; } /****************************************************************************** * Create an instance, if none already exists. */ TemplateDlg* TemplateDlg::create(QWidget* parent) { if (mInstance) return nullptr; mInstance = new TemplateDlg(parent); return mInstance; } /****************************************************************************** * Called when the New Template button is clicked to create a new template. */ void TemplateDlg::slotNew(EditAlarmDlg::Type type) { KAlarm::editNewTemplate(type, mListView); } /****************************************************************************** * Called when the Copy button is clicked to edit a copy of an existing alarm, * to add to the list. */ void TemplateDlg::slotCopy() { KAEvent event = mListView->selectedEvent(); if (event.isValid()) KAlarm::editNewTemplate(&event, mListView); } /****************************************************************************** * Called when the Modify button is clicked to edit the currently highlighted * alarm in the list. */ void TemplateDlg::slotEdit() { KAEvent event = mListView->selectedEvent(); if (event.isValid()) KAlarm::editTemplate(&event, mListView); } /****************************************************************************** * Called when the Delete button is clicked to delete the currently highlighted * alarms in the list. */ void TemplateDlg::slotDelete() { QVector events = mListView->selectedEvents(); int n = events.count(); if (KAMessageBox::warningContinueCancel(this, i18ncp("@info", "Do you really want to delete the selected alarm template?", "Do you really want to delete the %1 selected alarm templates?", n), i18ncp("@title:window", "Delete Alarm Template", "Delete Alarm Templates", n), KGuiItem(i18nc("@action:button", "&Delete"), QStringLiteral("edit-delete"))) != KMessageBox::Continue) return; KAEvent::List delEvents; Undo::EventList undos; for (int i = 0; i < n; ++i) { KAEvent* event = &events[i]; delEvents.append(event); undos.append(*event, Resources::resourceForEvent(event->id())); } KAlarm::deleteTemplates(delEvents, this); Undo::saveDeletes(undos); } /****************************************************************************** * Called when the group of items selected changes. * Enable/disable the buttons depending on whether/how many templates are * currently highlighted. */ void TemplateDlg::slotSelectionChanged() { AlarmCalendar* resources = AlarmCalendar::resources(); QVector events = mListView->selectedEvents(); int count = events.count(); bool readOnly = false; for (int i = 0; i < count; ++i) { const KAEvent* event = &events[i]; if (resources->eventReadOnly(event->id())) { readOnly = true; break; } } mEditButton->setEnabled(count == 1); mCopyButton->setEnabled(count == 1); mDeleteButton->setEnabled(count && !readOnly); } /****************************************************************************** * Called when the dialog's size has changed. * Records the new size in the config file. */ void TemplateDlg::resizeEvent(QResizeEvent* re) { if (isVisible()) KAlarm::writeConfigWindowSize(TMPL_DIALOG_NAME, re->size()); QDialog::resizeEvent(re); } // vim: et sw=4: diff --git a/src/templatemenuaction.cpp b/src/templatemenuaction.cpp index 1908319b..fb194514 100644 --- a/src/templatemenuaction.cpp +++ b/src/templatemenuaction.cpp @@ -1,85 +1,85 @@ /* * templatemenuaction.cpp - menu action to select a template * Program: kalarm * Copyright © 2005,2006,2008,2011 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 "templatemenuaction.h" #include "alarmcalendar.h" #include "functions.h" #include "kalarm_debug.h" #include -#include +#include #include TemplateMenuAction::TemplateMenuAction(const QIcon& icon, const QString& label, QObject* parent) : KActionMenu(icon, label, parent) { setDelayed(false); connect(menu(), &QMenu::aboutToShow, this, &TemplateMenuAction::slotInitMenu); connect(menu(), &QMenu::triggered, this, &TemplateMenuAction::slotSelected); } /****************************************************************************** * Called when the New From Template action is clicked. * Creates a popup menu listing all alarm templates, in sorted name order. */ void TemplateMenuAction::slotInitMenu() { QMenu* m = menu(); m->clear(); mOriginalTexts.clear(); // Compile a sorted list of template names int i, end; QStringList sorted; KAEvent::List templates = KAlarm::templateList(); for (i = 0, end = templates.count(); i < end; ++i) { QString name = templates[i]->templateName(); int j = 0; for (int jend = sorted.count(); j < jend && QString::localeAwareCompare(name, sorted[j]) > 0; ++j) ; sorted.insert(j, name); } for (i = 0, end = sorted.count(); i < end; ++i) { QAction* act = m->addAction(sorted[i]); mOriginalTexts[act] = sorted[i]; // keep original text, since action text has shortcuts added } } /****************************************************************************** * Called when a template is selected from the New From Template popup menu. * Executes a New Alarm dialog, preset from the selected template. */ void TemplateMenuAction::slotSelected(QAction* action) { QMap::ConstIterator it = mOriginalTexts.constFind(action); if (it == mOriginalTexts.constEnd() || it.value().isEmpty()) return; KAEvent* templ = AlarmCalendar::resources()->templateEvent(it.value()); Q_EMIT selected(templ); } // vim: et sw=4: diff --git a/src/templatemenuaction.h b/src/templatemenuaction.h index 627fc83d..782d0148 100644 --- a/src/templatemenuaction.h +++ b/src/templatemenuaction.h @@ -1,49 +1,49 @@ /* * templatemenuaction.h - menu action to select a template * Program: kalarm * Copyright © 2005,2006,2008,2011 David Jarvie * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef TEMPLATEMENUACTION_H #define TEMPLATEMENUACTION_H -#include +#include #include #include class QAction; namespace KAlarmCal { class KAEvent; } class TemplateMenuAction : public KActionMenu { Q_OBJECT public: TemplateMenuAction(const QIcon& icon, const QString& label, QObject* parent); virtual ~TemplateMenuAction() {} Q_SIGNALS: void selected(const KAlarmCal::KAEvent*); private Q_SLOTS: void slotInitMenu(); void slotSelected(QAction*); private: QMap mOriginalTexts; // menu item texts without added ampersands }; #endif // TEMPLATEMENUACTION_H // vim: et sw=4: diff --git a/src/traywindow.cpp b/src/traywindow.cpp index c78322f4..8fc25fd0 100644 --- a/src/traywindow.cpp +++ b/src/traywindow.cpp @@ -1,428 +1,428 @@ /* * traywindow.cpp - the KDE system tray applet * Program: kalarm * Copyright © 2002-2019 David Jarvie * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "traywindow.h" #include "alarmcalendar.h" //#include "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 "resources/eventmodel.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 using namespace KAlarmCal; struct TipItem { QDateTime dateTime; QString text; }; /*============================================================================= = Class: TrayWindow = The KDE system tray window. =============================================================================*/ TrayWindow::TrayWindow(MainWindow* parent) : KStatusNotifierItem(parent) , mAssocMainWindow(parent) , mStatusUpdateTimer(new QTimer(this)) { qCDebug(KALARM_LOG) << "TrayWindow:"; setToolTipIconByName(QStringLiteral("kalarm")); setToolTipTitle(KAboutData::applicationData().displayName()); setIconByName(QStringLiteral("kalarm")); setStatus(KStatusNotifierItem::Active); // Set up the context menu mActionEnabled = KAlarm::createAlarmEnableAction(this); addAction(QStringLiteral("tAlarmsEnable"), mActionEnabled); contextMenu()->addAction(mActionEnabled); connect(theApp(), &KAlarmApp::alarmEnabledToggled, this, &TrayWindow::setEnabledStatus); contextMenu()->addSeparator(); mActionNew = new NewAlarmAction(false, i18nc("@action", "&New Alarm"), this); addAction(QStringLiteral("tNew"), mActionNew); contextMenu()->addAction(mActionNew); connect(mActionNew, &NewAlarmAction::selected, this, &TrayWindow::slotNewAlarm); connect(mActionNew->fromTemplateAlarmAction(QString()), &TemplateMenuAction::selected, this, &TrayWindow::slotNewFromTemplate); contextMenu()->addSeparator(); QAction* a = KAlarm::createStopPlayAction(this); addAction(QStringLiteral("tStopPlay"), a); contextMenu()->addAction(a); QObject::connect(theApp(), &KAlarmApp::audioPlaying, a, &QAction::setVisible); QObject::connect(theApp(), &KAlarmApp::audioPlaying, this, &TrayWindow::updateStatus); a = KAlarm::createSpreadWindowsAction(this); addAction(QStringLiteral("tSpread"), a); contextMenu()->addAction(a); contextMenu()->addSeparator(); contextMenu()->addAction(KStandardAction::preferences(this, SLOT(slotPreferences()), this)); // Disable standard quit behaviour. We have to intercept the quit even, // if the main window is hidden. QAction* act = action(QStringLiteral("quit")); if (act) { act->disconnect(SIGNAL(triggered(bool)), this, SLOT(maybeQuit())); connect(act, &QAction::triggered, this, &TrayWindow::slotQuit); } // Set icon to correspond with the alarms enabled menu status setEnabledStatus(theApp()->alarmsEnabled()); connect(AlarmCalendar::resources(), &AlarmCalendar::haveDisabledAlarmsChanged, this, &TrayWindow::slotHaveDisabledAlarms); connect(this, &TrayWindow::activateRequested, this, &TrayWindow::slotActivateRequested); connect(this, &TrayWindow::secondaryActivateRequested, this, &TrayWindow::slotSecondaryActivateRequested); slotHaveDisabledAlarms(AlarmCalendar::resources()->haveDisabledAlarms()); // Hack: KSNI does not let us know when it is about to show the tooltip, // so we need to update it whenever something change in it. // This timer ensures that updateToolTip() is not called several times in a row mToolTipUpdateTimer = new QTimer(this); mToolTipUpdateTimer->setInterval(0); mToolTipUpdateTimer->setSingleShot(true); connect(mToolTipUpdateTimer, &QTimer::timeout, this, &TrayWindow::updateToolTip); // Update every minute to show accurate deadlines MinuteTimer::connect(mToolTipUpdateTimer, SLOT(start())); // Update when alarms are modified AlarmListModel* all = AlarmListModel::all(); connect(all, SIGNAL(dataChanged(QModelIndex,QModelIndex)), mToolTipUpdateTimer, SLOT(start())); connect(all, SIGNAL(rowsInserted(QModelIndex,int,int)), mToolTipUpdateTimer, SLOT(start())); connect(all, SIGNAL(rowsMoved(QModelIndex,int,int,QModelIndex,int)), mToolTipUpdateTimer, SLOT(start())); connect(all, SIGNAL(rowsRemoved(QModelIndex,int,int)), mToolTipUpdateTimer, SLOT(start())); connect(all, SIGNAL(modelReset()), mToolTipUpdateTimer, SLOT(start())); // Set auto-hide status when next alarm or preferences change mStatusUpdateTimer->setSingleShot(true); connect(mStatusUpdateTimer, &QTimer::timeout, this, &TrayWindow::updateStatus); connect(AlarmCalendar::resources(), &AlarmCalendar::earliestAlarmChanged, this, &TrayWindow::updateStatus); Preferences::connect(SIGNAL(autoHideSystemTrayChanged(int)), this, SLOT(updateStatus())); updateStatus(); // Update when tooltip preferences are modified Preferences::connect(SIGNAL(tooltipPreferencesChanged()), mToolTipUpdateTimer, SLOT(start())); } TrayWindow::~TrayWindow() { qCDebug(KALARM_LOG) << "~TrayWindow"; theApp()->removeWindow(this); Q_EMIT deleted(); } /****************************************************************************** * Called when the "New Alarm" menu item is selected to edit a new alarm. */ void TrayWindow::slotNewAlarm(EditAlarmDlg::Type type) { KAlarm::editNewAlarm(type); } /****************************************************************************** * Called when the "New Alarm" menu item is selected to edit a new alarm from a * template. */ void TrayWindow::slotNewFromTemplate(const KAEvent* event) { KAlarm::editNewAlarm(event); } /****************************************************************************** * Called when the "Configure KAlarm" menu item is selected. */ void TrayWindow::slotPreferences() { KAlarmPrefDlg::display(); } /****************************************************************************** * Called when the Quit context menu item is selected. * Note that KAlarmApp::doQuit() must be called by the event loop, not directly * from the menu item, since otherwise the tray icon will be deleted while still * processing the menu, resulting in a crash. * Ideally, the connect() call setting up this slot in the constructor would use * Qt::QueuedConnection, but the slot is never called in that case. */ void TrayWindow::slotQuit() { // Note: QTimer::singleShot(0, ...) never calls the slot. QTimer::singleShot(1, this, &TrayWindow::slotQuitAfter); } void TrayWindow::slotQuitAfter() { theApp()->doQuit(static_cast(parent())); } /****************************************************************************** * Called when the Alarms Enabled action status has changed. * Updates the alarms enabled menu item check state, and the icon pixmap. */ void TrayWindow::setEnabledStatus(bool status) { qCDebug(KALARM_LOG) << "TrayWindow::setEnabledStatus:" << status; updateIcon(); updateStatus(); updateToolTip(); } /****************************************************************************** * Called when individual alarms are enabled or disabled. * Set the enabled icon to show or hide a disabled indication. */ void TrayWindow::slotHaveDisabledAlarms(bool haveDisabled) { qCDebug(KALARM_LOG) << "TrayWindow::slotHaveDisabledAlarms:" << haveDisabled; mHaveDisabledAlarms = haveDisabled; updateIcon(); updateToolTip(); } /****************************************************************************** * Show the associated main window. */ void TrayWindow::showAssocMainWindow() { if (mAssocMainWindow) { mAssocMainWindow->show(); mAssocMainWindow->raise(); mAssocMainWindow->activateWindow(); } } /****************************************************************************** * A left click displays the KAlarm main window. */ void TrayWindow::slotActivateRequested() { // Left click: display/hide the first main window if (mAssocMainWindow && mAssocMainWindow->isVisible()) { mAssocMainWindow->raise(); mAssocMainWindow->activateWindow(); } } /****************************************************************************** * A middle button click displays the New Alarm window. */ void TrayWindow::slotSecondaryActivateRequested() { if (mActionNew->isEnabled()) mActionNew->trigger(); // display a New Alarm dialog } /****************************************************************************** * Adjust icon auto-hide status according to when the next alarm is due. * The icon is always shown if audio is playing, to give access to the 'stop' * menu option. */ void TrayWindow::updateStatus() { mStatusUpdateTimer->stop(); int period = Preferences::autoHideSystemTray(); // If the icon is always to be shown (AutoHideSystemTray = 0), // or audio is playing, show the icon. bool active = !period || MessageWin::isAudioPlaying(); if (!active) { // Show the icon only if the next active alarm complies active = theApp()->alarmsEnabled(); if (active) { KAEvent* event = AlarmCalendar::resources()->earliestAlarm(); active = static_cast(event); if (event && period > 0) { const KADateTime dt = event->nextTrigger(KAEvent::ALL_TRIGGER).effectiveKDateTime(); qint64 delay = KADateTime::currentLocalDateTime().secsTo(dt); delay -= static_cast(period) * 60; // delay until icon to be shown active = (delay <= 0); if (!active) { // First alarm trigger is too far in future, so tray icon is to // be auto-hidden. Set timer for when it should be shown again. delay *= 1000; // convert to msec int delay_int = static_cast(delay); if (delay_int != delay) delay_int = INT_MAX; mStatusUpdateTimer->setInterval(delay_int); mStatusUpdateTimer->start(); } } } } setStatus(active ? Active : Passive); } /****************************************************************************** * Adjust tooltip according to the app state. * The tooltip text shows alarms due in the next 24 hours. The limit of 24 * hours is because only times, not dates, are displayed. */ void TrayWindow::updateToolTip() { bool enabled = theApp()->alarmsEnabled(); QString subTitle; if (enabled && Preferences::tooltipAlarmCount()) subTitle = tooltipAlarmText(); if (!enabled) subTitle = i18n("Disabled"); else if (mHaveDisabledAlarms) { if (!subTitle.isEmpty()) subTitle += QLatin1String("
"); subTitle += i18nc("@info:tooltip Brief: some alarms are disabled", "(Some alarms disabled)"); } setToolTipSubTitle(subTitle); } /****************************************************************************** * Adjust icon according to the app state. */ void TrayWindow::updateIcon() { setIconByName(!theApp()->alarmsEnabled() ? QStringLiteral("kalarm-disabled") : mHaveDisabledAlarms ? QStringLiteral("kalarm-partdisabled") : QStringLiteral("kalarm")); } /****************************************************************************** * Return the tooltip text showing alarms due in the next 24 hours. * The limit of 24 hours is because only times, not dates, are displayed. */ QString TrayWindow::tooltipAlarmText() const { KAEvent event; const QString& prefix = Preferences::tooltipTimeToPrefix(); int maxCount = Preferences::tooltipAlarmCount(); const KADateTime now = KADateTime::currentLocalDateTime(); const KADateTime tomorrow = now.addDays(1); // Get today's and tomorrow's alarms, sorted in time order int i, iend; QList items; QVector events = KAlarm::getSortedActiveEvents(const_cast(this), &mAlarmsModel); for (i = 0, iend = events.count(); i < iend; ++i) { KAEvent* event = &events[i]; if (event->actionSubType() == KAEvent::MESSAGE) { TipItem item; QDateTime dateTime = event->nextTrigger(KAEvent::DISPLAY_TRIGGER).effectiveKDateTime().toLocalZone().qDateTime(); if (dateTime > tomorrow.qDateTime()) break; // ignore alarms after tomorrow at the current clock time item.dateTime = dateTime; // The alarm is due today, or early tomorrow if (Preferences::showTooltipAlarmTime()) { item.text += QLocale().toString(item.dateTime.time(), QLocale::ShortFormat); item.text += QLatin1Char(' '); } if (Preferences::showTooltipTimeToAlarm()) { int mins = (now.qDateTime().secsTo(item.dateTime) + 59) / 60; if (mins < 0) mins = 0; char minutes[3] = "00"; minutes[0] = static_cast((mins%60) / 10 + '0'); minutes[1] = static_cast((mins%60) % 10 + '0'); if (Preferences::showTooltipAlarmTime()) item.text += i18nc("@info prefix + hours:minutes", "(%1%2:%3)", prefix, mins/60, QLatin1String(minutes)); else item.text += i18nc("@info prefix + hours:minutes", "%1%2:%3", prefix, mins/60, QLatin1String(minutes)); item.text += QLatin1Char(' '); } item.text += AlarmText::summary(*event); // Insert the item into the list in time-sorted order int it = 0; for (int itend = items.count(); it < itend; ++it) { if (item.dateTime <= items[it].dateTime) break; } items.insert(it, item); } } qCDebug(KALARM_LOG) << "TrayWindow::tooltipAlarmText"; QString text; int count = 0; for (i = 0, iend = items.count(); i < iend; ++i) { qCDebug(KALARM_LOG) << "TrayWindow::tooltipAlarmText: --" << (count+1) << ")" << items[i].text; if (i > 0) text += QLatin1String("
"); text += items[i].text; if (++count == maxCount) break; } return text; } /****************************************************************************** * Called when the associated main window is closed. */ void TrayWindow::removeWindow(MainWindow* win) { if (win == mAssocMainWindow) mAssocMainWindow = nullptr; } // vim: et sw=4: diff --git a/src/traywindow.h b/src/traywindow.h index 7907f29f..9bb5a7d0 100644 --- a/src/traywindow.h +++ b/src/traywindow.h @@ -1,80 +1,80 @@ /* * traywindow.h - the KDE system tray applet * Program: kalarm * Copyright © 2002-2019 David Jarvie * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef TRAYWINDOW_H #define TRAYWINDOW_H #include "editdlg.h" #include -#include +#include class QTimer; class KToggleAction; class MainWindow; class NewAlarmAction; class AlarmListModel; using namespace KAlarmCal; class TrayWindow : public KStatusNotifierItem { Q_OBJECT public: explicit TrayWindow(MainWindow* parent); ~TrayWindow(); void removeWindow(MainWindow*); MainWindow* assocMainWindow() const { return mAssocMainWindow; } void setAssocMainWindow(MainWindow* win) { mAssocMainWindow = win; } void showAssocMainWindow(); Q_SIGNALS: void deleted(); private Q_SLOTS: void slotActivateRequested(); void slotSecondaryActivateRequested(); void slotNewAlarm(EditAlarmDlg::Type); void slotNewFromTemplate(const KAEvent*); void slotPreferences(); void setEnabledStatus(bool status); void slotHaveDisabledAlarms(bool disabled); void slotQuit(); void slotQuitAfter(); void updateStatus(); void updateToolTip(); private: QString tooltipAlarmText() const; void updateIcon(); MainWindow* mAssocMainWindow; // main window associated with this, or null KToggleAction* mActionEnabled; NewAlarmAction* mActionNew; mutable AlarmListModel* mAlarmsModel{nullptr}; // active alarms sorted in time order QTimer* mStatusUpdateTimer; QTimer* mToolTipUpdateTimer; bool mHaveDisabledAlarms{false}; // some individually disabled alarms exist }; #endif // TRAYWINDOW_H // vim: et sw=4: diff --git a/src/wakedlg.cpp b/src/wakedlg.cpp index f8c63a06..a8b17dbd 100644 --- a/src/wakedlg.cpp +++ b/src/wakedlg.cpp @@ -1,204 +1,204 @@ /* * wakedlg.cpp - dialog to configure wake-from-suspend alarms * Program: kalarm * Copyright © 2011-2019 David Jarvie * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "wakedlg.h" #include "ui_wakedlg.h" #include "alarmcalendar.h" #include "functions.h" #include "kalarmapp.h" #include "mainwindow.h" #include "messagebox.h" #include "preferences.h" #include "kalarm_debug.h" #include #include #include #include -#include +#include #include using namespace KAlarmCal; WakeFromSuspendDlg* WakeFromSuspendDlg::mInstance = nullptr; WakeFromSuspendDlg* WakeFromSuspendDlg::create(QWidget* parent) { if (!mInstance) mInstance = new WakeFromSuspendDlg(parent); return mInstance; } WakeFromSuspendDlg::WakeFromSuspendDlg(QWidget* parent) : QDialog(parent) { setAttribute(Qt::WA_DeleteOnClose); setWindowTitle(i18nc("@title:window", "Wake From Suspend")); mUi = new Ui_WakeFromSuspendDlgWidget; mUi->setupUi(this); mUi->advanceWakeTime->setValue(Preferences::wakeFromSuspendAdvance()); mMainWindow = qobject_cast(parent); if (!mMainWindow) mMainWindow = MainWindow::mainMainWindow(); // Check if there is any alarm selected in the main window, and enable/disable // the Show and Cancel buttons as necessary. enableDisableUseButton(); // Update the Show and Cancel button status every 5 seconds mTimer = new QTimer(this); connect(mTimer, &QTimer::timeout, this, &WakeFromSuspendDlg::checkPendingAlarm); mTimer->start(5000); connect(mMainWindow, &MainWindow::selectionChanged, this, &WakeFromSuspendDlg::enableDisableUseButton); connect(mUi->showWakeButton, &QPushButton::clicked, this, &WakeFromSuspendDlg::showWakeClicked); connect(mUi->useWakeButton, &QPushButton::clicked, this, &WakeFromSuspendDlg::useWakeClicked); connect(mUi->cancelWakeButton, &QPushButton::clicked, this, &WakeFromSuspendDlg::cancelWakeClicked); connect(mUi->buttonBox, &QDialogButtonBox::rejected, this, &WakeFromSuspendDlg::close); connect(theApp(), &KAlarmApp::alarmEnabledToggled, this, &WakeFromSuspendDlg::enableDisableUseButton); } WakeFromSuspendDlg::~WakeFromSuspendDlg() { if (mInstance == this) mInstance = nullptr; delete mUi; } /****************************************************************************** * Called when the alarm selection in the main window changes. * Enable or disable the Use Highlighted Alarm button. */ void WakeFromSuspendDlg::enableDisableUseButton() { bool enable = theApp()->alarmsEnabled(); if (enable) { const QString wakeFromSuspendId = KAlarm::checkRtcWakeConfig().value(0); const KAEvent event = mMainWindow->selectedEvent(); enable = event.isValid() && event.category() == CalEvent::ACTIVE && event.enabled() && !event.mainDateTime().isDateOnly() && event.id() != wakeFromSuspendId; } mUi->useWakeButton->setEnabled(enable); checkPendingAlarm(); } /****************************************************************************** * Update the Show and Cancel buttons if the pending alarm status has changed. * Reply = true if an alarm is still pending. */ bool WakeFromSuspendDlg::checkPendingAlarm() { if (KAlarm::checkRtcWakeConfig(true).isEmpty()) { mUi->showWakeButton->setEnabled(false); mUi->cancelWakeButton->setEnabled(false); return false; } return true; } /****************************************************************************** * Called when the user clicks the Show Current Alarm button. * Highlight the currently scheduled wake-from-suspend alarm in the main window. */ void WakeFromSuspendDlg::showWakeClicked() { if (checkPendingAlarm()) { const QStringList params = KAlarm::checkRtcWakeConfig(); if (!params.isEmpty()) { const KAEvent* event = AlarmCalendar::resources()->event(EventId(params[0].toLongLong(), params[1])); if (event) { mMainWindow->selectEvent(event->id()); return; } } } mMainWindow->clearSelection(); } /****************************************************************************** * Called when the user clicks the Use Highlighted Alarm button. * Schedules system wakeup for that alarm. */ void WakeFromSuspendDlg::useWakeClicked() { const KAEvent event = mMainWindow->selectedEvent(); if (!event.isValid()) return; const KADateTime dt = event.mainDateTime().kDateTime(); if (dt.isDateOnly()) { KAMessageBox::sorry(this, i18nc("@info", "Cannot schedule wakeup time for a date-only alarm")); return; } if (KAMessageBox::warningContinueCancel(this, xi18nc("@info", "This wakeup will cancel any existing wakeup which has been set by KAlarm " "or any other application, because your computer can only schedule a single wakeup time." "Note: Wake From Suspend is not supported at all on some computers, especially older ones, " "and some computers only support setting a wakeup time up to 24 hours ahead. " "You may wish to set up a test alarm to check your system's capability."), QString(), KStandardGuiItem::cont(), KStandardGuiItem::cancel(), QStringLiteral("wakeupWarning")) != KMessageBox::Continue) return; const int advance = mUi->advanceWakeTime->value(); #if KALARMCAL_VERSION >= QT_VERSION_CHECK(5,12,1) const qint64 triggerTime = dt.addSecs(-advance * 60).toSecsSinceEpoch(); #else unsigned triggerTime = dt.addSecs(-advance * 60).toTime_t(); #endif if (KAlarm::setRtcWakeTime(triggerTime, this)) { const QStringList param{QString::number(event.collectionId()), event.id(), QString::number(triggerTime)}; KConfigGroup config(KSharedConfig::openConfig(), "General"); config.writeEntry("RtcWake", param); config.sync(); Preferences::setWakeFromSuspendAdvance(advance); close(); } } /****************************************************************************** * Called when the user clicks the Cancel Wake From Suspend button. * Cancels any currently scheduled system wakeup. */ void WakeFromSuspendDlg::cancelWakeClicked() { KAlarm::setRtcWakeTime(0, this); KAlarm::deleteRtcWakeConfig(); mUi->showWakeButton->setEnabled(false); mUi->cancelWakeButton->setEnabled(false); enableDisableUseButton(); } // vim: et sw=4: