diff --git a/CMakeLists.txt b/CMakeLists.txt index 05f6aa8d..6688bcd8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,127 +1,128 @@ cmake_minimum_required(VERSION 3.5) set(KDEPIM_VERSION_NUMBER "5.12.40") set(PIM_VERSION ${KDEPIM_VERSION_NUMBER}) set(KALARM_VERSION "2.12.5") project(kalarm VERSION ${KALARM_VERSION}) set(KF5_MIN_VERSION "5.60.0") find_package(ECM ${KF5_MIN_VERSION} CONFIG REQUIRED) set(CMAKE_MODULE_PATH ${kalarm_SOURCE_DIR}/cmake/modules ${ECM_MODULE_PATH}) include(ECMInstallIcons) include(ECMSetupVersion) include(ECMAddTests) include(GenerateExportHeader) include(ECMGenerateHeaders) include(FeatureSummary) include(CheckFunctionExists) include(ECMGeneratePriFile) include(KDEInstallDirs) include(KDECMakeSettings) include(KDEFrameworkCompilerSettings NO_POLICY_SCOPE) include(ECMAddAppIcon) include(ECMQtDeclareLoggingCategory) # Do NOT add quote set(KDEPIM_DEV_VERSION alpha) # add an extra space if(DEFINED KDEPIM_DEV_VERSION) set(KDEPIM_DEV_VERSION " ${KDEPIM_DEV_VERSION}") endif() set(KDEPIM_VERSION "${KDEPIM_VERSION_NUMBER}${KDEPIM_DEV_VERSION}") set(KIMAP_LIB_VERSION "5.12.40") set(AKONADI_MIMELIB_VERSION "5.12.40") set(AKONADI_CONTACT_VERSION "5.12.40") set(KMAILTRANSPORT_LIB_VERSION "5.12.40") set(KPIMTEXTEDIT_LIB_VERSION "5.12.40") set(IDENTITYMANAGEMENT_LIB_VERSION "5.12.40") set(AKONADI_VERSION "5.12.40") set(KMIME_LIB_VERSION "5.12.40") set(AKONADIKALARM_LIB_VERSION "5.12.40") set(PIMCOMMON_LIB_VERSION_LIB "5.12.40") set(KDEPIM_LIB_VERSION "${KDEPIM_VERSION_NUMBER}") set(KDEPIM_LIB_SOVERSION "5") set(QT_REQUIRED_VERSION "5.11.0") find_package(Qt5 ${QT_REQUIRED_VERSION} CONFIG REQUIRED DBus Gui Network Widgets) find_package(Qt5X11Extras NO_MODULE) set(MAILCOMMON_LIB_VERSION_LIB "5.12.40") set(LIBKDEPIM_LIB_VERSION_LIB "5.12.40") set(KCALENDARCORE_LIB_VERSION "5.12.40") set(CALENDARUTILS_LIB_VERSION "5.12.40") set(KDEPIM_APPS_LIB_VERSION_LIB "5.12.40") # Find KF5 package find_package(KF5Auth ${KF5_MIN_VERSION} CONFIG REQUIRED) find_package(KF5Codecs ${KF5_MIN_VERSION} CONFIG REQUIRED) find_package(KF5Completion ${KF5_MIN_VERSION} REQUIRED) find_package(KF5Config ${KF5_MIN_VERSION} CONFIG REQUIRED) find_package(KF5ConfigWidgets ${KF5_MIN_VERSION} CONFIG REQUIRED) find_package(KF5Crash ${KF5_MIN_VERSION} CONFIG REQUIRED) find_package(KF5DBusAddons ${KF5_MIN_VERSION} CONFIG REQUIRED) find_package(KF5DocTools ${KF5_MIN_VERSION} REQUIRED) find_package(KF5GlobalAccel ${KF5_MIN_VERSION} REQUIRED) find_package(KF5GuiAddons ${KF5_MIN_VERSION} REQUIRED) find_package(KF5I18n ${KF5_MIN_VERSION} CONFIG REQUIRED) find_package(KF5IconThemes ${KF5_MIN_VERSION} REQUIRED) find_package(KF5JobWidgets ${KF5_MIN_VERSION} REQUIRED) find_package(KF5KCMUtils ${KF5_MIN_VERSION} CONFIG REQUIRED) find_package(KF5KIO ${KF5_MIN_VERSION} CONFIG REQUIRED) find_package(KF5Notifications ${KF5_MIN_VERSION} CONFIG REQUIRED) find_package(KF5Service ${KF5_MIN_VERSION} CONFIG REQUIRED) find_package(KF5WidgetsAddons ${KF5_MIN_VERSION} CONFIG REQUIRED) find_package(KF5WindowSystem ${KF5_MIN_VERSION} CONFIG REQUIRED) find_package(KF5XmlGui ${KF5_MIN_VERSION} CONFIG REQUIRED) find_package(KF5Holidays ${KF5_MIN_VERSION} CONFIG REQUIRED) find_package(Phonon4Qt5 CONFIG REQUIRED) # Find KdepimLibs Package find_package(KF5IMAP ${KIMAP_LIB_VERSION} CONFIG REQUIRED) find_package(KF5Akonadi ${AKONADI_VERSION} CONFIG REQUIRED) find_package(KF5AkonadiContact ${AKONADI_CONTACT_VERSION} CONFIG REQUIRED) find_package(KF5AkonadiMime ${AKONADI_MIMELIB_VERSION} CONFIG REQUIRED) find_package(KF5AlarmCalendar ${AKONADIKALARM_LIB_VERSION} CONFIG REQUIRED) find_package(KF5CalendarCore ${KCALENDARCORE_LIB_VERSION} CONFIG REQUIRED) find_package(KF5CalendarUtils ${CALENDARUTILS_LIB_VERSION} CONFIG REQUIRED) find_package(KF5IdentityManagement ${IDENTITYMANAGEMENT_LIB_VERSION} CONFIG REQUIRED) find_package(KF5KdepimDBusInterfaces ${KDEPIM_APPS_LIB_VERSION_LIB} CONFIG REQUIRED) find_package(KF5Libkdepim ${LIBKDEPIM_LIB_VERSION_LIB} CONFIG REQUIRED) find_package(KF5MailCommon ${MAILCOMMON_LIB_VERSION_LIB} CONFIG REQUIRED) find_package(KF5MailTransportAkonadi ${KMAILTRANSPORT_LIB_VERSION} CONFIG REQUIRED) find_package(KF5Mime ${KMIME_LIB_VERSION} CONFIG REQUIRED) find_package(KF5PimCommon ${PIMCOMMON_LIB_VERSION_LIB} CONFIG REQUIRED) find_package(KF5PimTextEdit ${KPIMTEXTEDIT_LIB_VERSION} CONFIG REQUIRED) if (NOT APPLE) find_package(X11) endif() set(CMAKE_MODULE_PATH ${kalarm_SOURCE_DIR}/cmake/modules ${ECM_MODULE_PATH}) find_package(Xsltproc) set_package_properties(Xsltproc PROPERTIES DESCRIPTION "XSLT processor from libxslt" TYPE REQUIRED PURPOSE "Required to generate D-Bus interfaces for all Akonadi resources.") set(KDEPIM_HAVE_X11 ${X11_FOUND}) configure_file(src/config-kalarm.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config-kalarm.h ) include_directories(${kalarm_SOURCE_DIR} ${kalarm_BINARY_DIR}) add_definitions(-DQT_MESSAGELOGCONTEXT) add_definitions(-DQT_NO_FOREACH) +add_definitions(-DQT_DISABLE_DEPRECATED_BEFORE=0x060000) add_subdirectory(src) install(FILES kalarm.renamecategories kalarm.categories DESTINATION ${KDE_INSTALL_LOGGINGCATEGORIESDIR}) add_subdirectory(doc) feature_summary(WHAT ALL FATAL_ON_MISSING_REQUIRED_PACKAGES) diff --git a/src/collectionmodel.cpp b/src/collectionmodel.cpp index b212d55f..69c9420a 100644 --- a/src/collectionmodel.cpp +++ b/src/collectionmodel.cpp @@ -1,1460 +1,1460 @@ /* * collectionmodel.cpp - Akonadi collection models * Program: kalarm * Copyright © 2007-2019 David Jarvie * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "collectionmodel.h" #include "autoqpointer.h" #include "messagebox.h" #include "preferences.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "kalarm_debug.h" using namespace Akonadi; using namespace KAlarmCal; static Collection::Rights writableRights = Collection::CanChangeItem | Collection::CanCreateItem | Collection::CanDeleteItem; /*============================================================================= = Class: CollectionMimeTypeFilterModel = Proxy model to filter AkonadiModel to restrict its contents to Collections, = not Items, containing specified KAlarm content mime types. = It can optionally be restricted to writable and/or enabled Collections. =============================================================================*/ class CollectionMimeTypeFilterModel : public Akonadi::EntityMimeTypeFilterModel { Q_OBJECT public: explicit CollectionMimeTypeFilterModel(QObject* parent = nullptr); void setEventTypeFilter(CalEvent::Type); void setFilterWritable(bool writable); void setFilterEnabled(bool enabled); Akonadi::Collection collection(int row) const; Akonadi::Collection collection(const QModelIndex&) const; QModelIndex collectionIndex(const Akonadi::Collection&) const; protected: bool filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const override; private: CalEvent::Type mAlarmType; // collection content type contained in this model bool mWritableOnly; // only include writable collections in this model bool mEnabledOnly; // only include enabled collections in this model }; CollectionMimeTypeFilterModel::CollectionMimeTypeFilterModel(QObject* parent) : EntityMimeTypeFilterModel(parent), mAlarmType(CalEvent::EMPTY), mWritableOnly(false), mEnabledOnly(false) { addMimeTypeInclusionFilter(Collection::mimeType()); // select collections, not items setHeaderGroup(EntityTreeModel::CollectionTreeHeaders); setSourceModel(AkonadiModel::instance()); } void CollectionMimeTypeFilterModel::setEventTypeFilter(CalEvent::Type type) { if (type != mAlarmType) { mAlarmType = type; invalidateFilter(); } } void CollectionMimeTypeFilterModel::setFilterWritable(bool writable) { if (writable != mWritableOnly) { mWritableOnly = writable; invalidateFilter(); } } void CollectionMimeTypeFilterModel::setFilterEnabled(bool enabled) { if (enabled != mEnabledOnly) { Q_EMIT layoutAboutToBeChanged(); mEnabledOnly = enabled; invalidateFilter(); Q_EMIT layoutChanged(); } } bool CollectionMimeTypeFilterModel::filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const { if (!EntityMimeTypeFilterModel::filterAcceptsRow(sourceRow, sourceParent)) return false; AkonadiModel* model = AkonadiModel::instance(); const QModelIndex ix = model->index(sourceRow, 0, sourceParent); const Collection collection = model->data(ix, AkonadiModel::CollectionRole).value(); if (collection.remoteId().isEmpty()) return false; // invalidly configured resource if (!AgentManager::self()->instance(collection.resource()).isValid()) return false; if (!mWritableOnly && mAlarmType == CalEvent::EMPTY) return true; if (mWritableOnly && (collection.rights() & writableRights) != writableRights) return false; if (mAlarmType != CalEvent::EMPTY && !collection.contentMimeTypes().contains(CalEvent::mimeType(mAlarmType))) return false; if ((mWritableOnly || mEnabledOnly) && !collection.hasAttribute()) return false; if (mWritableOnly && (!collection.hasAttribute() || collection.attribute()->compatibility() != KACalendar::Current)) return false; if (mEnabledOnly && !collection.attribute()->isEnabled(mAlarmType)) return false; return true; } /****************************************************************************** * Return the collection for a given row. */ Collection CollectionMimeTypeFilterModel::collection(int row) const { return static_cast(sourceModel())->data(mapToSource(index(row, 0)), EntityTreeModel::CollectionRole).value(); } Collection CollectionMimeTypeFilterModel::collection(const QModelIndex& index) const { return static_cast(sourceModel())->data(mapToSource(index), EntityTreeModel::CollectionRole).value(); } QModelIndex CollectionMimeTypeFilterModel::collectionIndex(const Collection& collection) const { return mapFromSource(static_cast(sourceModel())->collectionIndex(collection)); } /*============================================================================= = Class: CollectionListModel = Proxy model converting the AkonadiModel collection tree into a flat list. = The model may be restricted to specified content mime types. = It can optionally be restricted to writable and/or enabled Collections. =============================================================================*/ CollectionListModel::CollectionListModel(QObject* parent) : KDescendantsProxyModel(parent), mUseCollectionColour(true) { setSourceModel(new CollectionMimeTypeFilterModel(this)); setDisplayAncestorData(false); } /****************************************************************************** * Return the collection for a given row. */ Collection CollectionListModel::collection(int row) const { return data(index(row, 0), EntityTreeModel::CollectionRole).value(); } Collection CollectionListModel::collection(const QModelIndex& index) const { return data(index, EntityTreeModel::CollectionRole).value(); } QModelIndex CollectionListModel::collectionIndex(const Collection& collection) const { return mapFromSource(static_cast(sourceModel())->collectionIndex(collection)); } void CollectionListModel::setEventTypeFilter(CalEvent::Type type) { static_cast(sourceModel())->setEventTypeFilter(type); } void CollectionListModel::setFilterWritable(bool writable) { static_cast(sourceModel())->setFilterWritable(writable); } void CollectionListModel::setFilterEnabled(bool enabled) { static_cast(sourceModel())->setFilterEnabled(enabled); } bool CollectionListModel::isDescendantOf(const QModelIndex& ancestor, const QModelIndex& descendant) const { Q_UNUSED(descendant); return !ancestor.isValid(); } /****************************************************************************** * Return the data for a given role, for a specified item. */ QVariant CollectionListModel::data(const QModelIndex& index, int role) const { switch (role) { case Qt::BackgroundRole: if (!mUseCollectionColour) role = AkonadiModel::BaseColourRole; break; default: break; } return KDescendantsProxyModel::data(index, role); } /*============================================================================= = Class: CollectionCheckListModel = Proxy model providing a checkable list of all Collections. A Collection's = checked status is equivalent to whether it is selected or not. = An alarm type is specified, whereby Collections which are enabled for that = alarm type are checked; Collections which do not contain that alarm type, or = which are disabled for that alarm type, are unchecked. =============================================================================*/ CollectionListModel* CollectionCheckListModel::mModel = nullptr; int CollectionCheckListModel::mInstanceCount = 0; CollectionCheckListModel::CollectionCheckListModel(CalEvent::Type type, QObject* parent) : KCheckableProxyModel(parent), mAlarmType(type) { ++mInstanceCount; if (!mModel) mModel = new CollectionListModel(nullptr); setSourceModel(mModel); // the source model is NOT filtered by alarm type mSelectionModel = new QItemSelectionModel(mModel); setSelectionModel(mSelectionModel); connect(mSelectionModel, &QItemSelectionModel::selectionChanged, this, &CollectionCheckListModel::selectionChanged); connect(mModel, SIGNAL(rowsAboutToBeInserted(QModelIndex,int,int)), SIGNAL(layoutAboutToBeChanged())); connect(mModel, &QAbstractItemModel::rowsInserted, this, &CollectionCheckListModel::slotRowsInserted); // This is probably needed to make CollectionFilterCheckListModel update // (similarly to when rows are inserted). connect(mModel, SIGNAL(rowsAboutToBeRemoved(QModelIndex,int,int)), SIGNAL(layoutAboutToBeChanged())); connect(mModel, SIGNAL(rowsRemoved(QModelIndex,int,int)), SIGNAL(layoutChanged())); connect(AkonadiModel::instance(), &AkonadiModel::collectionStatusChanged, this, &CollectionCheckListModel::collectionStatusChanged); // Initialise checked status for all collections. // Note that this is only necessary if the model is recreated after // being deleted. for (int row = 0, count = mModel->rowCount(); row < count; ++row) setSelectionStatus(mModel->collection(row), mModel->index(row, 0)); } CollectionCheckListModel::~CollectionCheckListModel() { if (--mInstanceCount <= 0) { delete mModel; mModel = nullptr; } } /****************************************************************************** * Return the collection for a given row. */ Collection CollectionCheckListModel::collection(int row) const { return mModel->collection(mapToSource(index(row, 0))); } Collection CollectionCheckListModel::collection(const QModelIndex& index) const { return mModel->collection(mapToSource(index)); } /****************************************************************************** * Return model data for one index. */ QVariant CollectionCheckListModel::data(const QModelIndex& index, int role) const { const Collection collection = mModel->collection(index); if (collection.isValid()) { // This is a Collection row switch (role) { case Qt::ForegroundRole: { const QString mimeType = CalEvent::mimeType(mAlarmType); if (collection.contentMimeTypes().contains(mimeType)) return AkonadiModel::foregroundColor(collection, QStringList(mimeType)); break; } case Qt::FontRole: { if (!collection.hasAttribute() || !AkonadiModel::isCompatible(collection)) break; const CollectionAttribute* attr = collection.attribute(); if (!attr->enabled()) break; const QStringList mimeTypes = collection.contentMimeTypes(); if (attr->isStandard(mAlarmType) && mimeTypes.contains(CalEvent::mimeType(mAlarmType))) { // It's the standard collection for a mime type QFont font = qvariant_cast(KCheckableProxyModel::data(index, role)); font.setBold(true); return font; } break; } default: break; } } return KCheckableProxyModel::data(index, role); } /****************************************************************************** * Set model data for one index. * If the change is to disable a collection, check for eligibility and prevent * the change if necessary. */ bool CollectionCheckListModel::setData(const QModelIndex& index, const QVariant& value, int role) { if (role == Qt::CheckStateRole && static_cast(value.toInt()) != Qt::Checked) { // A collection is to be disabled. const Collection collection = mModel->collection(index); if (collection.isValid() && collection.hasAttribute()) { const CollectionAttribute* attr = collection.attribute(); if (attr->isEnabled(mAlarmType)) { QString errmsg; QWidget* messageParent = qobject_cast(QObject::parent()); if (attr->isStandard(mAlarmType) && AkonadiModel::isCompatible(collection)) { // It's the standard collection for some alarm type. if (mAlarmType == CalEvent::ACTIVE) { errmsg = i18nc("@info", "You cannot disable your default active alarm calendar."); } else if (mAlarmType == CalEvent::ARCHIVED && Preferences::archivedKeepDays()) { // Only allow the archived alarms standard collection to be disabled if // we're not saving expired alarms. errmsg = i18nc("@info", "You cannot disable your default archived alarm calendar " "while expired alarms are configured to be kept."); } else if (KAMessageBox::warningContinueCancel(messageParent, i18nc("@info", "Do you really want to disable your default calendar?")) == KMessageBox::Cancel) return false; } if (!errmsg.isEmpty()) { KAMessageBox::sorry(messageParent, errmsg); return false; } } } } return KCheckableProxyModel::setData(index, value, role); } /****************************************************************************** * Called when rows have been inserted into the model. * Select or deselect them according to their enabled status. */ void CollectionCheckListModel::slotRowsInserted(const QModelIndex& parent, int start, int end) { Q_EMIT layoutAboutToBeChanged(); for (int row = start; row <= end; ++row) { const QModelIndex ix = mapToSource(index(row, 0, parent)); const Collection collection = mModel->collection(ix); if (collection.isValid()) setSelectionStatus(collection, ix); } Q_EMIT layoutChanged(); // this is needed to make CollectionFilterCheckListModel update } /****************************************************************************** * Called when the user has ticked/unticked a collection to enable/disable it * (or when the selection changes for any other reason). */ void CollectionCheckListModel::selectionChanged(const QItemSelection& selected, const QItemSelection& deselected) { const QModelIndexList sel = selected.indexes(); for (const QModelIndex& ix : sel) { // Try to enable the collection, but untick it if not possible if (!CollectionControlModel::setEnabled(mModel->collection(ix), mAlarmType, true)) mSelectionModel->select(ix, QItemSelectionModel::Deselect); } const QModelIndexList desel = deselected.indexes(); for (const QModelIndex& ix : desel) CollectionControlModel::setEnabled(mModel->collection(ix), mAlarmType, false); } /****************************************************************************** * Called when a collection parameter or status has changed. * If the collection's alarm types have been reconfigured, ensure that the * model views are updated to reflect this. */ void CollectionCheckListModel::collectionStatusChanged(const Collection& collection, AkonadiModel::Change change, const QVariant&, bool inserted) { if (inserted || !collection.isValid()) return; switch (change) { case AkonadiModel::Enabled: qCDebug(KALARM_LOG) << debugType("collectionStatusChanged").constData() << "Enabled" << collection.id(); break; case AkonadiModel::AlarmTypes: qCDebug(KALARM_LOG) << debugType("collectionStatusChanged").constData() << "AlarmTypes" << collection.id(); break; default: return; } const QModelIndex ix = mModel->collectionIndex(collection); if (ix.isValid()) setSelectionStatus(collection, ix); if (change == AkonadiModel::AlarmTypes) Q_EMIT collectionTypeChange(this); } /****************************************************************************** * Select or deselect an index according to its enabled status. */ void CollectionCheckListModel::setSelectionStatus(const Collection& collection, const QModelIndex& sourceIndex) { const QItemSelectionModel::SelectionFlags sel = (collection.hasAttribute() && collection.attribute()->isEnabled(mAlarmType)) ? QItemSelectionModel::Select : QItemSelectionModel::Deselect; mSelectionModel->select(sourceIndex, sel); } /****************************************************************************** * Return the instance's alarm type, as a string. */ QByteArray CollectionCheckListModel::debugType(const char* func) const { const char* type; switch (mAlarmType) { case CalEvent::ACTIVE: type = "CollectionCheckListModel[Act]::"; break; case CalEvent::ARCHIVED: type = "CollectionCheckListModel[Arch]::"; break; case CalEvent::TEMPLATE: type = "CollectionCheckListModel[Tmpl]::"; break; default: type = "CollectionCheckListModel::"; break; } return QByteArray(type) + func + ":"; } /*============================================================================= = Class: CollectionFilterCheckListModel = Proxy model providing a checkable collection list. The model contains all = alarm types, but returns only one type at any given time. The selected alarm = type may be changed as desired. =============================================================================*/ CollectionFilterCheckListModel::CollectionFilterCheckListModel(QObject* parent) : QSortFilterProxyModel(parent), mActiveModel(new CollectionCheckListModel(CalEvent::ACTIVE, this)), mArchivedModel(new CollectionCheckListModel(CalEvent::ARCHIVED, this)), mTemplateModel(new CollectionCheckListModel(CalEvent::TEMPLATE, this)), mAlarmType(CalEvent::EMPTY) { setDynamicSortFilter(true); connect(mActiveModel, &CollectionCheckListModel::collectionTypeChange, this, &CollectionFilterCheckListModel::collectionTypeChanged); connect(mArchivedModel, &CollectionCheckListModel::collectionTypeChange, this, &CollectionFilterCheckListModel::collectionTypeChanged); connect(mTemplateModel, &CollectionCheckListModel::collectionTypeChange, this, &CollectionFilterCheckListModel::collectionTypeChanged); } void CollectionFilterCheckListModel::setEventTypeFilter(CalEvent::Type type) { if (type != mAlarmType) { CollectionCheckListModel* newModel; switch (type) { case CalEvent::ACTIVE: newModel = mActiveModel; break; case CalEvent::ARCHIVED: newModel = mArchivedModel; break; case CalEvent::TEMPLATE: newModel = mTemplateModel; break; default: return; } mAlarmType = type; setSourceModel(newModel); invalidate(); } } /****************************************************************************** * Return the collection for a given row. */ Collection CollectionFilterCheckListModel::collection(int row) const { return static_cast(sourceModel())->collection(mapToSource(index(row, 0))); } Collection CollectionFilterCheckListModel::collection(const QModelIndex& index) const { return static_cast(sourceModel())->collection(mapToSource(index)); } QVariant CollectionFilterCheckListModel::data(const QModelIndex& index, int role) const { switch (role) { case Qt::ToolTipRole: { const Collection col = collection(index); if (col.isValid()) return AkonadiModel::instance()->tooltip(col, mAlarmType); break; } default: break; } return QSortFilterProxyModel::data(index, role); } bool CollectionFilterCheckListModel::filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const { if (mAlarmType == CalEvent::EMPTY) return true; const CollectionCheckListModel* model = static_cast(sourceModel()); const Collection collection = model->collection(model->index(sourceRow, 0, sourceParent)); return collection.contentMimeTypes().contains(CalEvent::mimeType(mAlarmType)); } /****************************************************************************** * Called when a collection alarm type has changed. * Ensure that the collection is removed from or added to the current model view. */ void CollectionFilterCheckListModel::collectionTypeChanged(CollectionCheckListModel* model) { if (model == sourceModel()) invalidateFilter(); } /*============================================================================= = Class: CollectionView = View displaying a list of collections. =============================================================================*/ CollectionView::CollectionView(CollectionFilterCheckListModel* model, QWidget* parent) : QListView(parent) { setModel(model); } void CollectionView::setModel(QAbstractItemModel* model) { QListView::setModel(model); } /****************************************************************************** * Return the collection for a given row. */ Collection CollectionView::collection(int row) const { return static_cast(model())->collection(row); } Collection CollectionView::collection(const QModelIndex& index) const { return static_cast(model())->collection(index); } /****************************************************************************** * Called when a mouse button is released. * Any currently selected collection is deselected. */ void CollectionView::mouseReleaseEvent(QMouseEvent* e) { if (!indexAt(e->pos()).isValid()) clearSelection(); QListView::mouseReleaseEvent(e); } /****************************************************************************** * Called when a ToolTip or WhatsThis event occurs. */ bool CollectionView::viewportEvent(QEvent* e) { if (e->type() == QEvent::ToolTip && isActiveWindow()) { const QHelpEvent* he = static_cast(e); const QModelIndex index = indexAt(he->pos()); QVariant value = static_cast(model())->data(index, Qt::ToolTipRole); if (value.canConvert()) { QString toolTip = value.toString(); int i = toolTip.indexOf(QLatin1Char('@')); if (i > 0) { int j = toolTip.indexOf(QRegExp(QLatin1String("<(nl|br)"), Qt::CaseInsensitive), i + 1); int k = toolTip.indexOf(QLatin1Char('@'), j); const QString name = toolTip.mid(i + 1, j - i - 1); value = model()->data(index, Qt::FontRole); const QFontMetrics fm(qvariant_cast(value).resolve(viewOptions().font)); int textWidth = fm.boundingRect(name).width() + 1; const int margin = QApplication::style()->pixelMetric(QStyle::PM_FocusFrameHMargin) + 1; QStyleOptionButton opt; opt.QStyleOption::operator=(viewOptions()); opt.rect = rectForIndex(index); - int checkWidth = QApplication::style()->subElementRect(QStyle::SE_ViewItemCheckIndicator, &opt).width(); + int checkWidth = QApplication::style()->subElementRect(QStyle::SE_ItemViewItemCheckIndicator, &opt).width(); int left = spacing() + 3*margin + checkWidth + viewOptions().decorationSize.width(); // left offset of text int right = left + textWidth; if (left >= horizontalOffset() + spacing() && right <= horizontalOffset() + width() - spacing() - 2*frameWidth()) { // The whole of the collection name is already displayed, // so omit it from the tooltip. if (k > 0) toolTip.remove(i, k + 1 - i); } else { toolTip.remove(k, 1); toolTip.remove(i, 1); } } QToolTip::showText(he->globalPos(), toolTip, this); return true; } } return QListView::viewportEvent(e); } /*============================================================================= = Class: CollectionControlModel = Proxy model to select which Collections will be enabled. Disabled Collections = are not populated or monitored; their contents are ignored. The set of = enabled Collections is stored in the config file's "Collections" group. = Note that this model is not used directly for displaying - its purpose is to = allow collections to be disabled, which will remove them from the other = collection models. =============================================================================*/ CollectionControlModel* CollectionControlModel::mInstance = nullptr; bool CollectionControlModel::mAskDestination = false; QHash CollectionControlModel::mAgentPaths; static QRegularExpression matchMimeType(QStringLiteral("^application/x-vnd\\.kde\\.alarm.*"), QRegularExpression::DotMatchesEverythingOption); CollectionControlModel* CollectionControlModel::instance() { if (!mInstance) mInstance = new CollectionControlModel(qApp); return mInstance; } CollectionControlModel::CollectionControlModel(QObject* parent) : FavoriteCollectionsModel(AkonadiModel::instance(), KConfigGroup(KSharedConfig::openConfig(), "Collections"), parent), mPopulatedCheckLoop(nullptr) { // Initialise the list of enabled collections EntityMimeTypeFilterModel* filter = new EntityMimeTypeFilterModel(this); filter->addMimeTypeInclusionFilter(Collection::mimeType()); filter->setSourceModel(AkonadiModel::instance()); QList collectionIds; findEnabledCollections(filter, QModelIndex(), collectionIds); setCollections(Collection::List()); for (Collection::Id id : qAsConst(collectionIds)) addCollection(Collection(id)); connect(AkonadiModel::instance(), &AkonadiModel::collectionStatusChanged, this, &CollectionControlModel::statusChanged); connect(AkonadiModel::instance(), &EntityTreeModel::collectionTreeFetched, this, &CollectionControlModel::collectionPopulated); connect(AkonadiModel::instance(), &EntityTreeModel::collectionPopulated, this, &CollectionControlModel::collectionPopulated); connect(AkonadiModel::instance(), SIGNAL(serverStopped()), SLOT(reset())); } /****************************************************************************** * Recursive function to check all collections' enabled status, and to compile a * list of all collections which have any alarm types enabled. * Collections which duplicate the same backend storage are filtered out, to * avoid crashes due to duplicate events in different resources. */ void CollectionControlModel::findEnabledCollections(const EntityMimeTypeFilterModel* filter, const QModelIndex& parent, QList& collectionIds) const { AkonadiModel* model = AkonadiModel::instance(); for (int row = 0, count = filter->rowCount(parent); row < count; ++row) { const QModelIndex ix = filter->index(row, 0, parent); const Collection collection = model->data(filter->mapToSource(ix), AkonadiModel::CollectionRole).value(); if (!AgentManager::self()->instance(collection.resource()).isValid()) continue; // the collection doesn't belong to a resource, so omit it const CalEvent::Types enabled = !collection.hasAttribute() ? CalEvent::EMPTY : collection.attribute()->enabled(); const CalEvent::Types canEnable = checkTypesToEnable(collection, collectionIds, enabled); if (canEnable != enabled) { // There is another collection which uses the same backend // storage. Disable alarm types enabled in the other collection. if (!model->isCollectionBeingDeleted(collection.id())) model->setData(model->collectionIndex(collection), static_cast(canEnable), AkonadiModel::EnabledTypesRole); } if (canEnable) collectionIds += collection.id(); if (filter->rowCount(ix) > 0) findEnabledCollections(filter, ix, collectionIds); } } /****************************************************************************** * Return whether a collection is enabled for a given alarm type. */ bool CollectionControlModel::isEnabled(const Collection& collection, CalEvent::Type type) { if (!collection.isValid() || !instance()->collectionIds().contains(collection.id())) return false; if (!AgentManager::self()->instance(collection.resource()).isValid()) { // The collection doesn't belong to a resource, so it can't be used. // Remove it from the list of collections. instance()->removeCollection(collection); return false; } Collection col = collection; AkonadiModel::instance()->refresh(col); // update with latest data return col.hasAttribute() && col.attribute()->isEnabled(type); } /****************************************************************************** * Enable or disable the specified alarm types for a collection. * Reply = alarm types which can be enabled */ CalEvent::Types CollectionControlModel::setEnabled(const Collection& collection, CalEvent::Types types, bool enabled) { qCDebug(KALARM_LOG) << "CollectionControlModel::setEnabled:" << collection.id() << ", alarm types" << types << "->" << enabled; if (!collection.isValid() || (!enabled && !instance()->collectionIds().contains(collection.id()))) return CalEvent::EMPTY; Collection col = collection; AkonadiModel::instance()->refresh(col); // update with latest data CalEvent::Types alarmTypes = !col.hasAttribute() ? CalEvent::EMPTY : col.attribute()->enabled(); if (enabled) alarmTypes |= static_cast(types & (CalEvent::ACTIVE | CalEvent::ARCHIVED | CalEvent::TEMPLATE)); else alarmTypes &= ~types; return instance()->setEnabledStatus(collection, alarmTypes, false); } /****************************************************************************** * Change the collection's enabled status. * Add or remove the collection to/from the enabled list. * Reply = alarm types which can be enabled */ CalEvent::Types CollectionControlModel::setEnabledStatus(const Collection& collection, CalEvent::Types types, bool inserted) { qCDebug(KALARM_LOG) << "CollectionControlModel::setEnabledStatus:" << collection.id() << ", types=" << types; CalEvent::Types disallowedStdTypes{}; CalEvent::Types stdTypes{}; // Prevent the enabling of duplicate alarm types if another collection // uses the same backend storage. const QList colIds = collectionIds(); const CalEvent::Types canEnable = checkTypesToEnable(collection, colIds, types); // Update the list of enabled collections if (canEnable) { if (!colIds.contains(collection.id())) { // It's a new collection. // Prevent duplicate standard collections being created for any alarm type. stdTypes = collection.hasAttribute() ? collection.attribute()->standard() : CalEvent::EMPTY; if (stdTypes) { for (const Collection::Id& id : colIds) { Collection c(id); AkonadiModel::instance()->refresh(c); // update with latest data if (c.isValid()) { const CalEvent::Types t = stdTypes & CalEvent::types(c.contentMimeTypes()); if (t) { if (c.hasAttribute() && AkonadiModel::isCompatible(c)) { disallowedStdTypes |= c.attribute()->standard() & t; if (disallowedStdTypes == stdTypes) break; } } } } } addCollection(collection); } } else removeCollection(collection); if (disallowedStdTypes || !inserted || canEnable != types) { // Update the collection's status AkonadiModel* model = static_cast(sourceModel()); if (!model->isCollectionBeingDeleted(collection.id())) { const QModelIndex ix = model->collectionIndex(collection); if (!inserted || canEnable != types) model->setData(ix, static_cast(canEnable), AkonadiModel::EnabledTypesRole); if (disallowedStdTypes) model->setData(ix, static_cast(stdTypes & ~disallowedStdTypes), AkonadiModel::IsStandardRole); } } return canEnable; } /****************************************************************************** * Called when a collection parameter or status has changed. * If it's the enabled status, add or remove the collection to/from the enabled * list. */ void CollectionControlModel::statusChanged(const Collection& collection, AkonadiModel::Change change, const QVariant& value, bool inserted) { if (!collection.isValid()) return; switch (change) { case AkonadiModel::Enabled: { const CalEvent::Types enabled = static_cast(value.toInt()); qCDebug(KALARM_LOG) << "CollectionControlModel::statusChanged:" << collection.id() << ", enabled=" << enabled << ", inserted=" << inserted; setEnabledStatus(collection, enabled, inserted); break; } case AkonadiModel::ReadOnly: { bool readOnly = value.toBool(); qCDebug(KALARM_LOG) << "CollectionControlModel::statusChanged:" << collection.id() << ", readOnly=" << readOnly; if (readOnly) { // A read-only collection can't be the default for any alarm type const CalEvent::Types std = standardTypes(collection, false); if (std != CalEvent::EMPTY) { Collection c(collection); setStandard(c, CalEvent::Types(CalEvent::EMPTY)); QWidget* messageParent = qobject_cast(QObject::parent()); bool singleType = true; QString msg; switch (std) { case CalEvent::ACTIVE: msg = xi18nc("@info", "The calendar %1 has been made read-only. " "This was the default calendar for active alarms.", collection.name()); break; case CalEvent::ARCHIVED: msg = xi18nc("@info", "The calendar %1 has been made read-only. " "This was the default calendar for archived alarms.", collection.name()); break; case CalEvent::TEMPLATE: msg = xi18nc("@info", "The calendar %1 has been made read-only. " "This was the default calendar for alarm templates.", collection.name()); break; default: msg = xi18nc("@info", "The calendar %1 has been made read-only. " "This was the default calendar for:%2" "Please select new default calendars.", collection.name(), typeListForDisplay(std)); singleType = false; break; } if (singleType) msg = xi18nc("@info", "%1Please select a new default calendar.", msg); KAMessageBox::information(messageParent, msg); } } break; } default: break; } } /****************************************************************************** * Check which alarm types can be enabled for a specified collection. * If the collection uses the same backend storage as another collection, any * alarm types already enabled in the other collection must be disabled in this * collection. This is to avoid duplicating events between different resources, * which causes user confusion and annoyance, and causes crashes. * Parameters: * collection - must be up to date (using AkonadiModel::refresh() etc.) * collectionIds = list of collection IDs to search for duplicates. * types = alarm types to be enabled for the collection. * Reply = alarm types which can be enabled without duplicating other collections. */ CalEvent::Types CollectionControlModel::checkTypesToEnable(const Collection& collection, const QList& collectionIds, CalEvent::Types types) { types &= (CalEvent::ACTIVE | CalEvent::ARCHIVED | CalEvent::TEMPLATE); if (types) { // At least one alarm type is to be enabled const QUrl location = QUrl::fromUserInput(collection.remoteId(), QString(), QUrl::AssumeLocalFile); for (const Collection::Id& id : collectionIds) { const Collection c(id); const QUrl cLocation = QUrl::fromUserInput(c.remoteId(), QString(), QUrl::AssumeLocalFile); if (id != collection.id() && cLocation == location) { // The collection duplicates the backend storage // used by another enabled collection. // N.B. don't refresh this collection - assume no change. qCDebug(KALARM_LOG) << "CollectionControlModel::checkTypesToEnable:" << c.id() << "duplicates backend for" << collection.id(); if (c.hasAttribute()) { types &= ~c.attribute()->enabled(); if (!types) break; } } } } return types; } /****************************************************************************** * Create a bulleted list of alarm types for insertion into .... */ QString CollectionControlModel::typeListForDisplay(CalEvent::Types alarmTypes) { QString list; if (alarmTypes & CalEvent::ACTIVE) list += QLatin1String("") + i18nc("@info", "Active Alarms") + QLatin1String(""); if (alarmTypes & CalEvent::ARCHIVED) list += QLatin1String("") + i18nc("@info", "Archived Alarms") + QLatin1String(""); if (alarmTypes & CalEvent::TEMPLATE) list += QLatin1String("") + i18nc("@info", "Alarm Templates") + QLatin1String(""); if (!list.isEmpty()) list = QStringLiteral("") + list + QStringLiteral(""); return list; } /****************************************************************************** * Return whether a collection is both enabled and fully writable for a given * alarm type. * Optionally, the enabled status can be ignored. * Reply: 1 = fully enabled and writable, * 0 = enabled and writable except that backend calendar is in an old KAlarm format, * -1 = not enabled, read-only, or incompatible format. */ int CollectionControlModel::isWritableEnabled(const Akonadi::Collection& collection, CalEvent::Type type) { KACalendar::Compat format; return isWritableEnabled(collection, type, format); } int CollectionControlModel::isWritableEnabled(const Akonadi::Collection& collection, CalEvent::Type type, KACalendar::Compat& format) { int writable = AkonadiModel::isWritable(collection, format); if (writable == -1) return -1; // Check the collection's enabled status if (!instance()->collectionIds().contains(collection.id()) || !collection.hasAttribute()) return -1; if (!collection.attribute()->isEnabled(type)) return -1; return writable; } /****************************************************************************** * Return the standard collection for a specified mime type. * If the mime type is 'archived' and there is no standard collection, the only * writable archived collection is set to be the standard. */ Collection CollectionControlModel::getStandard(CalEvent::Type type) { const QString mimeType = CalEvent::mimeType(type); int defaultArch = -1; const QList colIds = instance()->collectionIds(); Collection::List cols; for (int i = 0, count = colIds.count(); i < count; ++i) { cols.append(Collection(colIds[i])); Collection& col = cols.last(); AkonadiModel::instance()->refresh(col); // update with latest data if (col.isValid() && col.contentMimeTypes().contains(mimeType)) { if (col.hasAttribute() && (col.attribute()->standard() & type) && AkonadiModel::isCompatible(col)) return col; if (type == CalEvent::ARCHIVED && ((col.rights() & writableRights) == writableRights)) defaultArch = (defaultArch == -1) ? i : -2; } } if (defaultArch >= 0) { // There is no standard collection for archived alarms, but there is // only one writable collection for the type. Set the collection to be // the standard. setStandard(cols[defaultArch], type, true); return cols[defaultArch]; } return Collection(); } /****************************************************************************** * Return whether a collection is the standard collection for a specified * mime type. */ bool CollectionControlModel::isStandard(Akonadi::Collection& collection, CalEvent::Type type) { // If it's for archived alarms, set the standard collection if necessary. if (type == CalEvent::ARCHIVED) return getStandard(type) == collection; if (!instance()->collectionIds().contains(collection.id())) return false; AkonadiModel::instance()->refresh(collection); // update with latest data if (!collection.hasAttribute() || !AkonadiModel::isCompatible(collection)) return false; return collection.attribute()->isStandard(type); } /****************************************************************************** * Return the alarm type(s) for which a collection is the standard collection. */ CalEvent::Types CollectionControlModel::standardTypes(const Collection& collection, bool useDefault) { if (!instance()->collectionIds().contains(collection.id())) return CalEvent::EMPTY; Collection col = collection; AkonadiModel::instance()->refresh(col); // update with latest data if (!AkonadiModel::isCompatible(col)) return CalEvent::EMPTY; CalEvent::Types stdTypes = col.hasAttribute() ? col.attribute()->standard() : CalEvent::EMPTY; if (useDefault) { // Also return alarm types for which this is the only collection. CalEvent::Types wantedTypes = AkonadiModel::types(collection) & ~stdTypes; const QList colIds = instance()->collectionIds(); for (int i = 0, count = colIds.count(); wantedTypes && i < count; ++i) { if (colIds[i] == col.id()) continue; Collection c(colIds[i]); AkonadiModel::instance()->refresh(c); // update with latest data if (c.isValid()) wantedTypes &= ~AkonadiModel::types(c); } stdTypes |= wantedTypes; } return stdTypes; } /****************************************************************************** * Set or clear a collection as the standard collection for a specified mime * type. If it is being set as standard, the standard status for the mime type * is cleared for all other collections. */ void CollectionControlModel::setStandard(Akonadi::Collection& collection, CalEvent::Type type, bool standard) { AkonadiModel* model = AkonadiModel::instance(); model->refresh(collection); // update with latest data if (!AkonadiModel::isCompatible(collection)) standard = false; // the collection isn't writable if (standard) { // The collection is being set as standard. // Clear the 'standard' status for all other collections. const QList colIds = instance()->collectionIds(); if (!colIds.contains(collection.id())) return; const CalEvent::Types ctypes = collection.hasAttribute() ? collection.attribute()->standard() : CalEvent::EMPTY; if (ctypes & type) return; // it's already the standard collection for this type for (int i = 0, count = colIds.count(); i < count; ++i) { CalEvent::Types types; Collection c(colIds[i]); if (colIds[i] == collection.id()) { c = collection; // update with latest data types = ctypes | type; } else { model->refresh(c); // update with latest data types = c.hasAttribute() ? c.attribute()->standard() : CalEvent::EMPTY; if (!(types & type)) continue; types &= ~type; } const QModelIndex index = model->collectionIndex(c); model->setData(index, static_cast(types), AkonadiModel::IsStandardRole); } } else { // The 'standard' status is being cleared for the collection. // The collection doesn't have to be in this model's list of collections. CalEvent::Types types = collection.hasAttribute() ? collection.attribute()->standard() : CalEvent::EMPTY; if (types & type) { types &= ~type; const QModelIndex index = model->collectionIndex(collection); model->setData(index, static_cast(types), AkonadiModel::IsStandardRole); } } } /****************************************************************************** * Set which mime types a collection is the standard collection for. * If it is being set as standard for any mime types, the standard status for * those mime types is cleared for all other collections. */ void CollectionControlModel::setStandard(Akonadi::Collection& collection, CalEvent::Types types) { AkonadiModel* model = AkonadiModel::instance(); model->refresh(collection); // update with latest data if (!AkonadiModel::isCompatible(collection)) types = CalEvent::EMPTY; // the collection isn't writable if (types) { // The collection is being set as standard for at least one mime type. // Clear the 'standard' status for all other collections. const QList colIds = instance()->collectionIds(); if (!colIds.contains(collection.id())) return; const CalEvent::Types t = collection.hasAttribute() ? collection.attribute()->standard() : CalEvent::EMPTY; if (t == types) return; // there's no change to the collection's status for (int i = 0, count = colIds.count(); i < count; ++i) { CalEvent::Types t; Collection c(colIds[i]); if (colIds[i] == collection.id()) { c = collection; // update with latest data t = types; } else { model->refresh(c); // update with latest data t = c.hasAttribute() ? c.attribute()->standard() : CalEvent::EMPTY; if (!(t & types)) continue; t &= ~types; } const QModelIndex index = model->collectionIndex(c); model->setData(index, static_cast(t), AkonadiModel::IsStandardRole); } } else { // The 'standard' status is being cleared for the collection. // The collection doesn't have to be in this model's list of collections. if (collection.hasAttribute() && collection.attribute()->standard()) { const QModelIndex index = model->collectionIndex(collection); model->setData(index, static_cast(types), AkonadiModel::IsStandardRole); } } } /****************************************************************************** * Get the collection to use for storing an alarm. * Optionally, the standard collection for the alarm type is returned. If more * than one collection is a candidate, the user is prompted. */ Collection CollectionControlModel::destination(CalEvent::Type type, QWidget* promptParent, bool noPrompt, bool* cancelled) { if (cancelled) *cancelled = false; Collection standard; if (type == CalEvent::EMPTY) return standard; standard = getStandard(type); // Archived alarms are always saved in the default resource, // else only prompt if necessary. if (type == CalEvent::ARCHIVED || noPrompt || (!mAskDestination && standard.isValid())) return standard; // Prompt for which collection to use CollectionListModel* model = new CollectionListModel(promptParent); model->setFilterWritable(true); model->setFilterEnabled(true); model->setEventTypeFilter(type); model->useCollectionColour(false); Collection col; switch (model->rowCount()) { case 0: break; case 1: col = model->collection(0); break; default: { // Use AutoQPointer to guard against crash on application exit while // the dialogue is still open. It prevents double deletion (both on // deletion of 'promptParent', and on return from this function). AutoQPointer dlg = new CollectionDialog(model, promptParent); dlg->setWindowTitle(i18nc("@title:window", "Choose Calendar")); dlg->setDefaultCollection(standard); dlg->setMimeTypeFilter(QStringList(CalEvent::mimeType(type))); if (dlg->exec()) col = dlg->selectedCollection(); if (!col.isValid() && cancelled) *cancelled = true; } } return col; } /****************************************************************************** * Return all collections which belong to a resource and which optionally * contain a specified mime type. */ Collection::List CollectionControlModel::allCollections(CalEvent::Type type) { const bool allTypes = (type == CalEvent::EMPTY); const QString mimeType = CalEvent::mimeType(type); AgentManager* agentManager = AgentManager::self(); Collection::List result; const QList colIds = instance()->collectionIds(); for (Collection::Id id : colIds) { Collection c(id); AkonadiModel::instance()->refresh(c); // update with latest data if ((allTypes || c.contentMimeTypes().contains(mimeType)) && agentManager->instance(c.resource()).isValid()) result += c; } return result; } /****************************************************************************** * Return the enabled collections which contain a specified mime type. * If 'writable' is true, only writable collections are included. */ Collection::List CollectionControlModel::enabledCollections(CalEvent::Type type, bool writable) { const QString mimeType = CalEvent::mimeType(type); const QList colIds = instance()->collectionIds(); Collection::List result; for (int i = 0, count = colIds.count(); i < count; ++i) { Collection c(colIds[i]); AkonadiModel::instance()->refresh(c); // update with latest data if (c.contentMimeTypes().contains(mimeType) && (!writable || ((c.rights() & writableRights) == writableRights))) result += c; } return result; } /****************************************************************************** * Return the collection ID for a given resource ID. */ Collection CollectionControlModel::collectionForResource(const QString& resourceId) { const QList colIds = instance()->collectionIds(); for (Collection::Id id : colIds) { Collection c(id); if (c.resource() == resourceId) return c; } return Collection(); } /****************************************************************************** * Return whether all enabled collections have been populated. */ bool CollectionControlModel::isPopulated(Collection::Id colId) { AkonadiModel* model = AkonadiModel::instance(); const QList colIds = instance()->collectionIds(); for (int i = 0, count = colIds.count(); i < count; ++i) { if ((colId == -1 || colId == colIds[i]) && !model->data(model->collectionIndex(colIds[i]), AkonadiModel::IsPopulatedRole).toBool()) { Collection c(colIds[i]); model->refresh(c); // update with latest data if (!c.hasAttribute() || c.attribute()->enabled() == CalEvent::EMPTY) return false; } } return true; } /****************************************************************************** * Wait for one or all enabled collections to be populated. * Reply = true if successful. */ bool CollectionControlModel::waitUntilPopulated(Collection::Id colId, int timeout) { qCDebug(KALARM_LOG) << "CollectionControlModel::waitUntilPopulated"; int result = 1; AkonadiModel* model = AkonadiModel::instance(); while (!model->isCollectionTreeFetched() || !isPopulated(colId)) { if (!mPopulatedCheckLoop) mPopulatedCheckLoop = new QEventLoop(this); if (timeout > 0) QTimer::singleShot(timeout * 1000, mPopulatedCheckLoop, &QEventLoop::quit); result = mPopulatedCheckLoop->exec(); } delete mPopulatedCheckLoop; mPopulatedCheckLoop = nullptr; return result; } /****************************************************************************** * Check for, and remove, any Akonadi resources which duplicate use of calendar * files/directories. */ void CollectionControlModel::removeDuplicateResources() { mAgentPaths.clear(); const AgentInstance::List agents = AgentManager::self()->instances(); for (const AgentInstance& agent : agents) { if (agent.type().mimeTypes().indexOf(matchMimeType) >= 0) { CollectionFetchJob* job = new CollectionFetchJob(Collection::root(), CollectionFetchJob::Recursive); job->fetchScope().setResource(agent.identifier()); connect(job, &CollectionFetchJob::result, instance(), &CollectionControlModel::collectionFetchResult); } } } /****************************************************************************** * Called when a CollectionFetchJob has completed. */ void CollectionControlModel::collectionFetchResult(KJob* j) { CollectionFetchJob* job = qobject_cast(j); if (j->error()) qCCritical(KALARM_LOG) << "CollectionControlModel::collectionFetchResult: CollectionFetchJob" << job->fetchScope().resource()<< "error: " << j->errorString(); else { AgentManager* agentManager = AgentManager::self(); const Collection::List collections = job->collections(); for (const Collection& c : collections) { if (c.contentMimeTypes().indexOf(matchMimeType) >= 0) { ResourceCol thisRes(job->fetchScope().resource(), c.id()); auto it = mAgentPaths.constFind(c.remoteId()); if (it != mAgentPaths.constEnd()) { // Remove the resource containing the higher numbered Collection // ID, which is likely to be the more recently created. ResourceCol prevRes = it.value(); if (thisRes.collectionId > prevRes.collectionId) { qCWarning(KALARM_LOG) << "CollectionControlModel::collectionFetchResult: Removing duplicate resource" << thisRes.resourceId; agentManager->removeInstance(agentManager->instance(thisRes.resourceId)); continue; } qCWarning(KALARM_LOG) << "CollectionControlModel::collectionFetchResult: Removing duplicate resource" << prevRes.resourceId; agentManager->removeInstance(agentManager->instance(prevRes.resourceId)); } mAgentPaths[c.remoteId()] = thisRes; } } } } /****************************************************************************** * Called when the Akonadi server has stopped. Reset the model. */ void CollectionControlModel::reset() { delete mPopulatedCheckLoop; mPopulatedCheckLoop = nullptr; // Clear the collections list. This is required because addCollection() or // setCollections() don't work if the collections which they specify are // already in the list. setCollections(Collection::List()); } /****************************************************************************** * Exit from the populated event loop when a collection has been populated. */ void CollectionControlModel::collectionPopulated() { if (mPopulatedCheckLoop) mPopulatedCheckLoop->exit(1); } /****************************************************************************** * Return the data for a given role, for a specified item. */ QVariant CollectionControlModel::data(const QModelIndex& index, int role) const { return sourceModel()->data(mapToSource(index), role); } #include "collectionmodel.moc" // vim: et sw=4: diff --git a/src/lib/packedlayout.cpp b/src/lib/packedlayout.cpp index a9135359..b90e0a53 100644 --- a/src/lib/packedlayout.cpp +++ b/src/lib/packedlayout.cpp @@ -1,172 +1,179 @@ /* * packedlayout.cpp - layout to pack items into rows * Program: kalarm * Copyright © 2007 by David Jarvie * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kalarm.h" //krazy:exclude=includes (kalarm.h must be first) #include "packedlayout.h" PackedLayout::PackedLayout(QWidget* parent, Qt::Alignment alignment) : QLayout(parent), mAlignment(alignment), mWidthCached(0) { } PackedLayout::PackedLayout(Qt::Alignment alignment) : QLayout(), mAlignment(alignment), mWidthCached(0) { } PackedLayout::~PackedLayout() { while (!mItems.isEmpty()) delete mItems.takeFirst(); } /****************************************************************************** * Inserts a button into the group. */ void PackedLayout::addItem(QLayoutItem* item) { mWidthCached = 0; mItems.append(item); } QLayoutItem* PackedLayout::itemAt(int index) const { return (index >= 0 && index < mItems.count()) ? mItems[index] : nullptr; } QLayoutItem* PackedLayout::takeAt(int index) { if (index < 0 || index >= mItems.count()) return nullptr; return mItems.takeAt(index); } int PackedLayout::heightForWidth(int w) const { if (w != mWidthCached) { mHeightCached = arrange(QRect(0, 0, w, 0), false); mWidthCached = w; } return mHeightCached; } void PackedLayout::setGeometry(const QRect& rect) { QLayout::setGeometry(rect); arrange(rect, true); } // Return the maximum size of any widget. QSize PackedLayout::minimumSize() const { QSize size; for (int i = 0, end = mItems.count(); i < end; ++i) size = size.expandedTo(mItems[i]->minimumSize()); - int m = margin() * 2; + + int marginValue = -1; + int left, top, right, bottom; + getContentsMargins(&left, &top, &right, &bottom); + if (left == top && top == right && right == bottom) { + marginValue = left; + } + int m = marginValue * 2; return QSize(size.width() + m, size.height() + m); } // Arranges widgets and returns height required. int PackedLayout::arrange(const QRect& rect, bool set) const { int x = rect.x(); int y = rect.y(); int yrow = 0; QList posn; int end = mItems.count(); QList items; for (int i = 0; i < end; ++i) { QLayoutItem* item = mItems[i]; if (item->isEmpty()) continue; QSize size = item->sizeHint(); int right = x + size.width(); if (right > rect.right() && x > rect.x()) { x = rect.x(); y = y + yrow + spacing(); right = x + size.width(); yrow = size.height(); } else yrow = qMax(yrow, size.height()); items.append(item); posn.append(QRect(QPoint(x, y), size)); x = right + spacing(); } if (set) { int count = items.count(); if (mAlignment == Qt::AlignLeft) { // Left aligned: no position adjustment needed // Set the positions of all the layout items for (int i = 0; i < count; ++i) items[i]->setGeometry(posn[i]); } else { // Set the positions of all the layout items for (int i = 0; i < count; ) { // Adjust item positions a row at a time y = posn[i].y(); int last; // after last item in this row for (last = i + 1; last < count && posn[last].y() == y; ++last) ; int n = last - i; // number of items in this row int free = rect.right() - posn[last - 1].right(); switch (mAlignment) { case Qt::AlignJustify: if (n == 1) { items[i]->setGeometry(posn[i]); ++i; } else if (n > 1) { for (int j = 0; i < last; ++j, ++i) items[i]->setGeometry(QRect(QPoint(posn[i].x() + (free * j)/(n - 1), y), posn[i].size())); } break; case Qt::AlignHCenter: free /= 2; // fall through to AlignRight Q_FALLTHROUGH(); case Qt::AlignRight: for ( ; i < last; ++i) items[i]->setGeometry(QRect(QPoint(posn[i].x() + free, y), posn[i].size())); break; default: break; } } } } return y + yrow - rect.y(); } // vim: et sw=4: diff --git a/src/lib/synchtimer.cpp b/src/lib/synchtimer.cpp index 9d720109..db9b6b9a 100644 --- a/src/lib/synchtimer.cpp +++ b/src/lib/synchtimer.cpp @@ -1,242 +1,242 @@ /* * synchtimer.cpp - timers which synchronize to time boundaries * Program: kalarm * Copyright © 2004,2005,2007-2009 by David Jarvie * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kalarm.h" #include "synchtimer.h" #include #include #include #include "kalarm_debug.h" /*============================================================================= = Class: SynchTimer = Virtual base class for application-wide timer synchronized to a time boundary. =============================================================================*/ SynchTimer::SynchTimer() { mTimer = new QTimer(this); mTimer->setSingleShot(true); } SynchTimer::~SynchTimer() { delete mTimer; mTimer = nullptr; } /****************************************************************************** * Connect to the timer. The timer is started if necessary. */ void SynchTimer::connecT(QObject* receiver, const char* member) { Connection connection(receiver, member); if (mConnections.contains(connection)) return; // the slot is already connected, so ignore request connect(mTimer, SIGNAL(timeout()), receiver, member); mConnections.append(connection); if (!mTimer->isActive()) { connect(mTimer, &QTimer::timeout, this, &SynchTimer::slotTimer); start(); } } /****************************************************************************** * Disconnect from the timer. The timer is stopped if no longer needed. */ void SynchTimer::disconnecT(QObject* receiver, const char* member) { if (mTimer) { mTimer->disconnect(receiver, member); if (member) { int i = mConnections.indexOf(Connection(receiver, member)); if (i >= 0) mConnections.removeAt(i); } else { for (int i = 0; i < mConnections.count(); ) { if (mConnections[i].receiver == receiver) mConnections.removeAt(i); else ++i; } } if (mConnections.isEmpty()) { mTimer->disconnect(); mTimer->stop(); } } } /*============================================================================= = Class: MinuteTimer = Application-wide timer synchronized to the minute boundary. =============================================================================*/ MinuteTimer* MinuteTimer::mInstance = nullptr; MinuteTimer* MinuteTimer::instance() { if (!mInstance) mInstance = new MinuteTimer; return mInstance; } /****************************************************************************** * Called when the timer triggers, or to start the timer. * Timers can under some circumstances wander off from the correct trigger time, * so rather than setting a 1 minute interval, calculate the correct next * interval each time it triggers. */ void MinuteTimer::slotTimer() { qCDebug(KALARM_LOG) << "MinuteTimer::slotTimer"; int interval = 62 - QTime::currentTime().second(); mTimer->start(interval * 1000); // execute a single shot } /*============================================================================= = Class: DailyTimer = Application-wide timer synchronized to midnight. =============================================================================*/ QList DailyTimer::mFixedTimers; DailyTimer::DailyTimer(const QTime& timeOfDay, bool fixed) : mTime(timeOfDay), mFixed(fixed) { if (fixed) mFixedTimers.append(this); } DailyTimer::~DailyTimer() { if (mFixed) mFixedTimers.removeAt(mFixedTimers.indexOf(this)); } DailyTimer* DailyTimer::fixedInstance(const QTime& timeOfDay, bool create) { for (int i = 0, end = mFixedTimers.count(); i < end; ++i) if (mFixedTimers[i]->mTime == timeOfDay) return mFixedTimers[i]; return create ? new DailyTimer(timeOfDay, true) : nullptr; } /****************************************************************************** * Disconnect from the timer signal which triggers at the given fixed time of day. * If there are no remaining connections to that timer, it is destroyed. */ void DailyTimer::disconnect(const QTime& timeOfDay, QObject* receiver, const char* member) { DailyTimer* timer = fixedInstance(timeOfDay, false); if (!timer) return; timer->disconnecT(receiver, member); if (!timer->hasConnections()) delete timer; } /****************************************************************************** * Change the time at which the variable timer triggers. */ void DailyTimer::changeTime(const QTime& newTimeOfDay, bool triggerMissed) { if (mFixed) return; if (mTimer->isActive()) { mTimer->stop(); bool triggerNow = false; if (triggerMissed) { QTime now = QTime::currentTime(); if (now >= newTimeOfDay && now < mTime) { // The trigger time is now earlier and it has already arrived today. // Trigger a timer event immediately. triggerNow = true; } } mTime = newTimeOfDay; if (triggerNow) mTimer->start(0); // trigger immediately else start(); } else mTime = newTimeOfDay; } /****************************************************************************** * Initialise the timer to trigger at the specified time. * This will either be today or tomorrow, depending on whether the trigger time * has already passed. */ void DailyTimer::start() { // TIMEZONE = local time QDateTime now = QDateTime::currentDateTime(); // Find out whether to trigger today or tomorrow. // In preference, use the last trigger date to determine this, since // that will avoid possible errors due to daylight savings time changes. bool today; if (mLastDate.isValid()) today = (mLastDate < now.date()); else today = (now.time() < mTime); QDateTime next; if (today) next = QDateTime(now.date(), mTime); else next = QDateTime(now.date().addDays(1), mTime); - uint interval = next.toTime_t() - now.toTime_t(); + const qint64 interval = next.toSecsSinceEpoch() - now.toSecsSinceEpoch(); mTimer->start(interval * 1000); // execute a single shot qCDebug(KALARM_LOG) << "DailyTimer::start: at" << mTime.hour() << ":" << mTime.minute() << ": interval =" << interval/3600 << ":" << (interval/60)%60 << ":" << interval%60; } /****************************************************************************** * Called when the timer triggers. * Set the timer to trigger again tomorrow at the specified time. * Note that if daylight savings time changes occur, this will not be 24 hours * from now. */ void DailyTimer::slotTimer() { // TIMEZONE = local time QDateTime now = QDateTime::currentDateTime(); mLastDate = now.date(); QDateTime next = QDateTime(mLastDate.addDays(1), mTime); - uint interval = next.toTime_t() - now.toTime_t(); + const qint64 interval = next.toSecsSinceEpoch() - now.toSecsSinceEpoch(); mTimer->start(interval * 1000); // execute a single shot qCDebug(KALARM_LOG) << "DailyTimer::slotTimer: at" << mTime.hour() << ":" << mTime.minute() << ": interval =" << interval/3600 << ":" << (interval/60)%60 << ":" << interval%60; } // vim: et sw=4: diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 2209092f..39913d19 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -1,1633 +1,1633 @@ /* * mainwindow.cpp - main application window * Program: kalarm * Copyright © 2001-2019 David Jarvie * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kalarm.h" #include "mainwindow.h" #include "alarmcalendar.h" #include "alarmlistdelegate.h" #include "autoqpointer.h" #include "alarmlistview.h" #include "birthdaydlg.h" #include "functions.h" #include "kalarmapp.h" #include "kamail.h" #include "messagebox.h" #include "newalarmaction.h" #include "prefdlg.h" #include "preferences.h" #include "resourceselector.h" #include "synchtimer.h" #include "templatedlg.h" #include "templatemenuaction.h" #include "templatepickdlg.h" #include "traywindow.h" #include "wakedlg.h" #include "kalarm_debug.h" #include #include #include #include #include #include #include #include #include #include using namespace Akonadi; 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 using namespace KAlarmCal; namespace { const QString UI_FILE(QStringLiteral("kalarmui.rc")); const char* WINDOW_NAME = "MainWindow"; const char* VIEW_GROUP = "View"; const char* SHOW_COLUMNS = "ShowColumns"; const char* SHOW_ARCHIVED_KEY = "ShowArchivedAlarms"; const char* SHOW_RESOURCES_KEY = "ShowResources"; QString undoText; QString undoTextStripped; QList undoShortcut; QString redoText; QString redoTextStripped; QList redoShortcut; } /*============================================================================= = Class: MainWindow =============================================================================*/ MainWindow::WindowList MainWindow::mWindowList; TemplateDlg* MainWindow::mTemplateDlg = nullptr; /****************************************************************************** * Construct an instance. * To avoid resize() events occurring while still opening the calendar (and * resultant crashes), the calendar is opened before constructing the instance. */ MainWindow* MainWindow::create(bool restored) { theApp()->checkCalendar(); // ensure calendar is open return new MainWindow(restored); } MainWindow::MainWindow(bool restored) : MainWindowBase(nullptr, Qt::WindowContextHelpButtonHint), mResourcesWidth(-1), mHiddenTrayParent(false), mShown(false), mResizing(false) { qCDebug(KALARM_LOG) << "MainWindow:"; setAttribute(Qt::WA_DeleteOnClose); setWindowModality(Qt::WindowModal); setObjectName(QStringLiteral("MainWin")); // used by LikeBack setPlainCaption(KAboutData::applicationData().displayName()); KConfigGroup config(KSharedConfig::openConfig(), VIEW_GROUP); mShowResources = config.readEntry(SHOW_RESOURCES_KEY, false); mShowArchived = config.readEntry(SHOW_ARCHIVED_KEY, false); const QList showColumns = config.readEntry(SHOW_COLUMNS, QList()); if (!restored) { KConfigGroup wconfig(KSharedConfig::openConfig(), WINDOW_NAME); mResourcesWidth = wconfig.readEntry(QStringLiteral("Splitter %1").arg(QApplication::desktop()->width()), (int)0); } setAcceptDrops(true); // allow drag-and-drop onto this window mSplitter = new QSplitter(Qt::Horizontal, this); mSplitter->setChildrenCollapsible(false); mSplitter->installEventFilter(this); setCentralWidget(mSplitter); // Create the calendar resource selector widget Akonadi::ControlGui::widgetNeedsAkonadi(this); mResourceSelector = new ResourceSelector(mSplitter); mSplitter->setStretchFactor(0, 0); // don't resize resource selector when window is resized mSplitter->setStretchFactor(1, 1); // Create the alarm list widget mListFilterModel = new AlarmListModel(this); mListFilterModel->setEventTypeFilter(mShowArchived ? CalEvent::ACTIVE | CalEvent::ARCHIVED : CalEvent::ACTIVE); mListView = new AlarmListView(WINDOW_NAME, mSplitter); mListView->setModel(mListFilterModel); mListView->setColumnsVisible(showColumns); mListView->setItemDelegate(new AlarmListDelegate(mListView)); connect(mListView->selectionModel(), &QItemSelectionModel::selectionChanged, this, &MainWindow::slotSelection); connect(mListView, &AlarmListView::contextMenuRequested, this, &MainWindow::slotContextMenuRequested); connect(mListView, &AlarmListView::columnsVisibleChanged, this, &MainWindow::slotAlarmListColumnsChanged); connect(AkonadiModel::instance(), &AkonadiModel::collectionStatusChanged, this, &MainWindow::slotCalendarStatusChanged); connect(mResourceSelector, &ResourceSelector::resized, this, &MainWindow::resourcesResized); mListView->installEventFilter(this); initActions(); setAutoSaveSettings(QLatin1String(WINDOW_NAME), true); // save toolbars, window sizes etc. mWindowList.append(this); if (mWindowList.count() == 1) { // It's the first main window if (theApp()->wantShowInSystemTray()) theApp()->displayTrayIcon(true, this); // create system tray icon for run-in-system-tray mode else if (theApp()->trayWindow()) theApp()->trayWindow()->setAssocMainWindow(this); // associate this window with the system tray icon } slotCalendarStatusChanged(); // initialise action states now that window is registered } MainWindow::~MainWindow() { qCDebug(KALARM_LOG) << "~MainWindow"; bool trayParent = isTrayParent(); // must call before removing from window list mWindowList.removeAt(mWindowList.indexOf(this)); // Prevent view updates during window destruction delete mResourceSelector; mResourceSelector = nullptr; delete mListView; mListView = nullptr; if (theApp()->trayWindow()) { if (trayParent) delete theApp()->trayWindow(); else theApp()->trayWindow()->removeWindow(this); } KSharedConfig::openConfig()->sync(); // save any new window size to disc theApp()->quitIf(); } /****************************************************************************** * Called when the QApplication::saveStateRequest() signal has been emitted. * Save settings to the session managed config file, for restoration * when the program is restored. */ void MainWindow::saveProperties(KConfigGroup& config) { config.writeEntry("HiddenTrayParent", isTrayParent() && isHidden()); config.writeEntry("ShowArchived", mShowArchived); config.writeEntry("ShowColumns", mListView->columnsVisible()); config.writeEntry("ResourcesWidth", mResourceSelector->isHidden() ? 0 : mResourceSelector->width()); } /****************************************************************************** * Read settings from the session managed config file. * This function is automatically called whenever the app is being * restored. Read in whatever was saved in saveProperties(). */ void MainWindow::readProperties(const KConfigGroup& config) { mHiddenTrayParent = config.readEntry("HiddenTrayParent", true); mShowArchived = config.readEntry("ShowArchived", false); mResourcesWidth = config.readEntry("ResourcesWidth", (int)0); mShowResources = (mResourcesWidth > 0); mListView->setColumnsVisible(config.readEntry("ShowColumns", QList())); } /****************************************************************************** * Get the main main window, i.e. the parent of the system tray icon, or if * none, the first main window to be created. Visible windows take precedence * over hidden ones. */ MainWindow* MainWindow::mainMainWindow() { MainWindow* tray = theApp()->trayWindow() ? theApp()->trayWindow()->assocMainWindow() : nullptr; if (tray && tray->isVisible()) return tray; for (int i = 0, end = mWindowList.count(); i < end; ++i) if (mWindowList[i]->isVisible()) return mWindowList[i]; if (tray) return tray; if (mWindowList.isEmpty()) return nullptr; return mWindowList[0]; } /****************************************************************************** * Check whether this main window is effectively the parent of the system tray icon. */ bool MainWindow::isTrayParent() const { TrayWindow* tray = theApp()->trayWindow(); if (!tray || !QSystemTrayIcon::isSystemTrayAvailable()) return false; if (tray->assocMainWindow() == this) return true; return mWindowList.count() == 1; } /****************************************************************************** * Close all main windows. */ void MainWindow::closeAll() { while (!mWindowList.isEmpty()) delete mWindowList[0]; // N.B. the destructor removes the window from the list } /****************************************************************************** * Intercept events for the splitter widget. */ bool MainWindow::eventFilter(QObject* obj, QEvent* e) { if (obj == mSplitter) { switch (e->type()) { case QEvent::Resize: // Don't change resources size while WINDOW is being resized. // Resize event always occurs before Paint. mResizing = true; break; case QEvent::Paint: // Allow resources to be resized again mResizing = false; break; default: break; } } else if (obj == mListView) { switch (e->type()) { case QEvent::KeyPress: { QKeyEvent* ke = static_cast(e); if (ke->key() == Qt::Key_Delete && ke->modifiers() == Qt::ShiftModifier) { // Prevent Shift-Delete being processed by EventListDelegate mActionDeleteForce->trigger(); return true; } break; } default: break; } } return false; } /****************************************************************************** * Called when the window's size has changed (before it is painted). * Sets the last column in the list view to extend at least to the right hand * edge of the list view. * Records the new size in the config file. */ void MainWindow::resizeEvent(QResizeEvent* re) { // Save the window's new size only if it's the first main window MainWindowBase::resizeEvent(re); if (mResourcesWidth > 0) { QList widths; widths.append(mResourcesWidth); widths.append(width() - mResourcesWidth - mSplitter->handleWidth()); mSplitter->setSizes(widths); } } /****************************************************************************** * Called when the resources panel has been resized. * Records the new size in the config file. */ void MainWindow::resourcesResized() { if (!mShown || mResizing) return; QList widths = mSplitter->sizes(); if (widths.count() > 1) { mResourcesWidth = widths[0]; // Width is reported as non-zero when resource selector is // actually invisible, so note a zero width in these circumstances. if (mResourcesWidth <= 5) mResourcesWidth = 0; else if (mainMainWindow() == this) { KConfigGroup config(KSharedConfig::openConfig(), WINDOW_NAME); config.writeEntry(QStringLiteral("Splitter %1").arg(QApplication::desktop()->width()), mResourcesWidth); config.sync(); } } } /****************************************************************************** * Called when the window is first displayed. * Sets the last column in the list view to extend at least to the right hand * edge of the list view. */ void MainWindow::showEvent(QShowEvent* se) { if (mResourcesWidth > 0) { QList widths; widths.append(mResourcesWidth); widths.append(width() - mResourcesWidth - mSplitter->handleWidth()); mSplitter->setSizes(widths); } MainWindowBase::showEvent(se); mShown = true; } /****************************************************************************** * Display the window. */ void MainWindow::show() { MainWindowBase::show(); if (mMenuError) { // Show error message now that the main window has been displayed. // Waiting until now lets the user easily associate the message with // the main window which is faulty. KAMessageBox::error(this, xi18nc("@info", "Failure to create menus (perhaps %1 missing or corrupted)", UI_FILE)); mMenuError = false; } } /****************************************************************************** * Called after the window is hidden. */ void MainWindow::hideEvent(QHideEvent* he) { MainWindowBase::hideEvent(he); } /****************************************************************************** * Initialise the menu, toolbar and main window actions. */ void MainWindow::initActions() { KActionCollection* actions = actionCollection(); mActionTemplates = new QAction(i18nc("@action", "&Templates..."), this); actions->addAction(QStringLiteral("templates"), mActionTemplates); connect(mActionTemplates, &QAction::triggered, this, &MainWindow::slotTemplates); mActionNew = new NewAlarmAction(false, i18nc("@action", "&New"), this, actions); actions->addAction(QStringLiteral("new"), mActionNew); QAction* action = mActionNew->displayAlarmAction(QStringLiteral("newDisplay")); connect(action, &QAction::triggered, this, &MainWindow::slotNewDisplay); action = mActionNew->commandAlarmAction(QStringLiteral("newCommand")); connect(action, &QAction::triggered, this, &MainWindow::slotNewCommand); action = mActionNew->emailAlarmAction(QStringLiteral("newEmail")); connect(action, &QAction::triggered, this, &MainWindow::slotNewEmail); action = mActionNew->audioAlarmAction(QStringLiteral("newAudio")); connect(action, &QAction::triggered, this, &MainWindow::slotNewAudio); TemplateMenuAction* templateMenuAction = mActionNew->fromTemplateAlarmAction(QStringLiteral("newFromTemplate")); connect(templateMenuAction, &TemplateMenuAction::selected, this, &MainWindow::slotNewFromTemplate); mActionCreateTemplate = new QAction(i18nc("@action", "Create Tem&plate..."), this); actions->addAction(QStringLiteral("createTemplate"), mActionCreateTemplate); connect(mActionCreateTemplate, &QAction::triggered, this, &MainWindow::slotNewTemplate); mActionCopy = new QAction(QIcon::fromTheme(QStringLiteral("edit-copy")), i18nc("@action", "&Copy..."), this); actions->addAction(QStringLiteral("copy"), mActionCopy); actions->setDefaultShortcut(mActionCopy, QKeySequence(Qt::SHIFT + Qt::Key_Insert)); connect(mActionCopy, &QAction::triggered, this, &MainWindow::slotCopy); mActionModify = new QAction(QIcon::fromTheme(QStringLiteral("document-properties")), i18nc("@action", "&Edit..."), this); actions->addAction(QStringLiteral("modify"), mActionModify); actions->setDefaultShortcut(mActionModify, QKeySequence(Qt::CTRL + Qt::Key_E)); connect(mActionModify, &QAction::triggered, this, &MainWindow::slotModify); mActionDelete = new QAction(QIcon::fromTheme(QStringLiteral("edit-delete")), i18nc("@action", "&Delete"), this); actions->addAction(QStringLiteral("delete"), mActionDelete); actions->setDefaultShortcut(mActionDelete, QKeySequence::Delete); connect(mActionDelete, &QAction::triggered, this, &MainWindow::slotDeleteIf); // Set up Shift-Delete as a shortcut to delete without confirmation mActionDeleteForce = new QAction(i18nc("@action", "Delete Without Confirmation"), this); actions->addAction(QStringLiteral("delete-force"), mActionDeleteForce); actions->setDefaultShortcut(mActionDeleteForce, QKeySequence::Delete + Qt::SHIFT); connect(mActionDeleteForce, &QAction::triggered, this, &MainWindow::slotDeleteForce); mActionReactivate = new QAction(i18nc("@action", "Reac&tivate"), this); actions->addAction(QStringLiteral("undelete"), mActionReactivate); actions->setDefaultShortcut(mActionReactivate, QKeySequence(Qt::CTRL + Qt::Key_R)); connect(mActionReactivate, &QAction::triggered, this, &MainWindow::slotReactivate); mActionEnable = new QAction(this); actions->addAction(QStringLiteral("disable"), mActionEnable); actions->setDefaultShortcut(mActionEnable, QKeySequence(Qt::CTRL + Qt::Key_B)); connect(mActionEnable, &QAction::triggered, this, &MainWindow::slotEnable); action = new QAction(i18nc("@action", "Wake From Suspend..."), this); actions->addAction(QStringLiteral("wakeSuspend"), action); connect(action, &QAction::triggered, this, &MainWindow::slotWakeFromSuspend); action = KAlarm::createStopPlayAction(this); actions->addAction(QStringLiteral("stopAudio"), action); KGlobalAccel::setGlobalShortcut(action, QList()); // allow user to set a global shortcut mActionShowArchived = new KToggleAction(i18nc("@action", "Show Archi&ved Alarms"), this); actions->addAction(QStringLiteral("showArchivedAlarms"), mActionShowArchived); actions->setDefaultShortcut(mActionShowArchived, QKeySequence(Qt::CTRL + Qt::Key_P)); connect(mActionShowArchived, &KToggleAction::triggered, this, &MainWindow::slotShowArchived); mActionToggleTrayIcon = new KToggleAction(i18nc("@action", "Show in System &Tray"), this); actions->addAction(QStringLiteral("showInSystemTray"), mActionToggleTrayIcon); connect(mActionToggleTrayIcon, &KToggleAction::triggered, this, &MainWindow::slotToggleTrayIcon); mActionToggleResourceSel = new KToggleAction(QIcon::fromTheme(QStringLiteral("view-choose")), i18nc("@action", "Show &Calendars"), this); actions->addAction(QStringLiteral("showResources"), mActionToggleResourceSel); connect(mActionToggleResourceSel, &KToggleAction::triggered, this, &MainWindow::slotToggleResourceSelector); mActionSpreadWindows = KAlarm::createSpreadWindowsAction(this); actions->addAction(QStringLiteral("spread"), mActionSpreadWindows); KGlobalAccel::setGlobalShortcut(mActionSpreadWindows, QList()); // allow user to set a global shortcut mActionImportAlarms = new QAction(i18nc("@action", "Import &Alarms..."), this); actions->addAction(QStringLiteral("importAlarms"), mActionImportAlarms); connect(mActionImportAlarms, &QAction::triggered, this, &MainWindow::slotImportAlarms); mActionImportBirthdays = new QAction(i18nc("@action", "Import &Birthdays..."), this); actions->addAction(QStringLiteral("importBirthdays"), mActionImportBirthdays); connect(mActionImportBirthdays, &QAction::triggered, this, &MainWindow::slotBirthdays); mActionExportAlarms = new QAction(i18nc("@action", "E&xport Selected Alarms..."), this); actions->addAction(QStringLiteral("exportAlarms"), mActionExportAlarms); connect(mActionExportAlarms, &QAction::triggered, this, &MainWindow::slotExportAlarms); mActionExport = new QAction(i18nc("@action", "E&xport..."), this); actions->addAction(QStringLiteral("export"), mActionExport); connect(mActionExport, &QAction::triggered, this, &MainWindow::slotExportAlarms); action = new QAction(QIcon::fromTheme(QStringLiteral("view-refresh")), i18nc("@action", "&Refresh Alarms"), this); actions->addAction(QStringLiteral("refreshAlarms"), action); connect(action, &QAction::triggered, this, &MainWindow::slotRefreshAlarms); KToggleAction* toggleAction = KAlarm::createAlarmEnableAction(this); actions->addAction(QStringLiteral("alarmsEnable"), toggleAction); if (undoText.isNull()) { // Get standard texts, etc., for Undo and Redo actions QAction* act = KStandardAction::undo(this, nullptr, actions); undoShortcut = act->shortcuts(); undoText = act->text(); undoTextStripped = KLocalizedString::removeAcceleratorMarker(undoText); delete act; act = KStandardAction::redo(this, nullptr, actions); redoShortcut = act->shortcuts(); redoText = act->text(); redoTextStripped = KLocalizedString::removeAcceleratorMarker(redoText); delete act; } mActionUndo = new KToolBarPopupAction(QIcon::fromTheme(QStringLiteral("edit-undo")), undoText, this); actions->addAction(QStringLiteral("edit_undo"), mActionUndo); actions->setDefaultShortcuts(mActionUndo, undoShortcut); connect(mActionUndo, &KToolBarPopupAction::triggered, this, &MainWindow::slotUndo); mActionRedo = new KToolBarPopupAction(QIcon::fromTheme(QStringLiteral("edit-redo")), redoText, this); actions->addAction(QStringLiteral("edit_redo"), mActionRedo); actions->setDefaultShortcuts(mActionRedo, redoShortcut); connect(mActionRedo, &KToolBarPopupAction::triggered, this, &MainWindow::slotRedo); KStandardAction::find(mListView, SLOT(slotFind()), actions); mActionFindNext = KStandardAction::findNext(mListView, SLOT(slotFindNext()), actions); mActionFindPrev = KStandardAction::findPrev(mListView, SLOT(slotFindPrev()), actions); KStandardAction::selectAll(mListView, SLOT(selectAll()), actions); KStandardAction::deselect(mListView, SLOT(clearSelection()), actions); // Quit only once the event loop is called; otherwise, the parent window will // be deleted while still processing the action, resulting in a crash. QAction* act = KStandardAction::quit(nullptr, nullptr, actions); connect(act, &QAction::triggered, this, &MainWindow::slotQuit, Qt::QueuedConnection); KStandardAction::keyBindings(this, SLOT(slotConfigureKeys()), actions); KStandardAction::configureToolbars(this, SLOT(slotConfigureToolbar()), actions); KStandardAction::preferences(this, SLOT(slotPreferences()), actions); mResourceSelector->initActions(actions); setStandardToolBarMenuEnabled(true); createGUI(UI_FILE); // Load menu and toolbar settings applyMainWindowSettings(KSharedConfig::openConfig()->group(WINDOW_NAME)); mContextMenu = static_cast(factory()->container(QStringLiteral("listContext"), this)); mActionsMenu = static_cast(factory()->container(QStringLiteral("actions"), this)); QMenu* resourceMenu = static_cast(factory()->container(QStringLiteral("resourceContext"), this)); mResourceSelector->setContextMenu(resourceMenu); mMenuError = (!mContextMenu || !mActionsMenu || !resourceMenu); connect(mActionUndo->menu(), &QMenu::aboutToShow, this, &MainWindow::slotInitUndoMenu); connect(mActionUndo->menu(), &QMenu::triggered, this, &MainWindow::slotUndoItem); connect(mActionRedo->menu(), &QMenu::aboutToShow, this, &MainWindow::slotInitRedoMenu); connect(mActionRedo->menu(), &QMenu::triggered, this, &MainWindow::slotRedoItem); connect(Undo::instance(), &Undo::changed, this, &MainWindow::slotUndoStatus); connect(mListView, &AlarmListView::findActive, this, &MainWindow::slotFindActive); Preferences::connect(SIGNAL(archivedKeepDaysChanged(int)), this, SLOT(updateKeepArchived(int))); Preferences::connect(SIGNAL(showInSystemTrayChanged(bool)), this, SLOT(updateTrayIconAction())); connect(theApp(), &KAlarmApp::trayIconToggled, this, &MainWindow::updateTrayIconAction); // Set menu item states setEnableText(true); mActionShowArchived->setChecked(mShowArchived); if (!Preferences::archivedKeepDays()) mActionShowArchived->setEnabled(false); mActionToggleResourceSel->setChecked(mShowResources); slotToggleResourceSelector(); updateTrayIconAction(); // set the correct text for this action mActionUndo->setEnabled(Undo::haveUndo()); mActionRedo->setEnabled(Undo::haveRedo()); mActionFindNext->setEnabled(false); mActionFindPrev->setEnabled(false); mActionCopy->setEnabled(false); mActionModify->setEnabled(false); mActionDelete->setEnabled(false); mActionReactivate->setEnabled(false); mActionEnable->setEnabled(false); mActionCreateTemplate->setEnabled(false); mActionExport->setEnabled(false); Undo::emitChanged(); // set the Undo/Redo menu texts // Daemon::monitoringAlarms(); } /****************************************************************************** * Enable or disable the Templates menu item in every main window instance. */ void MainWindow::enableTemplateMenuItem(bool enable) { for (int i = 0, end = mWindowList.count(); i < end; ++i) mWindowList[i]->mActionTemplates->setEnabled(enable); } /****************************************************************************** * Refresh the alarm list in every main window instance. */ void MainWindow::refresh() { qCDebug(KALARM_LOG) << "MainWindow::refresh"; AkonadiModel::instance()->reload(); } /****************************************************************************** * Called when the keep archived alarm setting changes in the user preferences. * Enable/disable Show archived alarms option. */ void MainWindow::updateKeepArchived(int days) { qCDebug(KALARM_LOG) << "MainWindow::updateKeepArchived:" << (bool)days; if (mShowArchived && !days) slotShowArchived(); // toggle Show Archived option setting mActionShowArchived->setEnabled(days); } /****************************************************************************** * Select an alarm in the displayed list. */ void MainWindow::selectEvent(Akonadi::Item::Id eventId) { mListView->clearSelection(); QModelIndex index = mListFilterModel->eventIndex(eventId); if (index.isValid()) { mListView->select(index); mListView->scrollTo(index); } } /****************************************************************************** * Return the single selected alarm in the displayed list. */ KAEvent MainWindow::selectedEvent() const { return mListView->selectedEvent(); } /****************************************************************************** * Deselect all alarms in the displayed list. */ void MainWindow::clearSelection() { mListView->clearSelection(); } /****************************************************************************** * Called when the New button is clicked to edit a new alarm to add to the list. */ void MainWindow::slotNew(EditAlarmDlg::Type type) { KAlarm::editNewAlarm(type, mListView); } /****************************************************************************** * Called when a template is selected from the New From Template popup menu. * Executes a New Alarm dialog, preset from the selected template. */ void MainWindow::slotNewFromTemplate(const KAEvent* tmplate) { KAlarm::editNewAlarm(tmplate, mListView); } /****************************************************************************** * Called when the New Template button is clicked to create a new template * based on the currently selected alarm. */ void MainWindow::slotNewTemplate() { KAEvent event = mListView->selectedEvent(); if (event.isValid()) KAlarm::editNewTemplate(&event, this); } /****************************************************************************** * Called when the Copy button is clicked to edit a copy of an existing alarm, * to add to the list. */ void MainWindow::slotCopy() { KAEvent event = mListView->selectedEvent(); if (event.isValid()) KAlarm::editNewAlarm(&event, this); } /****************************************************************************** * Called when the Modify button is clicked to edit the currently highlighted * alarm in the list. */ void MainWindow::slotModify() { KAEvent event = mListView->selectedEvent(); if (event.isValid()) KAlarm::editAlarm(&event, this); // edit alarm (view-only mode if archived or read-only) } /****************************************************************************** * Called when the Delete button is clicked to delete the currently highlighted * alarms in the list. */ void MainWindow::slotDelete(bool force) { QVector events = mListView->selectedEvents(); if (!force && Preferences::confirmAlarmDeletion()) { int n = events.count(); if (KAMessageBox::warningContinueCancel(this, i18ncp("@info", "Do you really want to delete the selected alarm?", "Do you really want to delete the %1 selected alarms?", n), i18ncp("@title:window", "Delete Alarm", "Delete Alarms", n), KGuiItem(i18nc("@action:button", "&Delete"), QStringLiteral("edit-delete")), KStandardGuiItem::cancel(), Preferences::CONFIRM_ALARM_DELETION) != KMessageBox::Continue) return; } // Remove any events which have just triggered, from the list to delete. Undo::EventList undos; AlarmCalendar* resources = AlarmCalendar::resources(); for (int i = 0; i < events.count(); ) { Akonadi::Collection c = resources->collectionForEvent(events[i].itemId()); if (!c.isValid()) events.remove(i); else undos.append(events[i++], c); } if (events.isEmpty()) qCDebug(KALARM_LOG) << "MainWindow::slotDelete: No alarms left to delete"; else { // Delete the events from the calendar and displays KAlarm::deleteEvents(events, true, this); Undo::saveDeletes(undos); } } /****************************************************************************** * Called when the Reactivate button is clicked to reinstate the currently * highlighted archived alarms in the list. */ void MainWindow::slotReactivate() { QVector events = mListView->selectedEvents(); mListView->clearSelection(); // Add the alarms to the displayed lists and to the calendar file Undo::EventList undos; QVector ineligibleIDs; KAlarm::reactivateEvents(events, ineligibleIDs, nullptr, this); // Create the undo list, excluding ineligible events AlarmCalendar* resources = AlarmCalendar::resources(); for (int i = 0, end = events.count(); i < end; ++i) { if (!ineligibleIDs.contains(EventId(events[i]))) { undos.append(events[i], resources->collectionForEvent(events[i].itemId())); } } Undo::saveReactivates(undos); } /****************************************************************************** * Called when the Enable/Disable button is clicked to enable or disable the * currently highlighted alarms in the list. */ void MainWindow::slotEnable() { bool enable = mActionEnableEnable; // save since changed in response to KAlarm::enableEvent() QVector events = mListView->selectedEvents(); QVector eventCopies; for (int i = 0, end = events.count(); i < end; ++i) eventCopies += events[i]; KAlarm::enableEvents(eventCopies, enable, this); slotSelection(); // update Enable/Disable action text } /****************************************************************************** * Called when the columns visible in the alarm list view have changed. */ void MainWindow::slotAlarmListColumnsChanged() { KConfigGroup config(KSharedConfig::openConfig(), VIEW_GROUP); config.writeEntry(SHOW_COLUMNS, mListView->columnsVisible()); config.sync(); } /****************************************************************************** * Called when the Show Archived Alarms menu item is selected or deselected. */ void MainWindow::slotShowArchived() { mShowArchived = !mShowArchived; mActionShowArchived->setChecked(mShowArchived); mActionShowArchived->setToolTip(mShowArchived ? i18nc("@info:tooltip", "Hide Archived Alarms") : i18nc("@info:tooltip", "Show Archived Alarms")); mListFilterModel->setEventTypeFilter(mShowArchived ? CalEvent::ACTIVE | CalEvent::ARCHIVED : CalEvent::ACTIVE); mListView->reset(); KConfigGroup config(KSharedConfig::openConfig(), VIEW_GROUP); config.writeEntry(SHOW_ARCHIVED_KEY, mShowArchived); config.sync(); } /****************************************************************************** * Called when the Spread Windows global shortcut is selected, to spread alarm * windows so that they are all visible. */ void MainWindow::slotSpreadWindowsShortcut() { mActionSpreadWindows->trigger(); } /****************************************************************************** * Called when the Wake From Suspend menu option is selected. */ void MainWindow::slotWakeFromSuspend() { (WakeFromSuspendDlg::create(this))->show(); } /****************************************************************************** * Called when the Import Alarms menu item is selected, to merge alarms from an * external calendar into the current calendars. */ void MainWindow::slotImportAlarms() { AlarmCalendar::importAlarms(this); } /****************************************************************************** * Called when the Export Alarms menu item is selected, to export the selected * alarms to an external calendar. */ void MainWindow::slotExportAlarms() { QVector events = mListView->selectedEvents(); if (!events.isEmpty()) { KAEvent::List evts = KAEvent::ptrList(events); AlarmCalendar::exportAlarms(evts, this); } } /****************************************************************************** * Called when the Import Birthdays menu item is selected, to display birthdays * from the address book for selection as alarms. */ void MainWindow::slotBirthdays() { // Use AutoQPointer to guard against crash on application exit while // the dialogue is still open. It prevents double deletion (both on // deletion of MainWindow, and on return from this function). AutoQPointer dlg = new BirthdayDlg(this); if (dlg->exec() == QDialog::Accepted) { QVector events = dlg->events(); if (!events.isEmpty()) { mListView->clearSelection(); // Add alarm to the displayed lists and to the calendar file KAlarm::UpdateResult status = KAlarm::addEvents(events, dlg, true, true); Undo::EventList undos; AlarmCalendar* resources = AlarmCalendar::resources(); for (int i = 0, end = events.count(); i < end; ++i) { Akonadi::Collection c = resources->collectionForEvent(events[i].itemId()); undos.append(events[i], c); } Undo::saveAdds(undos, i18nc("@info", "Import birthdays")); if (status != KAlarm::UPDATE_FAILED) KAlarm::outputAlarmWarnings(dlg); } } } /****************************************************************************** * Called when the Templates menu item is selected, to display the alarm * template editing dialog. */ void MainWindow::slotTemplates() { if (!mTemplateDlg) { mTemplateDlg = TemplateDlg::create(this); enableTemplateMenuItem(false); // disable menu item in all windows connect(mTemplateDlg, &QDialog::finished, this, &MainWindow::slotTemplatesEnd); mTemplateDlg->show(); } } /****************************************************************************** * Called when the alarm template editing dialog has exited. */ void MainWindow::slotTemplatesEnd() { if (mTemplateDlg) { mTemplateDlg->deleteLater(); // this deletes the dialog once it is safe to do so mTemplateDlg = nullptr; enableTemplateMenuItem(true); // re-enable menu item in all windows } } /****************************************************************************** * Called when the Display System Tray Icon menu item is selected. */ void MainWindow::slotToggleTrayIcon() { theApp()->displayTrayIcon(!theApp()->trayIconDisplayed(), this); } /****************************************************************************** * Called when the Show Resource Selector menu item is selected. */ void MainWindow::slotToggleResourceSelector() { mShowResources = mActionToggleResourceSel->isChecked(); if (mShowResources) { if (mResourcesWidth <= 0) { mResourcesWidth = mResourceSelector->sizeHint().width(); mResourceSelector->resize(mResourcesWidth, mResourceSelector->height()); QList widths = mSplitter->sizes(); if (widths.count() == 1) { int listwidth = widths[0] - mSplitter->handleWidth() - mResourcesWidth; mListView->resize(listwidth, mListView->height()); widths.append(listwidth); widths[0] = mResourcesWidth; } mSplitter->setSizes(widths); } mResourceSelector->show(); } else mResourceSelector->hide(); KConfigGroup config(KSharedConfig::openConfig(), VIEW_GROUP); config.writeEntry(SHOW_RESOURCES_KEY, mShowResources); config.sync(); } /****************************************************************************** * Called when an error occurs in the resource calendar, to display a message. */ void MainWindow::showErrorMessage(const QString& msg) { KAMessageBox::error(this, msg); } /****************************************************************************** * Called when the system tray icon is created or destroyed. * Set the system tray icon menu text according to whether or not the system * tray icon is currently visible. */ void MainWindow::updateTrayIconAction() { mActionToggleTrayIcon->setEnabled(QSystemTrayIcon::isSystemTrayAvailable()); mActionToggleTrayIcon->setChecked(theApp()->trayIconDisplayed()); } /****************************************************************************** * Called when the active status of Find changes. */ void MainWindow::slotFindActive(bool active) { mActionFindNext->setEnabled(active); mActionFindPrev->setEnabled(active); } /****************************************************************************** * Called when the Undo action is selected. */ void MainWindow::slotUndo() { Undo::undo(this, KLocalizedString::removeAcceleratorMarker(mActionUndo->text())); } /****************************************************************************** * Called when the Redo action is selected. */ void MainWindow::slotRedo() { Undo::redo(this, KLocalizedString::removeAcceleratorMarker(mActionRedo->text())); } /****************************************************************************** * Called when an Undo item is selected. */ void MainWindow::slotUndoItem(QAction* action) { int id = mUndoMenuIds[action]; Undo::undo(id, this, Undo::actionText(Undo::UNDO, id)); } /****************************************************************************** * Called when a Redo item is selected. */ void MainWindow::slotRedoItem(QAction* action) { int id = mUndoMenuIds[action]; Undo::redo(id, this, Undo::actionText(Undo::REDO, id)); } /****************************************************************************** * Called when the Undo menu is about to show. * Populates the menu. */ void MainWindow::slotInitUndoMenu() { initUndoMenu(mActionUndo->menu(), Undo::UNDO); } /****************************************************************************** * Called when the Redo menu is about to show. * Populates the menu. */ void MainWindow::slotInitRedoMenu() { initUndoMenu(mActionRedo->menu(), Undo::REDO); } /****************************************************************************** * Populate the undo or redo menu. */ void MainWindow::initUndoMenu(QMenu* menu, Undo::Type type) { menu->clear(); mUndoMenuIds.clear(); const QString& action = (type == Undo::UNDO) ? undoTextStripped : redoTextStripped; QList ids = Undo::ids(type); for (int i = 0, end = ids.count(); i < end; ++i) { int id = ids[i]; QString actText = Undo::actionText(type, id); QString descrip = Undo::description(type, id); QString text = descrip.isEmpty() ? i18nc("@action Undo/Redo [action]", "%1 %2", action, actText) : i18nc("@action Undo [action]: message", "%1 %2: %3", action, actText, descrip); QAction* act = menu->addAction(text); mUndoMenuIds[act] = id; } } /****************************************************************************** * Called when the status of the Undo or Redo list changes. * Change the Undo or Redo text to include the action which would be undone/redone. */ void MainWindow::slotUndoStatus(const QString& undo, const QString& redo) { if (undo.isNull()) { mActionUndo->setEnabled(false); mActionUndo->setText(undoText); } else { mActionUndo->setEnabled(true); mActionUndo->setText(QStringLiteral("%1 %2").arg(undoText, undo)); } if (redo.isNull()) { mActionRedo->setEnabled(false); mActionRedo->setText(redoText); } else { mActionRedo->setEnabled(true); mActionRedo->setText(QStringLiteral("%1 %2").arg(redoText, redo)); } } /****************************************************************************** * Called when the Refresh Alarms menu item is selected. */ void MainWindow::slotRefreshAlarms() { KAlarm::refreshAlarms(); } /****************************************************************************** * Called when the "Configure KAlarm" menu item is selected. */ void MainWindow::slotPreferences() { KAlarmPrefDlg::display(); } /****************************************************************************** * Called when the Configure Keys menu item is selected. */ void MainWindow::slotConfigureKeys() { KShortcutsDialog::configure(actionCollection(), KShortcutsEditor::LetterShortcutsAllowed, this); } /****************************************************************************** * Called when the Configure Toolbars menu item is selected. */ void MainWindow::slotConfigureToolbar() { KConfigGroup grp(KSharedConfig::openConfig()->group(WINDOW_NAME)); saveMainWindowSettings(grp); KEditToolBar dlg(factory()); connect(&dlg, &KEditToolBar::newToolBarConfig, this, &MainWindow::slotNewToolbarConfig); dlg.exec(); } /****************************************************************************** * Called when OK or Apply is clicked in the Configure Toolbars dialog, to save * the new configuration. */ void MainWindow::slotNewToolbarConfig() { createGUI(UI_FILE); applyMainWindowSettings(KSharedConfig::openConfig()->group(WINDOW_NAME)); } /****************************************************************************** * Called when the Quit menu item is selected. * Note that this must be called by the event loop, not directly from the menu * item, since otherwise the window will be deleted while still processing the * menu, resulting in a crash. */ void MainWindow::slotQuit() { theApp()->doQuit(this); } /****************************************************************************** * Called when the user or the session manager attempts to close the window. */ void MainWindow::closeEvent(QCloseEvent* ce) { if (!qApp->isSavingSession()) { // The user (not the session manager) wants to close the window. if (isTrayParent()) { // It's the parent window of the system tray icon, so just hide // it to prevent the system tray icon closing. hide(); theApp()->quitIf(); ce->ignore(); return; } } ce->accept(); } /****************************************************************************** * Called when the drag cursor enters a main or system tray window, to accept * or reject the dragged object. */ void MainWindow::executeDragEnterEvent(QDragEnterEvent* e) { const QMimeData* data = e->mimeData(); bool accept = ICalDrag::canDecode(data) ? !e->source() // don't accept "text/calendar" objects from this application : data->hasText() || data->hasUrls() || KPIM::MailList::canDecode(data); if (accept) e->acceptProposedAction(); } /****************************************************************************** * Called when an object is dropped on the window. * If the object is recognised, the edit alarm dialog is opened appropriately. */ void MainWindow::dropEvent(QDropEvent* e) { executeDropEvent(this, e); } static QString getMailHeader(const char* header, KMime::Content& content) { KMime::Headers::Base* hd = content.headerByType(header); return hd ? hd->asUnicodeString() : QString(); } /****************************************************************************** * Called when an object is dropped on a main or system tray window, to * evaluate the action required and extract the text. */ void MainWindow::executeDropEvent(MainWindow* win, QDropEvent* e) { qCDebug(KALARM_LOG) << "MainWindow::executeDropEvent: Formats:" << e->mimeData()->formats(); const QMimeData* data = e->mimeData(); KAEvent::SubAction action = KAEvent::MESSAGE; QByteArray bytes; AlarmText alarmText; KPIM::MailList mailList; QList urls; MemoryCalendar::Ptr calendar(new MemoryCalendar(Preferences::timeSpecAsZone())); #ifndef NDEBUG QString fmts = data->formats().join(QStringLiteral(", ")); qCDebug(KALARM_LOG) << "MainWindow::executeDropEvent:" << fmts; #endif /* The order of the tests below matters, since some dropped objects * provide more than one mime type. * Don't change them without careful thought !! */ if (!(bytes = data->data(QStringLiteral("message/rfc822"))).isEmpty()) { // Email message(s). Ignore all but the first. qCDebug(KALARM_LOG) << "MainWindow::executeDropEvent: email"; KMime::Content content; content.setContent(bytes); content.parse(); QString body; if (content.textContent()) body = content.textContent()->decodedText(true, true); // strip trailing newlines & spaces unsigned long sernum = 0; if (KPIM::MailList::canDecode(data)) { // Get its KMail serial number to allow the KMail message // to be called up from the alarm message window. mailList = KPIM::MailList::fromMimeData(data); if (!mailList.isEmpty()) sernum = mailList.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.setTime_t(summary.date()); + 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 Item item = 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")) == QStringLiteral("message/rfc822")) { // It's an email held in Akonadi qCDebug(KALARM_LOG) << "MainWindow::executeDropEvent: Akonadi email"; ItemFetchJob* job = new ItemFetchJob(item); job->fetchScope().fetchFullPayload(); 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 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(QStringLiteral("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 = !CollectionControlModel::enabledCollections(CalEvent::ACTIVE, true).isEmpty(); bool templat = !CollectionControlModel::enabledCollections(CalEvent::TEMPLATE, true).isEmpty(); for (int i = 0, end = mWindowList.count(); i < end; ++i) { MainWindow* w = mWindowList[i]; w->mActionImportAlarms->setEnabled(active || templat); w->mActionImportBirthdays->setEnabled(active); w->mActionCreateTemplate->setEnabled(templat); // Note: w->mActionNew enabled status is set in the NewAlarmAction class. w->slotSelection(); } } /****************************************************************************** * Called when the selected items in the ListView change. * Enables the actions appropriately. */ void MainWindow::slotSelection() { // Find which events have been selected QVector events = mListView->selectedEvents(); int count = events.count(); if (!count) { selectionCleared(); // disable actions Q_EMIT selectionChanged(); return; } // Find whether there are any writable resources bool active = mActionNew->isEnabled(); bool readOnly = false; bool allArchived = true; bool enableReactivate = true; bool enableEnableDisable = true; bool enableEnable = false; bool enableDisable = false; AlarmCalendar* resources = AlarmCalendar::resources(); const KADateTime now = KADateTime::currentUtcDateTime(); for (int i = 0; i < count; ++i) { KAEvent* ev = resources->event(EventId(events.at(i))); // get up-to-date status KAEvent* event = ev ? ev : &events[i]; bool expired = event->expired(); if (!expired) allArchived = false; if (resources->eventReadOnly(event->itemId())) readOnly = true; if (enableReactivate && (!expired || !event->occursAfter(now, true))) enableReactivate = false; if (enableEnableDisable) { if (expired) enableEnableDisable = enableEnable = enableDisable = false; else { if (!enableEnable && !event->enabled()) enableEnable = true; if (!enableDisable && event->enabled()) enableDisable = true; } } } qCDebug(KALARM_LOG) << "MainWindow::slotSelection: true"; mActionCreateTemplate->setEnabled((count == 1) && !CollectionControlModel::enabledCollections(CalEvent::TEMPLATE, true).isEmpty()); mActionExportAlarms->setEnabled(true); mActionExport->setEnabled(true); mActionCopy->setEnabled(active && count == 1); mActionModify->setEnabled(count == 1); mActionDelete->setEnabled(!readOnly && (active || allArchived)); mActionReactivate->setEnabled(active && enableReactivate); mActionEnable->setEnabled(active && !readOnly && (enableEnable || enableDisable)); if (enableEnable || enableDisable) setEnableText(enableEnable); Q_EMIT selectionChanged(); } /****************************************************************************** * Called when a context menu is requested in the ListView. * Displays a context menu to modify or delete the selected item. */ void MainWindow::slotContextMenuRequested(const QPoint& globalPos) { qCDebug(KALARM_LOG) << "MainWindow::slotContextMenuRequested"; if (mContextMenu) mContextMenu->popup(globalPos); } /****************************************************************************** * Disables actions when no item is selected. */ void MainWindow::selectionCleared() { mActionCreateTemplate->setEnabled(false); mActionExportAlarms->setEnabled(false); mActionExport->setEnabled(false); mActionCopy->setEnabled(false); mActionModify->setEnabled(false); mActionDelete->setEnabled(false); mActionReactivate->setEnabled(false); mActionEnable->setEnabled(false); } /****************************************************************************** * Set the text of the Enable/Disable menu action. */ void MainWindow::setEnableText(bool enable) { mActionEnableEnable = enable; mActionEnable->setText(enable ? i18nc("@action", "Ena&ble") : i18nc("@action", "Disa&ble")); } /****************************************************************************** * Display or hide the specified main window. * This should only be called when the application doesn't run in the system tray. */ MainWindow* MainWindow::toggleWindow(MainWindow* win) { if (win && mWindowList.indexOf(win) != -1) { // A window is specified (and it exists) if (win->isVisible()) { // The window is visible, so close it win->close(); return nullptr; } else { // The window is hidden, so display it win->hide(); // in case it's on a different desktop win->setWindowState(win->windowState() & ~Qt::WindowMinimized); win->raise(); win->activateWindow(); return win; } } // No window is specified, or the window doesn't exist. Open a new one. win = create(); win->show(); return win; } /****************************************************************************** * Called when the Edit button is clicked in an alarm message window. * This controls the alarm edit dialog created by the alarm window, and allows * it to remain unaffected by the alarm window closing. * See MessageWin::slotEdit() for more information. */ void MainWindow::editAlarm(EditAlarmDlg* dlg, const KAEvent& event) { mEditAlarmMap[dlg] = event; connect(dlg, &KEditToolBar::accepted, this, &MainWindow::editAlarmOk); connect(dlg, &KEditToolBar::destroyed, this, &MainWindow::editAlarmDeleted); dlg->setAttribute(Qt::WA_DeleteOnClose, true); // ensure no memory leaks dlg->show(); } /****************************************************************************** * Called when OK is clicked in the alarm edit dialog shown by editAlarm(). * Updates the event which has been edited. */ void MainWindow::editAlarmOk() { EditAlarmDlg* dlg = qobject_cast(sender()); if (!dlg) return; QMap::Iterator it = mEditAlarmMap.find(dlg); if (it == mEditAlarmMap.end()) return; KAEvent event = it.value(); mEditAlarmMap.erase(it); if (!event.isValid()) return; if (dlg->result() != QDialog::Accepted) return; Akonadi::Collection c = AkonadiModel::instance()->collection(event); KAlarm::updateEditedAlarm(dlg, event, c); } /****************************************************************************** * Called when the alarm edit dialog shown by editAlarm() is deleted. * Removes the dialog from the pending list. */ void MainWindow::editAlarmDeleted(QObject* obj) { mEditAlarmMap.remove(static_cast(obj)); } // vim: et sw=4: diff --git a/src/rtcwakeaction.cpp b/src/rtcwakeaction.cpp index 5176696f..f8a44130 100644 --- a/src/rtcwakeaction.cpp +++ b/src/rtcwakeaction.cpp @@ -1,106 +1,106 @@ /* * rtcwakeaction.cpp - KAuth helper application to execute rtcwake commands * Program: kalarm * Copyright © 2011,2018 by David Jarvie * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "rtcwakeaction.h" #include #include #include "kalarm_debug.h" #include #include #include RtcWakeAction::RtcWakeAction() { KLocalizedString::setApplicationDomain("kalarm"); } ActionReply RtcWakeAction::settimer(const QVariantMap& args) { unsigned t = args[QStringLiteral("time")].toUInt(); qCDebug(KALARM_LOG) << "RtcWakeAction::settimer(" << t << ")"; // Find the rtcwake executable QString exe(QStringLiteral("/usr/sbin/rtcwake")); // default location FILE* wh = popen("whereis -b rtcwake", "r"); if (wh) { char buff[512] = { '\0' }; fgets(buff, sizeof(buff), wh); pclose(wh); // The string should be in the form "rtcwake: /path/rtcwake" char* start = strchr(buff, ':'); if (start) { if (*++start == ' ') ++start; char* end = strpbrk(start, " \r\n"); if (end) *end = 0; if (*start) { exe = QString::fromLocal8Bit(start); qCDebug(KALARM_LOG) << "RtcWakeAction::settimer:" << exe; } } } // Set the wakeup by executing the rtcwake command int result = -2; // default = command not found QProcess proc; if (!exe.isEmpty()) { // The wakeup time is set using a time from now ("-s") in preference to // an absolute time ("-t") so that if the hardware clock is not in sync // with the system clock, the alarm will still occur at the correct time. // The "-m no" option sets the wakeup time without suspending the computer. // If 't' is zero, the current wakeup is cancelled by setting a new wakeup // time 2 seconds from now, which will then expire. - unsigned now = QDateTime::currentDateTimeUtc().toTime_t(); + qint64 now = QDateTime::currentDateTimeUtc().toSecsSinceEpoch(); proc.setProgram(exe); proc.setArguments({ QStringLiteral("-m"), QStringLiteral("no"), QStringLiteral("-s"), QString::number(t ? t - now : 2) }); proc.start(); proc.waitForStarted(5000); // allow a timeout of 5 seconds result = proc.exitCode(); } QString errmsg; switch (result) { case 0: return ActionReply::SuccessReply(); case -2: errmsg = xi18nc("@text/plain", "Could not run %1 to set wake from suspend", QStringLiteral("rtcwake")); break; default: errmsg = xi18nc("@text/plain", "Error setting wake from suspend.Command was: %1 %2Error code: %3.", proc.program(), proc.arguments().join(QStringLiteral(" ")), result); break; } ActionReply reply = ActionReply::HelperErrorReply(result); reply.setErrorDescription(errmsg); qCDebug(KALARM_LOG) << "RtcWakeAction::settimer: Code=" << reply.errorCode() << reply.errorDescription(); return reply; } KAUTH_HELPER_MAIN("org.kde.kalarm.rtcwake", RtcWakeAction) // vim: et sw=4: