diff --git a/src/resources/calendarupdater.cpp b/src/resources/calendarupdater.cpp index fc1f4d6f..dd98c8e3 100644 --- a/src/resources/calendarupdater.cpp +++ b/src/resources/calendarupdater.cpp @@ -1,109 +1,109 @@ /* * calendarupdater.cpp - base class to update a calendar to current KAlarm format * Program: kalarm * Copyright © 2020 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 "calendarupdater.h" #include "lib/desktop.h" #include #include #include /*============================================================================= = Class to prompt the user to update the storage format for a resource, if it = currently uses an old KAlarm storage format. =============================================================================*/ QVector CalendarUpdater::mInstances; CalendarUpdater::CalendarUpdater(ResourceId resourceId, bool ignoreKeepFormat, QObject* parent, QWidget* promptParent) : QObject(parent) , mResourceId(resourceId) , mParent(parent) , mPromptParent(promptParent ? promptParent : Desktop::mainWindow()) , mIgnoreKeepFormat(ignoreKeepFormat) , mDuplicate(containsResource(resourceId)) { mInstances.append(this); } CalendarUpdater::~CalendarUpdater() { mInstances.removeAll(this); } bool CalendarUpdater::containsResource(ResourceId id) { for (CalendarUpdater* instance : mInstances) { if (instance->mResourceId == id) return true; } return false; } /****************************************************************************** * Wait until all instances have completed and been deleted. */ void CalendarUpdater::waitForCompletion() { while (!mInstances.isEmpty()) { for (int i = mInstances.count(); --i >= 0; ) if (mInstances.at(i)->isComplete()) delete mInstances.at(i); // the destructor removes the instance from mInstances QCoreApplication::processEvents(); if (!mInstances.isEmpty()) QThread::msleep(100); } } /****************************************************************************** * Mark the instance as completed, and schedule its deletion. */ void CalendarUpdater::setCompleted() { mCompleted = true; deleteLater(); } /****************************************************************************** * Return a prompt string to ask the user whether to convert the calendar to the * current format. */ QString CalendarUpdater::conversionPrompt(const QString& calendarName, const QString& calendarVersion, bool whole) { const QString msg = whole - ? xi18n("Calendar %1 is in an old format (KAlarm version %2), " + ? xi18nc("@info", "Calendar %1 is in an old format (KAlarm version %2), " "and will be read-only unless you choose to update it to the current format.", calendarName, calendarVersion) - : xi18n("Some or all of the alarms in calendar %1 are in an old KAlarm format, " + : xi18nc("@info", "Some or all of the alarms in calendar %1 are in an old KAlarm format, " "and will be read-only unless you choose to update them to the current format.", calendarName); return xi18nc("@info", "%1" "Do not update the calendar if it is also used with an older version of KAlarm " "(e.g. on another computer). If you do so, the calendar may become unusable there." "Do you wish to update the calendar?", msg); } // vim: et sw=4: diff --git a/src/resources/datamodel.h b/src/resources/datamodel.h index ca0fb3d9..73bcfa99 100644 --- a/src/resources/datamodel.h +++ b/src/resources/datamodel.h @@ -1,104 +1,86 @@ /* * datamodel.h - model independent access to calendar functions * Program: kalarm * Copyright © 2019-2020 David Jarvie * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef DATAMODEL_H #define DATAMODEL_H #include class Resource; class ResourceListModel; class ResourceFilterCheckListModel; class AlarmListModel; class TemplateListModel; class ResourceCreator; /*============================================================================= = Class: DataModel = Static methods providing model independent access to the resource data model. =============================================================================*/ class DataModel { public: /** Initialise the data model. */ static void initialise(); /** Terminate access to the data model, and tidy up. */ static void terminate(); /** Reload all resources' data from storage. * @note In the case of Akonadi, this does not reload from the backend storage. */ static void reload(); /** Reload a resource's data from storage. * @note In the case of Akonadi, this does not reload from the backend storage. */ static bool reload(Resource&); /** Return whether calendar migration/creation at initialisation has completed. */ static bool isMigrationComplete(); /** Check for, and remove, any duplicate Akonadi resources, i.e. those which * use the same calendar file/directory. */ static void removeDuplicateResources(); /** Disable the widget if the database engine is not available, and display an * error overlay. */ static void widgetNeedsDatabase(QWidget*); /** Create a ResourceCreator instance for the model. */ static ResourceCreator* createResourceCreator(KAlarmCal::CalEvent::Type defaultType, QWidget* parent); /** Update a resource's backend calendar file to the current KAlarm format. */ static void updateCalendarToCurrentFormat(Resource&, bool ignoreKeepFormat, QObject* parent); static ResourceListModel* createResourceListModel(QObject* parent); static ResourceFilterCheckListModel* createResourceFilterCheckListModel(QObject* parent); static AlarmListModel* createAlarmListModel(QObject* parent); static AlarmListModel* allAlarmListModel(); static TemplateListModel* createTemplateListModel(QObject* parent); static TemplateListModel* allTemplateListModel(); - -#if 0 - static QSize iconSize() { return mIconSize; } - - /** Return a bulleted list of alarm types for inclusion in an i18n message. */ - static QString typeListForDisplay(CalEvent::Types); - - /** Get the tooltip for a resource. The resource's enabled status is - * evaluated for specified alarm types. */ - QString tooltip(const Resource&, CalEvent::Types) const; - - /** Return the read-only status tooltip for a resource. - * A null string is returned if the resource is fully writable. */ - static QString readOnlyTooltip(const Resource&); - - /** Return offset to add to headerData() role, for item models. */ - virtual int headerDataEventRoleOffset() const { return 0; } -#endif }; #endif // DATAMODEL_H // vim: et sw=4: diff --git a/src/resources/dirresourceimportdialog.cpp b/src/resources/dirresourceimportdialog.cpp index a3472e4f..b79b791c 100644 --- a/src/resources/dirresourceimportdialog.cpp +++ b/src/resources/dirresourceimportdialog.cpp @@ -1,510 +1,510 @@ /* * dirresourceimportdialog.cpp - configuration dialog to import directory resources * Program: kalarm * Copyright © 2020 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 "dirresourceimportdialog.h" #include "dirresourceimportdialog_p.h" #include "ui_dirresourceimportdialog_intro.h" #include "ui_dirresourceimportdialog_type.h" #include "resources.h" #include "fileresource.h" #include #include #include #include #include #include using namespace KAlarmCal; /****************************************************************************** * Constructor. */ DirResourceImportDialog::DirResourceImportDialog(const QString& dirResourceName, const QString& dirResourcePath, KAlarmCal::CalEvent::Types types, QWidget* parent) : KAssistantDialog(parent) , mAlarmTypes(types) { - setWindowTitle(i18n("Import Directory Resource")); + setWindowTitle(i18nc("@title:window", "Import Directory Resource")); // Remove Help button buttonBox()->removeButton(button(QDialogButtonBox::Help)); mPageIntro = new DirResourceImportIntroWidget(dirResourceName, dirResourcePath, types, this); - addPage(mPageIntro, i18n("Import Calendar Directory Resource")); + addPage(mPageIntro, i18nc("@title:tab", "Import Calendar Directory Resource")); mAlarmTypeCount = 0; if (mAlarmTypes & CalEvent::ACTIVE) { ++mAlarmTypeCount; mPageActive = new DirResourceImportTypeWidget(CalEvent::ACTIVE, this); - addPage(mPageActive, i18n("Import Active Alarms")); + addPage(mPageActive, i18nc("@title:tab", "Import Active Alarms")); connect(mPageActive, &DirResourceImportTypeWidget::status, this, &DirResourceImportDialog::typeStatusChanged); mLastPage = mPageActive; } if (mAlarmTypes & CalEvent::ARCHIVED) { ++mAlarmTypeCount; mPageArchived = new DirResourceImportTypeWidget(CalEvent::ARCHIVED, this); - addPage(mPageArchived, i18n("Import Archived Alarms")); + addPage(mPageArchived, i18nc("@title:tab", "Import Archived Alarms")); connect(mPageArchived, &DirResourceImportTypeWidget::status, this, &DirResourceImportDialog::typeStatusChanged); mLastPage = mPageArchived; } if (mAlarmTypes & CalEvent::TEMPLATE) { ++mAlarmTypeCount; mPageTemplate = new DirResourceImportTypeWidget(CalEvent::TEMPLATE, this); - addPage(mPageTemplate, i18n("Import Alarm Templates")); + addPage(mPageTemplate, i18nc("@title:tab", "Import Alarm Templates")); connect(mPageTemplate, &DirResourceImportTypeWidget::status, this, &DirResourceImportDialog::typeStatusChanged); mLastPage = mPageTemplate; } connect(this, &KPageDialog::currentPageChanged, this, &DirResourceImportDialog::pageChanged); } DirResourceImportDialog::~DirResourceImportDialog() { } /****************************************************************************** * Return the existing resource to import into, for a specified alarm type. */ ResourceId DirResourceImportDialog::resourceId(KAlarmCal::CalEvent::Type type) const { const DirResourceImportTypeWidget* page = typePage(type); return page ? page->resourceId() : -1; } /****************************************************************************** * Return the new calendar file URL, for a specified alarm type. */ QUrl DirResourceImportDialog::url(KAlarmCal::CalEvent::Type type) const { const DirResourceImportTypeWidget* page = typePage(type); return page ? page->url() : QUrl(); } /****************************************************************************** * Return the resource's display name, for a specified alarm type. */ QString DirResourceImportDialog::displayName(KAlarmCal::CalEvent::Type type) const { const DirResourceImportTypeWidget* page = typePage(type); return page ? page->displayName() : QString(); } /****************************************************************************** * Set a validation function to apply to an entered URL. */ void DirResourceImportDialog::setUrlValidation(QString (*func)(const QUrl&)) { if (mPageActive) mPageActive->setUrlValidation(func); if (mPageArchived) mPageArchived->setUrlValidation(func); if (mPageTemplate) mPageTemplate->setUrlValidation(func); } /****************************************************************************** * When a new page is displayed, set appropriate heights for elements within the * page, and enable/disable elements according to their status. */ void DirResourceImportDialog::pageChanged(KPageWidgetItem* current, KPageWidgetItem* before) { Q_UNUSED(before); if (current) { DirResourceImportWidgetBase* page = static_cast(current->widget()); page->setTextSizes(); DirResourceImportTypeWidget* typePage = qobject_cast(page); if (typePage) typePage->validate(); } } /****************************************************************************** * Called when the data entered into an alarm type import page has changed. * Enable or disable the Next button, depending on whether the entered data is * valid or not. */ void DirResourceImportDialog::typeStatusChanged(bool ok) { DirResourceImportTypeWidget* page = qobject_cast(currentPage()->widget()); if (page) { nextButton()->setEnabled(ok && (page != mLastPage)); finishButton()->setEnabled(ok && (page == mLastPage)); } } const DirResourceImportTypeWidget* DirResourceImportDialog::typePage(KAlarmCal::CalEvent::Type type) const { if (mAlarmTypes & type) { switch (type) { case CalEvent::ACTIVE: return mPageActive; case CalEvent::ARCHIVED: return mPageArchived; case CalEvent::TEMPLATE: return mPageTemplate; default: break; } } return nullptr; } /*============================================================================= = Class DirResourceImportWidgetBase = Base class for page widgets. =============================================================================*/ DirResourceImportWidgetBase::DirResourceImportWidgetBase(QWidget* parent) : QWidget(parent) { } /*============================================================================= = Class DirResourceImportIntroWidget = The first page of the directory resource import dialog, which gives general = information to the user. =============================================================================*/ DirResourceImportIntroWidget::DirResourceImportIntroWidget(const QString& dirResourceName, const QString& dirResourcePath, KAlarmCal::CalEvent::Types types, QWidget* parent) : DirResourceImportWidgetBase(parent) { mUi = new Ui_DirResourceImportIntroWidget; mUi->setupUi(this); mUi->dirNameLabel->setText(dirResourceName); mUi->dirPathLabel->setText(dirResourcePath); QStringList typeNames; if (types & CalEvent::ACTIVE) - typeNames += i18n("Active alarms"); + typeNames += i18nc("@item:intext", "Active alarms"); if (types & CalEvent::ARCHIVED) - typeNames += i18n("Archived alarms"); + typeNames += i18nc("@item:intext", "Archived alarms"); if (types & CalEvent::TEMPLATE) - typeNames += i18n("Alarm templates"); + typeNames += i18nc("@item:intext", "Alarm templates"); mUi->dirTypesLabel->setText(typeNames.join(QStringLiteral(", "))); if (typeNames.size() > 1) mUi->warning1->setVisible(false); else mUi->warning2->setVisible(false); } DirResourceImportIntroWidget::~DirResourceImportIntroWidget() { delete mUi; } /****************************************************************************** * Called when the page is displayed, to set appropriate heights for QLabel * elements with text wrapping, and then remove empty space between widgets. * By default, if QLabel contains more than one line of text, it often occupies * more space than required, and calling setMinimumHeight() doesn't affect this. */ void DirResourceImportIntroWidget::setTextSizes() { mUi->warning1->setFixedHeight(mUi->warning1->height()); mUi->warning2->setFixedHeight(mUi->warning2->height()); mUi->note->setFixedHeight(mUi->note->height()); setFixedHeight(sizeHint().height()); } /*============================================================================= = Class DirResourceImportTypeWidget = The page of the directory resource import dialog which allows the user to = specify how to import one alarm type. =============================================================================*/ DirResourceImportTypeWidget::DirResourceImportTypeWidget(CalEvent::Type alarmType, QWidget* parent) : DirResourceImportWidgetBase(parent) { mUi = new Ui_DirResourceImportTypeWidget; mUi->setupUi(this); const QVector resources = Resources::allResources(alarmType, Resources::DefaultFirst | Resources::DisplayName); if (resources.isEmpty()) { mUi->mergeRadio->setVisible(false); mUi->mergeRadio->setEnabled(false); mUi->mergeResource->setVisible(false); } else { for (const Resource& resource : resources) mUi->mergeResource->addItem(resource.displayName(), QVariant(resource.id())); } connect(mUi->optionGroup, QOverload::of(&QButtonGroup::buttonToggled), this, &DirResourceImportTypeWidget::importTypeSelected); mUi->pathRequester->setMode(KFile::File); - mUi->pathRequester->setFilter(QStringLiteral("*.ics|%1").arg(i18nc("@info", "Calendar Files"))); + mUi->pathRequester->setFilter(QStringLiteral("*.ics|%1").arg(i18nc("@item:inlistbox", "Calendar Files"))); mUi->statusLabel->setText(QString()); mUi->pathRequester->setFocus(); mUi->pathRequester->installEventFilter(this); connect(mUi->pathRequester, QOverload<>::of(&KUrlRequester::returnPressed), this, [this]() { validate(); }); connect(mUi->pathRequester, &KUrlRequester::urlSelected, this, [this]() { validate(); }); connect(mUi->pathRequester, &KUrlRequester::textChanged, this, [this]() { setStatus(false); }); connect(mUi->nameText, &QLineEdit::textChanged, this, [this]() { validate(); }); // Indent fields beneath each radio button option. QRadioButton radio(this); QStyleOptionButton opt; opt.initFrom(&radio); int indentWidth = style()->subElementRect(QStyle::SE_RadioButtonIndicator, &opt).width(); mUi->grid->setColumnMinimumWidth(0, indentWidth); mUi->grid->setColumnStretch(1, 1); importTypeSelected(); QTimer::singleShot(0, this, &DirResourceImportTypeWidget::validate); } DirResourceImportTypeWidget::~DirResourceImportTypeWidget() { delete mUi; } /****************************************************************************** * Return the existing resource ID to import into. */ ResourceId DirResourceImportTypeWidget::resourceId() const { return mUi->mergeRadio->isChecked() ? mUi->mergeResource->currentData().toLongLong() : -1; } /****************************************************************************** * Return the new calendar file URL. */ QUrl DirResourceImportTypeWidget::url() const { return mUi->newRadio->isChecked() ? mUi->pathRequester->url() : QUrl(); } /****************************************************************************** * Return the resource's display name. */ QString DirResourceImportTypeWidget::displayName() const { return mUi->newRadio->isChecked() ? mUi->nameText->text() : QString(); } /****************************************************************************** * Notify the page that it is the last page. */ void DirResourceImportTypeWidget::setLastPage() { mLastPage = true; } /****************************************************************************** * Set a validation function to apply to an entered URL. */ void DirResourceImportTypeWidget::setUrlValidation(QString (*func)(const QUrl&)) { mUrlValidationFunc = func; } /****************************************************************************** * Called when the page is displayed, to set appropriate heights for QLabel * elements with text wrapping, and then remove empty space between widgets. * By default, if QLabel contains more than one line of text, it often occupies * more space than required, and calling setMinimumHeight() doesn't affect this. */ void DirResourceImportTypeWidget::setTextSizes() { const int spacing = style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing); mUi->spacer1->changeSize(10, 2*spacing, QSizePolicy::Fixed, QSizePolicy::Fixed); mUi->spacer2->changeSize(10, 2*spacing, QSizePolicy::Fixed, QSizePolicy::Fixed); mUi->spacer3->changeSize(10, 2*spacing, QSizePolicy::Fixed, QSizePolicy::Fixed); setFixedHeight(sizeHint().height()); } bool DirResourceImportTypeWidget::eventFilter(QObject* o, QEvent* e) { if (o == mUi->pathRequester && e->type() == QEvent::FocusOut) validate(); return QWidget::eventFilter(o, e); } /****************************************************************************** * Called when an import destination type radio button has been selected. * Enable or disable the other controls. */ void DirResourceImportTypeWidget::importTypeSelected() { const bool importMerge = mUi->mergeRadio->isChecked(); const bool importNew = mUi->newRadio->isChecked(); mUi->mergeResource->setEnabled(importMerge); mUi->pathLabel->setEnabled(importNew); mUi->pathRequester->setEnabled(importNew); mUi->statusLabel->setEnabled(importNew); mUi->nameLabel->setEnabled(importNew); mUi->nameText->setEnabled(importNew); if (importNew) validate(); else if (importMerge || mUi->noRadio->isChecked()) Q_EMIT status(true); } /****************************************************************************** * Validate the current user input. If invalid, disable the OK button. * Reply = false if invalid, or if waiting for remote validity to be checked. */ void DirResourceImportTypeWidget::validate() { if (!mUi->newRadio->isChecked()) return; // Validate URL first, in order to display any error message. const QUrl currentUrl = mUi->pathRequester->url(); if (mUi->pathRequester->text().trimmed().isEmpty() || currentUrl.isEmpty()) { setStatus(false); return; } if (mUrlValidationFunc) { const QString error = (*mUrlValidationFunc)(currentUrl); if (!error.isEmpty()) { setStatus(false, error); return; } } if (!currentUrl.isLocalFile()) { // It's a remote file. // Check whether the file can be read or written. if (mStatJob) mStatJob->kill(); mCheckingDir = false; mStatJob = KIO::statDetails(currentUrl, KIO::StatJob::SourceSide, KIO::StatDetail::StatNoDetails, KIO::HideProgressInfo); connect(mStatJob, &KIO::StatJob::result, this, &DirResourceImportTypeWidget::slotStatJobResult); // Disable the OK button until the file's status is determined. setStatus(false, i18nc("@info:status", "Checking file information..."), false); return; } // It's a local file. Check that it doesn't already exist, and that it can be created. QFileInfo file(currentUrl.toLocalFile()); if (file.exists()) { setStatus(false, i18nc("@info:status", "Error! File already exists.")); return; } QFileInfo dir(file.path()); if (!dir.exists()) { setStatus(false, i18nc("@info:status", "Error! Cannot create file (directory does not exist).")); return; } if (!dir.isWritable()) { setStatus(false, i18nc("@info:status", "Error! Cannot create file (directory is not writable).")); return; } if (mUi->nameText->text().trimmed().isEmpty()) { setStatus(false); return; } setStatus(true); } /****************************************************************************** * Called when the status of the remote URL has been determined. * Ensures that the URL doesn't exist, but can be created. */ void DirResourceImportTypeWidget::slotStatJobResult(KJob* job) { KIO::StatJob* statJob = static_cast(job); mStatJob = nullptr; if (!mCheckingDir) { // Results from checking the remote file. if (job->error() == KIO::ERR_DOES_NOT_EXIST) { // The file doesn't exist (as expected), so check that the file's // directory is writable. mCheckingDir = true; mStatJob = KIO::statDetails(KIO::upUrl(mUi->pathRequester->url()), KIO::StatJob::SourceSide, KIO::StatDetail::StatDefaultDetails, KIO::HideProgressInfo); connect(mStatJob, &KIO::StatJob::result, this, &DirResourceImportTypeWidget::slotStatJobResult); return; } setStatus(false, i18nc("@info:status", "Error! File already exists.")); } else { // Results from checking the remote file's directory. mCheckingDir = false; if (job->error()) { if (job->error() == KIO::ERR_DOES_NOT_EXIST) setStatus(false, i18nc("@info:status", "Error! Cannot create file (directory does not exist).")); else setStatus(false, i18nc("@info:status", "Error! Cannot create file (directory is not writable).")); } else { KFileItem fi(statJob->statResult(), KIO::upUrl(mUi->pathRequester->url())); if (!fi.isDir()) - setStatus(false, i18nc("@info:status", "Error! Cannot create file (directory does not exist).")); + setStatus(false, i18nc("@info", "Error! Cannot create file (directory does not exist).")); else if (!fi.isWritable()) - setStatus(false, i18nc("@info:status", "Error! Cannot create file (directory is not writable).")); + setStatus(false, i18nc("@info", "Error! Cannot create file (directory is not writable).")); else setStatus(true); } } } /****************************************************************************** * Set or clear the URL status message, and notify the dialog of the new status. */ void DirResourceImportTypeWidget::setStatus(bool ok, const QString& errorMessage, bool errorColour) { if (ok) mUi->statusLabel->setText(QString()); else { QPalette pal = mUi->pathLabel->palette(); if (errorColour && !errorMessage.isEmpty()) pal.setColor(QPalette::WindowText, KColorScheme(QPalette::Active).foreground(KColorScheme::NegativeText).color()); mUi->statusLabel->setPalette(pal); mUi->statusLabel->setText(errorMessage); } Q_EMIT status(ok); } // vim: et sw=4: diff --git a/src/resources/dirresourceimportdialog_intro.ui b/src/resources/dirresourceimportdialog_intro.ui index b7b5eff6..958214ec 100644 --- a/src/resources/dirresourceimportdialog_intro.ui +++ b/src/resources/dirresourceimportdialog_intro.ui @@ -1,83 +1,83 @@ DirResourceImportIntroWidget true - KAlarm no longer supports calendar directory resources. In order to continue using the contents of the directory resource below, you must import it into a KAlarm calendar file resource. + KAlarm no longer supports calendar directory resources. In order to continue using the contents of the directory resource below, you must import it into a KAlarm calendar file resource. true - KAlarm no longer supports calendar directory resources. In order to continue using the contents of the directory resource below, you must import each of its alarm types into separate KAlarm calendar file resources. + KAlarm no longer supports calendar directory resources. In order to continue using the contents of the directory resource below, you must import each of its alarm types into separate KAlarm calendar file resources. - Display name: + Display name: true - Calendar directory: + Calendar directory: true - Alarm types: + Alarm types: true true - If you prefer, you can import this calendar directory resource later, by using the Import Alarms menu option to import the calendar files contained within it. + If you prefer, you can import this calendar directory resource later, by using the Import Alarms menu option to import the calendar files contained within it. diff --git a/src/resources/dirresourceimportdialog_type.ui b/src/resources/dirresourceimportdialog_type.ui index 63d003a8..af2d7ebb 100644 --- a/src/resources/dirresourceimportdialog_type.ui +++ b/src/resources/dirresourceimportdialog_type.ui @@ -1,178 +1,178 @@ DirResourceImportTypeWidget - Import into existing calendar file resource + Import into existing calendar file resource true optionGroup - Select the resource to import the directory resource's alarms into. + Select the resource to import the directory resource's alarms into. Qt::Vertical QSizePolicy::Fixed 20 20 - Import into new calendar file resource + Import into new calendar file resource false optionGroup - Filename: + Filename: pathRequester - Select the file to contain this resource. The file must not already exist. + Select the file to contain this resource. The file must not already exist. - Select the file to contain this resource. + Select the file to contain this resource. The file must not already exist. Note that if the URL of a remote file is specified, monitoring for file changes will not work. 75 true - Status: + Status: - Display name: + Display name: nameText - Enter the name used to identify this resource in displays. + Enter the name used to identify this resource in displays. - Enter the name used to identify this resource in displays. + Enter the name used to identify this resource in displays. Qt::Vertical QSizePolicy::Fixed 20 20 - Do not import + Do not import false optionGroup Qt::Vertical QSizePolicy::Fixed 20 0 KUrlRequester QWidget
kurlrequester.h
KLineEdit QLineEdit
klineedit.h
diff --git a/src/resources/fileresource.cpp b/src/resources/fileresource.cpp index 5b882e48..68a3d21d 100644 --- a/src/resources/fileresource.cpp +++ b/src/resources/fileresource.cpp @@ -1,628 +1,623 @@ /* * fileresource.cpp - base class for calendar resource accessed via file system * Program: kalarm * Copyright © 2006-2020 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 "fileresource.h" #include "fileresourcesettings.h" #include "fileresourceconfigmanager.h" #include "singlefileresourceconfigdialog.h" #include "resources.h" #include "lib/autoqpointer.h" #include "kalarm_debug.h" #include #include -namespace -{ -const QString loadErrorMessage = i18n("Error loading calendar '%1'."); -const QString saveErrorMessage = i18n("Error saving calendar '%1'."); -} FileResource::FileResource(FileResourceSettings* settings) : ResourceType(settings->id()) , mSettings(settings) { } FileResource::~FileResource() { } bool FileResource::isValid() const { // The settings ID must not have changed since construction. return mSettings->isValid() && mStatus < Status::Unusable && id() >= 0 && mSettings->id() == id(); } ResourceId FileResource::displayId() const { return id() & ~IdFlag; } QString FileResource::storageTypeString(bool description) const { bool file; switch (mSettings->storageType()) { case FileResourceSettings::File: file = true; break; case FileResourceSettings::Directory: file = false; break; default: return QString(); } return storageTypeStr(description, file, mSettings->url().isLocalFile()); } QUrl FileResource::location() const { return mSettings->url(); } QString FileResource::displayLocation() const { return mSettings->displayLocation(); } QString FileResource::displayName() const { return mSettings->displayName(); } QString FileResource::configName() const { return mSettings->configName(); } CalEvent::Types FileResource::alarmTypes() const { return mSettings->alarmTypes(); } CalEvent::Types FileResource::enabledTypes() const { return mSettings->isValid() ? mSettings->enabledTypes() : CalEvent::EMPTY; } void FileResource::setEnabled(CalEvent::Type type, bool enabled) { const CalEvent::Types oldEnabled = mSettings->enabledTypes(); const Changes changes = mSettings->setEnabled(type, enabled); if (changes) { handleSettingsChange(changes); Resources::notifySettingsChanged(this, changes, oldEnabled); } } void FileResource::setEnabled(CalEvent::Types types) { const CalEvent::Types oldEnabled = mSettings->enabledTypes(); const Changes changes = mSettings->setEnabled(types); if (changes) { handleSettingsChange(changes); Resources::notifySettingsChanged(this, changes, oldEnabled); } } bool FileResource::readOnly() const { return mSettings->readOnly(); } void FileResource::setReadOnly(bool ronly) { const CalEvent::Types oldEnabled = mSettings->enabledTypes(); const Changes changes = mSettings->setReadOnly(ronly); if (changes) { handleSettingsChange(changes); Resources::notifySettingsChanged(this, changes, oldEnabled); } } int FileResource::writableStatus(CalEvent::Type type) const { if (!mSettings->isValid() || mSettings->readOnly()) return -1; if ((type == CalEvent::EMPTY && !mSettings->enabledTypes()) || (type != CalEvent::EMPTY && !mSettings->isEnabled(type))) return -1; switch (mCompatibility) { case KACalendar::Current: return 1; case KACalendar::Converted: case KACalendar::Convertible: return 0; default: return -1; } } bool FileResource::isWritable(const KAEvent& event) const { return isWritable(event.category()); } bool FileResource::keepFormat() const { return mSettings->keepFormat(); } void FileResource::setKeepFormat(bool keep) { const CalEvent::Types oldEnabled = mSettings->enabledTypes(); const Changes changes = mSettings->setKeepFormat(keep); if (changes) { handleSettingsChange(changes); Resources::notifySettingsChanged(this, changes, oldEnabled); } } QColor FileResource::backgroundColour() const { return mSettings->backgroundColour(); } void FileResource::setBackgroundColour(const QColor& colour) { const CalEvent::Types oldEnabled = mSettings->enabledTypes(); const Changes changes = mSettings->setBackgroundColour(colour); if (changes) { handleSettingsChange(changes); Resources::notifySettingsChanged(this, changes, oldEnabled); } } bool FileResource::configIsStandard(CalEvent::Type type) const { return mSettings->isStandard(type); } CalEvent::Types FileResource::configStandardTypes() const { return mSettings->standardTypes(); } void FileResource::configSetStandard(CalEvent::Type type, bool standard) { const CalEvent::Types oldEnabled = mSettings->enabledTypes(); const Changes changes = mSettings->setStandard(type, standard); if (changes) { handleSettingsChange(changes); Resources::notifySettingsChanged(this, changes, oldEnabled); } } void FileResource::configSetStandard(CalEvent::Types types) { const CalEvent::Types oldEnabled = mSettings->enabledTypes(); const Changes changes = mSettings->setStandard(types); if (changes) { handleSettingsChange(changes); Resources::notifySettingsChanged(this, changes, oldEnabled); } } KACalendar::Compat FileResource::compatibilityVersion(QString& versionString) const { versionString = KAlarmCal::getVersionString(mVersion); return mCompatibility; } /****************************************************************************** * Edit the resource's configuration. */ void FileResource::editResource(QWidget* dialogParent) { switch (storageType()) { case File: { // Use AutoQPointer to guard against crash on application exit while // the dialogue is still open. It prevents double deletion (both on // deletion of parent, and on return from this function). AutoQPointer dlg = new SingleFileResourceConfigDialog(false, dialogParent); const CalEvent::Types enabled = enabledTypes(); CalEvent::Types types = alarmTypes(); if (((types & CalEvent::ACTIVE) && (types & (CalEvent::ARCHIVED | CalEvent::TEMPLATE))) || ((types & CalEvent::ARCHIVED) && (types & CalEvent::TEMPLATE))) types &= enabled; const CalEvent::Type alarmType = (types & CalEvent::ACTIVE) ? CalEvent::ACTIVE : (types & CalEvent::ARCHIVED) ? CalEvent::ARCHIVED : (types & CalEvent::TEMPLATE) ? CalEvent::TEMPLATE : CalEvent::ACTIVE; dlg->setAlarmType(alarmType); // set default alarm type dlg->setUrl(location(), true); // show location but disallow edits dlg->setDisplayName(displayName()); dlg->setReadOnly(readOnly()); if (dlg->exec() == QDialog::Accepted) { // Make any changes requested by the user. // Note that the location and alarm type cannot be changed. qCDebug(KALARM_LOG) << "FileResource::editResource: Edited" << dlg->displayName(); setReadOnly(dlg->readOnly()); const Changes change = mSettings->setDisplayName(dlg->displayName()); if (change != NoChange) Resources::notifySettingsChanged(this, change, enabled); } break; } case Directory: // Not currently intended to be implemented. break; default: break; } } /****************************************************************************** * Remove the resource and its settings. The calendar file is not removed. * The instance will be invalid once it has been removed. */ bool FileResource::removeResource() { qCDebug(KALARM_LOG) << "FileResource::removeResource:" << displayId(); Resources::notifyResourceToBeRemoved(this); Resource res = Resources::resource(id()); bool ok = FileResourceConfigManager::removeResource(res); ResourceType::removeResource(id()); return ok; } /****************************************************************************** * Load the resource. */ bool FileResource::load(bool readThroughCache) { qCDebug(KALARM_LOG) << "FileResource::load:" << displayName(); QString errorMessage; if (!mSettings->isValid()) { qCWarning(KALARM_LOG) << "FileResource::load: Resource not configured!" << mSettings->displayName(); - errorMessage = i18n("Resource is not configured."); + errorMessage = i18nc("@info", "Resource is not configured."); } else if (mStatus == Status::Closed) qCWarning(KALARM_LOG) << "FileResource::load: Resource closed!" << mSettings->displayName(); else { if (!isEnabled(CalEvent::EMPTY)) { // Don't load a disabled resource, but mark it as usable (but not loaded). qCDebug(KALARM_LOG) << "FileResource::load: Resource disabled" << mSettings->displayName(); mStatus = Status::Ready; return false; } // Do the actual loading. QHash newEvents; switch (doLoad(newEvents, readThroughCache, errorMessage)) { case 1: // success loaded(true, newEvents, QString()); return true; case 0: // loading initiated return true; default: // failure break; } } if (!errorMessage.isEmpty()) - Resources::notifyResourceMessage(this, MessageType::Error, loadErrorMessage.arg(displayName()), errorMessage); + Resources::notifyResourceMessage(this, MessageType::Error, xi18nc("@info", "Error loading calendar %1.", displayName()), errorMessage); return false; } /****************************************************************************** * Called when the resource has loaded, to finish setting it up. */ void FileResource::loaded(bool success, QHash& newEvents, const QString& errorMessage) { if (!success) { // This is only done when a delayed load fails. // If the resource previously loaded successfully, leave its events (in // mEvents) unchanged. if (!success && !errorMessage.isEmpty()) - Resources::notifyResourceMessage(this, MessageType::Error, loadErrorMessage.arg(displayName()), errorMessage); + Resources::notifyResourceMessage(this, MessageType::Error, xi18nc("@info", "Error loading calendar %1.", displayName()), errorMessage); return; } if (isEnabled(CalEvent::ACTIVE)) { // Set any command execution error flags for the events. // These are stored in the KAlarm config file, not the alarm // calendar, since they are specific to the user's local system. bool changed = false; QHash cmdErrors = mSettings->commandErrors(); for (auto errit = cmdErrors.begin(); errit != cmdErrors.end(); ) { auto evit = newEvents.find(errit.key()); if (evit != newEvents.end()) { KAEvent& event = evit.value(); if (event.category() == CalEvent::ACTIVE) { event.setCommandError(errit.value()); ++errit; continue; } } // The event for this command error doesn't exist, or is not active, // so remove this command error from the settings. errit = cmdErrors.erase(errit); changed = true; } if (changed) mSettings->setCommandErrors(cmdErrors); } // Update the list of loaded events for the resource. setLoadedEvents(newEvents); } /****************************************************************************** * Save the resource. */ bool FileResource::save(bool writeThroughCache, bool force) { qCDebug(KALARM_LOG) << "FileResource::save:" << displayName(); if (!checkSave()) return false; QString errorMessage; switch (doSave(writeThroughCache, force, errorMessage)) { case 1: // success saved(true, QString()); return true; case 0: // saving initiated return true; default: // failure if (!errorMessage.isEmpty()) - Resources::notifyResourceMessage(this, MessageType::Error, saveErrorMessage.arg(displayName()), errorMessage); + Resources::notifyResourceMessage(this, MessageType::Error, xi18nc("@info", "Error saving calendar %1.", displayName()), errorMessage); return false; } } /****************************************************************************** * Check whether the resource can be saved. */ bool FileResource::checkSave() { QString errorMessage; if (!mSettings->isValid()) { qCWarning(KALARM_LOG) << "FileResource::checkSave: FileResource not configured!" << displayName(); - errorMessage = i18n("Resource is not configured."); + errorMessage = i18nc("@info", "Resource is not configured."); } else if (!isValid() || !mSettings->enabledTypes()) return false; else if (readOnly()) { qCWarning(KALARM_LOG) << "FileResource::checkSave: Read-only resource!" << displayName(); - errorMessage = i18n("Resource is read-only."); + errorMessage = i18nc("@info", "Resource is read-only."); } else if (mCompatibility != KACalendar::Current) { qCWarning(KALARM_LOG) << "FileResource::checkSave: Calendar is in wrong format" << displayLocation(); - errorMessage = i18n("Calendar file is in wrong format: '%1'.", displayLocation()); + errorMessage = xi18nc("@info", "Calendar file is in wrong format: %1.", displayLocation()); } else return true; - Resources::notifyResourceMessage(this, MessageType::Error, saveErrorMessage.arg(displayName()), errorMessage); + Resources::notifyResourceMessage(this, MessageType::Error, xi18nc("@info", "Error saving calendar %1.", displayName()), errorMessage); return false; } /****************************************************************************** * Called when the resource has saved, to finish the process. */ void FileResource::saved(bool success, const QString& errorMessage) { if (!success && !errorMessage.isEmpty()) - Resources::notifyResourceMessage(this, MessageType::Error, saveErrorMessage.arg(displayName()), errorMessage); + Resources::notifyResourceMessage(this, MessageType::Error, xi18nc("@info", "Error saving calendar %1.", displayName()), errorMessage); } /****************************************************************************** * Add an event to the resource. */ bool FileResource::addEvent(const KAEvent& event) { qCDebug(KALARM_LOG) << "FileResource::addEvent:" << event.id(); if (!isValid()) qCWarning(KALARM_LOG) << "FileResource::addEvent: Resource invalid!" << displayName(); else if (!isEnabled(CalEvent::EMPTY)) qCDebug(KALARM_LOG) << "FileResource::addEvent: Resource disabled!" << displayName(); else if (!isWritable(event.category())) qCWarning(KALARM_LOG) << "FileResource::addEvent: Calendar not writable" << displayName(); else if (doAddEvent(event)) { setUpdatedEvents({event}, false); if (mSettings->isEnabled(CalEvent::ACTIVE)) { // Add this event's command error to the settings. if (event.category() == CalEvent::ACTIVE && event.commandError() != KAEvent::CMD_NO_ERROR) { QHash cmdErrors = mSettings->commandErrors(); cmdErrors[event.id()] = event.commandError(); mSettings->setCommandErrors(cmdErrors); } } scheduleSave(); notifyUpdatedEvents(); return true; } return false; } /****************************************************************************** * Update an event in the resource. Its UID must be unchanged. */ bool FileResource::updateEvent(const KAEvent& event) { qCDebug(KALARM_LOG) << "FileResource::updateEvent:" << event.id(); if (!isValid()) qCWarning(KALARM_LOG) << "FileResource::updateEvent: Resource invalid!" << displayName(); else if (!isEnabled(CalEvent::EMPTY)) qCDebug(KALARM_LOG) << "FileResource::updateEvent: Resource disabled!" << displayName(); else if (!isWritable(event.category())) qCWarning(KALARM_LOG) << "FileResource::updateEvent: Calendar not writable" << displayName(); else if (doUpdateEvent(event)) { setUpdatedEvents({event}, false); // Update command errors held in the settings, if appropriate. if (mSettings->isEnabled(CalEvent::ACTIVE)) handleCommandErrorChange(event); scheduleSave(); notifyUpdatedEvents(); return true; } return false; } /****************************************************************************** * Delete an event from the resource. */ bool FileResource::deleteEvent(const KAEvent& event) { qCDebug(KALARM_LOG) << "FileResource::deleteEvent:" << event.id(); if (!isValid()) qCWarning(KALARM_LOG) << "FileResource::deleteEvent: Resource invalid!" << displayName(); else if (!isEnabled(CalEvent::EMPTY)) qCDebug(KALARM_LOG) << "FileResource::deleteEvent: Resource disabled!" << displayName(); else if (!isWritable(event.category())) qCWarning(KALARM_LOG) << "FileResource::deleteEvent: Calendar not writable" << displayName(); else if (doDeleteEvent(event)) { setDeletedEvents({event}); if (mSettings->isEnabled(CalEvent::ACTIVE)) { QHash cmdErrors = mSettings->commandErrors(); if (cmdErrors.remove(event.id())) mSettings->setCommandErrors(cmdErrors); } scheduleSave(); return true; } return false; } /****************************************************************************** * Save a command error change to the settings. */ void FileResource::handleCommandErrorChange(const KAEvent& event) { // Update command errors held in the settings, if appropriate. bool changed = false; QHash cmdErrors = mSettings->commandErrors(); if (event.category() != CalEvent::ACTIVE || event.commandError() == KAEvent::CMD_NO_ERROR) { if (cmdErrors.remove(event.id())) changed = true; } else if (event.category() == CalEvent::ACTIVE) { auto it = cmdErrors.find(event.id()); if (it == cmdErrors.end()) { cmdErrors[event.id()] = event.commandError(); changed = true; } else if (event.commandError() != it.value()) { it.value() = event.commandError(); changed = true; } } if (changed) { mSettings->setCommandErrors(cmdErrors); Resources::notifyEventUpdated(this, event); } } /****************************************************************************** * Update a resource to the current KAlarm storage format. */ bool FileResource::updateStorageFormat(Resource& res) { if (!res.is()) { qCCritical(KALARM_LOG) << "FileResource::updateStorageFormat: Error: Not a FileResource:" << res.displayName(); return false; } return resource(res)->updateStorageFmt(); } /****************************************************************************** * Return the resource's unique identifier for use in cache file names etc. */ QString FileResource::identifier() const { return QStringLiteral("FileResource%1").arg(mSettings->id() & ~IdFlag); } /****************************************************************************** * Find the compatibility of an existing calendar file. */ KACalendar::Compat FileResource::getCompatibility(const KCalendarCore::FileStorage::Ptr& fileStorage, int& version) { QString versionString; version = KACalendar::updateVersion(fileStorage, versionString); switch (version) { case KACalendar::IncompatibleFormat: return KACalendar::Incompatible; // calendar is not in KAlarm format, or is in a future format case KACalendar::CurrentFormat: return KACalendar::Current; // calendar is in the current format default: return KACalendar::Convertible; // calendar is in an out of date format } } // vim: et sw=4: diff --git a/src/resources/fileresourcecreator.cpp b/src/resources/fileresourcecreator.cpp index c93b0afc..bcead76e 100644 --- a/src/resources/fileresourcecreator.cpp +++ b/src/resources/fileresourcecreator.cpp @@ -1,157 +1,157 @@ /* * fileresourcecreator.cpp - interactively create a file system resource * Program: kalarm * Copyright © 2020 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 "fileresourcecreator.h" #include "resources/resources.h" #include "resources/fileresourcecalendarupdater.h" #include "resources/fileresourceconfigmanager.h" #include "resources/singlefileresourceconfigdialog.h" #include "lib/autoqpointer.h" #include "kalarm_debug.h" #include #include using namespace KAlarmCal; namespace { QString validateFileUrl(const QUrl&); } FileResourceCreator::FileResourceCreator(CalEvent::Type defaultType, QWidget* parent) : ResourceCreator(defaultType, parent) { } /****************************************************************************** * Create a new resource. The user will be prompted to enter its configuration. */ void FileResourceCreator::doCreateResource() { qCDebug(KALARM_LOG) << "FileResourceCreator::doCreateResource: Type:" << mDefaultType; const QList types = FileResourceConfigManager::storageTypes(); if (!types.isEmpty()) { QStringList typeDescs; for (ResourceType::StorageType t : types) typeDescs += Resource::storageTypeString(t); ResourceType::StorageType type = types[0]; if (types.count() > 1) { // Use AutoQPointer to guard against crash on application exit while // the dialogue is still open. It prevents double deletion (both on // deletion of ResourceSelector, and on return from this function). AutoQPointer dlg = new QInputDialog(mParent); dlg->setWindowTitle(i18nc("@title:window", "Calendar Configuration")); - dlg->setLabelText(i18nc("@info", "Select storage type of new calendar:")); + dlg->setLabelText(i18nc("@label:listbox", "Select storage type of new calendar:")); dlg->setOption(QInputDialog::UseListViewForComboBoxItems); dlg->setInputMode(QInputDialog::TextInput); dlg->setComboBoxEditable(false); dlg->setComboBoxItems(typeDescs); if (dlg->exec() != QDialog::Accepted) { deleteLater(); return; } const QString item = dlg->textValue(); for (int i = 0, iMax = typeDescs.count(); i < iMax; ++i) { if (typeDescs.at(i) == item) { type = types[i]; break; } } } switch (type) { case ResourceType::File: if (createSingleFileResource()) return; break; case ResourceType::Directory: // not currently intended to be implemented default: break; } } deleteLater(); // error result } /****************************************************************************** * Configure and create a single file resource. */ bool FileResourceCreator::createSingleFileResource() { // Use AutoQPointer to guard against crash on application exit while // the dialogue is still open. It prevents double deletion (both on // deletion of parent, and on return from this function). AutoQPointer dlg = new SingleFileResourceConfigDialog(true, mParent); dlg->setAlarmType(mDefaultType); // set default alarm type dlg->setUrlValidation(&validateFileUrl); if (dlg->exec() != QDialog::Accepted) return false; qCDebug(KALARM_LOG) << "FileResourceCreator::createSingleFileResource: Creating" << dlg->displayName(); FileResourceSettings::Ptr settings(new FileResourceSettings( FileResourceSettings::File, dlg->url(), dlg->alarmType(), dlg->displayName(), QColor(), dlg->alarmType(), CalEvent::EMPTY, dlg->readOnly())); Resource resource = FileResourceConfigManager::addResource(settings); // Update the calendar to the current KAlarm format if necessary, and // if the user agrees. FileResourceCalendarUpdater::updateToCurrentFormat(resource, true, mParent); Q_EMIT resourceAdded(resource, mDefaultType); return true; } namespace { /****************************************************************************** * Check whether the user-entered URL duplicates any existing resource. */ QString validateFileUrl(const QUrl& url) { // Ensure that the new resource doesn't use the same file or directory as // an existing file resource, to avoid duplicate processing. const QVector resources = Resources::allResources(); for (const Resource& res : resources) { if (res.location() == url) { const QString path = url.toDisplayString(QUrl::PrettyDecoded | QUrl::PreferLocalFile); qCWarning(KALARM_LOG) << "FileResourceCreator::validateFileUrl: Duplicate path for new resource:" << path; - return xi18nc("@info", "Error! The file is already used by an existing resource.", path); + return xi18nc("@info", "Error! The file %1 is already used by an existing resource.", path); } } return QString(); } } // vim: et sw=4: diff --git a/src/resources/fileresourcemigrator.cpp b/src/resources/fileresourcemigrator.cpp index b2a880d5..af6d7604 100644 --- a/src/resources/fileresourcemigrator.cpp +++ b/src/resources/fileresourcemigrator.cpp @@ -1,527 +1,527 @@ /* * fileresourcemigrator.cpp - migrates or creates KAlarm non-Akonadi resources * Program: kalarm * Copyright © 2011-2020 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 "fileresourcemigrator.h" #include "dirresourceimportdialog.h" #include "fileresourcecalendarupdater.h" #include "fileresourceconfigmanager.h" #include "resources.h" #include "calendarfunctions.h" #include "preferences.h" #include "lib/autoqpointer.h" #include "lib/desktop.h" #include "kalarm_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace KAlarmCal; namespace { const QString KALARM_RESOURCE(QStringLiteral("akonadi_kalarm_resource")); const QString KALARM_DIR_RESOURCE(QStringLiteral("akonadi_kalarm_dir_resource")); const Akonadi::Collection::Rights WritableRights = Akonadi::Collection::CanChangeItem | Akonadi::Collection::CanCreateItem | Akonadi::Collection::CanDeleteItem; bool readDirectoryResource(const QString& dirPath, CalEvent::Types alarmTypes, QHash>& events); } FileResourceMigrator* FileResourceMigrator::mInstance = nullptr; bool FileResourceMigrator::mCompleted = false; /****************************************************************************** * Constructor. */ FileResourceMigrator::FileResourceMigrator(QObject* parent) : QObject(parent) { } FileResourceMigrator::~FileResourceMigrator() { qCDebug(KALARM_LOG) << "~FileResourceMigrator"; mInstance = nullptr; } /****************************************************************************** * Create and return the unique FileResourceMigrator instance. */ FileResourceMigrator* FileResourceMigrator::instance() { if (!mInstance && !mCompleted) { // Check whether migration or default resource creation is actually needed. CalEvent::Types needed = CalEvent::ACTIVE | CalEvent::ARCHIVED | CalEvent::TEMPLATE; const QVector resources = Resources::allResources(); for (const Resource& resource : resources) { needed &= ~resource.alarmTypes(); if (!needed) { mCompleted = true; return mInstance; } } // Migration or default resource creation is required. mInstance = new FileResourceMigrator; } return mInstance; } /****************************************************************************** * Migrate old Akonadi or KResource calendars, and create default file system * resources. */ void FileResourceMigrator::execute() { if (mCompleted) { deleteLater(); return; } qCDebug(KALARM_LOG) << "FileResourceMigrator::execute"; // First, check whether any file system resources already exist, and if so, // find their alarm types. const QVector resources = Resources::allResources(); for (const Resource& resource : resources) mExistingAlarmTypes |= resource.alarmTypes(); if (mExistingAlarmTypes != CalEvent::EMPTY) { // Some file system resources already exist, so no migration is // required. Create any missing default file system resources. createDefaultResources(); } else { // There are no file system resources, so migrate any Akonadi resources. mMigratingAkonadi = true; connect(Akonadi::ServerManager::self(), &Akonadi::ServerManager::stateChanged, this, &FileResourceMigrator::migrateAkonadiResources); migrateAkonadiResources(Akonadi::ServerManager::state()); // Migration of Akonadi collections has now been initiated. On // completion, any missing default resources will be created. if (!mMigratingAkonadi) { // There are no Akonadi resources, so migrate any KResources alarm // calendars from pre-Akonadi versions of KAlarm. migrateKResources(); } } // Allow any calendar updater instances to complete and auto-delete. FileResourceCalendarUpdater::waitForCompletion(); } /****************************************************************************** * Called when the Akonadi server manager changes state. * Once it is running, migrate any Akonadi KAlarm resources. */ void FileResourceMigrator::migrateAkonadiResources(Akonadi::ServerManager::State state) { switch (state) { case Akonadi::ServerManager::Running: { qCDebug(KALARM_LOG) << "FileResourceMigrator::migrateAkonadiResources: initiated"; Akonadi::AttributeFactory::registerAttribute(); const Akonadi::AgentInstance::List agents = Akonadi::AgentManager::self()->instances(); // First, migrate KAlarm calendar file resources. // This will allow any KAlarm directory resources to be merged into // single file resources, if the user prefers that. for (const Akonadi::AgentInstance& agent : agents) { const QString type = agent.type().identifier(); if (type == KALARM_RESOURCE) { // Fetch the resource's collection to determine its alarm types Akonadi::CollectionFetchJob* job = new Akonadi::CollectionFetchJob(Akonadi::Collection::root(), Akonadi::CollectionFetchJob::FirstLevel); job->fetchScope().setResource(agent.identifier()); mFetchesPending << job; connect(job, &KJob::result, this, &FileResourceMigrator::collectionFetchResult); mMigrateKResources = false; // ignore KResources if Akonadi resources exist } } // Now migrate KAlarm directory resources, which must be merged // or converted into single file resources. for (const Akonadi::AgentInstance& agent : agents) { const QString type = agent.type().identifier(); if (type == KALARM_DIR_RESOURCE) { // Fetch the resource's collection to determine its alarm types Akonadi::CollectionFetchJob* job = new Akonadi::CollectionFetchJob(Akonadi::Collection::root(), Akonadi::CollectionFetchJob::FirstLevel); job->fetchScope().setResource(agent.identifier()); mFetchesPending << job; connect(job, &KJob::result, this, &FileResourceMigrator::collectionFetchResult); mMigrateKResources = false; // ignore KResources if Akonadi resources exist } } if (mFetchesPending.isEmpty()) mMigratingAkonadi = false; // there are no Akonadi resources to migrate break; } case Akonadi::ServerManager::Stopping: // Wait until the server has stopped, so that we can restart it. return; default: if (Akonadi::ServerManager::start()) return; // wait for the server to change to Running state // Can't start Akonadi, so give up trying to migrate. qCWarning(KALARM_LOG) << "FileResourceMigrator::migrateAkonadiResources: Failed to start Akonadi server"; mMigratingAkonadi = false; break; } disconnect(Akonadi::ServerManager::self(), nullptr, this, nullptr); if (mMigrateKResources) migrateKResources(); } /****************************************************************************** * Called when an Akonadi collection fetch job has completed. * Migrate the collection to a file system resource. */ void FileResourceMigrator::collectionFetchResult(KJob* j) { Akonadi::CollectionFetchJob* job = static_cast(j); const QString id = job->fetchScope().resource(); if (j->error()) qCCritical(KALARM_LOG) << "FileResourceMigrator::collectionFetchResult: CollectionFetchJob" << id << "error: " << j->errorString(); else { const Akonadi::Collection::List collections = job->collections(); if (collections.isEmpty()) qCCritical(KALARM_LOG) << "FileResourceMigrator::collectionFetchResult: No collections found for resource" << id; else { // Migrate this collection. const Akonadi::Collection& collection(collections[0]); const Akonadi::AgentInstance agent = Akonadi::AgentManager::self()->instance(collection.resource()); const QString resourceType = agent.type().identifier(); const bool readOnly = (collection.rights() & WritableRights) != WritableRights; const CalEvent::Types alarmTypes = CalEvent::types(collections[0].contentMimeTypes()); CalEvent::Types enabledTypes = CalEvent::EMPTY; CalEvent::Types standardTypes = CalEvent::EMPTY; QColor backgroundColour; if (collection.hasAttribute()) { const CollectionAttribute* attr = collection.attribute(); enabledTypes = attr->enabled(); standardTypes = attr->standard(); backgroundColour = attr->backgroundColor(); } if (resourceType == KALARM_RESOURCE) { qCDebug(KALARM_LOG) << "FileResourceMigrator: Creating resource" << collection.displayName() << ", alarm types:" << alarmTypes << ", standard types:" << standardTypes; FileResourceSettings::Ptr settings(new FileResourceSettings( FileResourceSettings::File, QUrl::fromUserInput(collection.remoteId(), QString(), QUrl::AssumeLocalFile), alarmTypes, collection.displayName(), backgroundColour, enabledTypes, standardTypes, readOnly)); Resource resource = FileResourceConfigManager::addResource(settings); // Don't delete the Akonadi resource in case it is wanted by any other application //Akonadi::AgentManager::self()->removeInstance(agent); // Update the calendar to the current KAlarm format if necessary, // and if the user agrees. FileResourceCalendarUpdater* updater = new FileResourceCalendarUpdater(resource, true, this); connect(updater, &QObject::destroyed, this, &FileResourceMigrator::checkIfComplete); updater->update(); // note that 'updater' will auto-delete when finished mExistingAlarmTypes |= alarmTypes; } else if (resourceType == KALARM_DIR_RESOURCE) { // Convert Akonadi directory resource to single file resources. // Use AutoQPointer to guard against crash on application exit while // the dialogue is still open. It prevents double deletion (both on // deletion of parent, and on return from this function). AutoQPointer dlg = new DirResourceImportDialog(collection.displayName(), collection.remoteId(), alarmTypes, Desktop::mainWindow()); if (dlg->exec() == QDialog::Accepted) { if (dlg) { QHash> events; readDirectoryResource(collection.remoteId(), alarmTypes, events); for (auto it = events.constBegin(); it != events.constEnd(); ++it) { const CalEvent::Type alarmType = it.key(); Resource resource; const ResourceId id = dlg->resourceId(alarmType); if (id >= 0) { // The directory resource's alarms are to be // imported into an existing resource. resource = Resources::resource(id); } else { const QUrl destUrl = dlg->url(alarmType); if (!destUrl.isValid()) continue; // this alarm type is not to be imported // The directory resource's alarms are to be // imported into a new resource. qCDebug(KALARM_LOG) << "FileResourceMigrator: Creating resource" << dlg->displayName(alarmType) << ", type:" << alarmType << ", standard:" << (bool)(standardTypes & alarmType); FileResourceSettings::Ptr settings(new FileResourceSettings( FileResourceSettings::File, destUrl, alarmType, dlg->displayName(alarmType), backgroundColour, enabledTypes, (standardTypes & alarmType), readOnly)); resource = FileResourceConfigManager::addResource(settings); } // Add directory events of the appropriate type to this resource. for (const KAEvent& event : qAsConst(it.value())) resource.addEvent(event); mExistingAlarmTypes |= alarmType; } } } } } } mFetchesPending.removeAll(job); if (mFetchesPending.isEmpty()) { // The alarm types of all collections have been found, so now create // any necessary default file system resources. mMigratingAkonadi = false; createDefaultResources(); } } /****************************************************************************** * Called when a CalendarUpdater has been destroyed. * If there are none left, and we have finished, delete this object. */ void FileResourceMigrator::checkIfComplete() { if (mCompleted && !FileResourceCalendarUpdater::pending()) deleteLater(); } /****************************************************************************** * Migrate old KResource calendars, and create default file system resources. */ void FileResourceMigrator::migrateKResources() { if (mExistingAlarmTypes == CalEvent::EMPTY) { // There are no file system resources, so migrate any KResources alarm // calendars from pre-Akonadi versions of KAlarm. qCDebug(KALARM_LOG) << "FileResourceMigrator::migrateKResources"; const QString configFile = QStandardPaths::writableLocation(QStandardPaths::ConfigLocation) + QStringLiteral("/kresources/alarms/stdrc"); const KConfig config(configFile, KConfig::SimpleConfig); // Fetch all the KResource identifiers which are actually in use const KConfigGroup group = config.group("General"); const QStringList keys = group.readEntry("ResourceKeys", QStringList()) + group.readEntry("PassiveResourceKeys", QStringList()); // Create a file system resource for each KResource id for (const QString& id : keys) { // Read the resource configuration parameters from the config const KConfigGroup configGroup = config.group(QLatin1String("Resource_") + id); const QString resourceType = configGroup.readEntry("ResourceType", QString()); const char* pathKey = nullptr; FileResourceSettings::StorageType storageType; if (resourceType == QLatin1String("file")) { storageType = FileResourceSettings::File; pathKey = "CalendarURL"; } else if (resourceType == QLatin1String("dir")) { storageType = FileResourceSettings::Directory; pathKey = "CalendarURL"; } else if (resourceType == QLatin1String("remote")) { storageType = FileResourceSettings::File; pathKey = "DownloadUrl"; } else { qCWarning(KALARM_LOG) << "CalendarCreator: Invalid resource type:" << resourceType; continue; // unknown resource type - can't convert } const QUrl url = QUrl::fromUserInput(configGroup.readPathEntry(pathKey, QString())); CalEvent::Type alarmType = CalEvent::EMPTY; switch (configGroup.readEntry("AlarmType", (int)0)) { case 1: alarmType = CalEvent::ACTIVE; break; case 2: alarmType = CalEvent::ARCHIVED; break; case 4: alarmType = CalEvent::TEMPLATE; break; default: qCWarning(KALARM_LOG) << "FileResourceMigrator::migrateKResources: Invalid alarm type for resource"; continue; } const QString name = configGroup.readEntry("ResourceName", QString()); const bool enabled = configGroup.readEntry("ResourceIsActive", false); const bool standard = configGroup.readEntry("Standard", false); qCDebug(KALARM_LOG) << "FileResourceMigrator::migrateKResources: Migrating:" << name << ", type=" << alarmType << ", path=" << url.toString(); FileResourceSettings::Ptr settings(new FileResourceSettings( storageType, url, alarmType, name, configGroup.readEntry("Color", QColor()), (enabled ? alarmType : CalEvent::EMPTY), (standard ? alarmType : CalEvent::EMPTY), configGroup.readEntry("ResourceIsReadOnly", true))); Resource resource = FileResourceConfigManager::addResource(settings); // Update the calendar to the current KAlarm format if necessary, // and if the user agrees. FileResourceCalendarUpdater* updater = new FileResourceCalendarUpdater(resource, true, this); connect(updater, &QObject::destroyed, this, &FileResourceMigrator::checkIfComplete); updater->update(); // note that 'updater' will auto-delete when finished mExistingAlarmTypes |= alarmType; } } // Create any necessary additional default file system resources. createDefaultResources(); } /****************************************************************************** * Create default file system resources for any alarm types not covered by * existing resources. Normally, this occurs on the first run of KAlarm, but if * resources have been deleted, it could occur on later runs. * If the default calendar files already exist, they will be used; otherwise * they will be created. */ void FileResourceMigrator::createDefaultResources() { qCDebug(KALARM_LOG) << "FileResourceMigrator::createDefaultResources"; if (!(mExistingAlarmTypes & CalEvent::ACTIVE)) - createCalendar(CalEvent::ACTIVE, QStringLiteral("calendar.ics"), i18nc("@info", "Active Alarms")); + createCalendar(CalEvent::ACTIVE, QStringLiteral("calendar.ics"), i18nc("@info/plain Name of a calendar", "Active Alarms")); if (!(mExistingAlarmTypes & CalEvent::ARCHIVED)) - createCalendar(CalEvent::ARCHIVED, QStringLiteral("expired.ics"), i18nc("@info", "Archived Alarms")); + createCalendar(CalEvent::ARCHIVED, QStringLiteral("expired.ics"), i18nc("@info/plain Name of a calendar", "Archived Alarms")); if (!(mExistingAlarmTypes & CalEvent::TEMPLATE)) - createCalendar(CalEvent::TEMPLATE, QStringLiteral("template.ics"), i18nc("@info", "Alarm Templates")); + createCalendar(CalEvent::TEMPLATE, QStringLiteral("template.ics"), i18nc("@info/plain Name of a calendar", "Alarm Templates")); mCompleted = true; checkIfComplete(); // delete this instance if everything is finished } /****************************************************************************** * Create a new default local file resource. * This is created as enabled, read-write, and standard for its alarm type. */ void FileResourceMigrator::createCalendar(CalEvent::Type alarmType, const QString& file, const QString& name) { const QUrl url = QUrl::fromLocalFile(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QLatin1Char('/') + file); qCDebug(KALARM_LOG) << "FileResourceMigrator: New:" << name << ", type=" << alarmType << ", path=" << url.toString(); FileResourceSettings::Ptr settings(new FileResourceSettings( FileResourceSettings::File, url, alarmType, name, QColor(), alarmType, CalEvent::EMPTY, false)); Resource resource = FileResourceConfigManager::addResource(settings); if (resource.failed()) { - QString errmsg = xi18nc("@info/plain", "Failed to create default calendar %1", name); - const QString locn = i18nc("@info File path or URL", "Location: %1", resource.displayLocation()); - errmsg = xi18nc("@info", "%1%2", errmsg, locn); + const QString errmsg = xi18nc("@info", "Failed to create default calendar %1" + "Location: %2", + name, resource.displayLocation()); Resources::notifyResourceMessage(resource.id(), ResourceType::MessageType::Error, errmsg, QString()); return; } // Update the calendar to the current KAlarm format if necessary, // and if the user agrees. FileResourceCalendarUpdater* updater = new FileResourceCalendarUpdater(resource, true, this); connect(updater, &QObject::destroyed, this, &FileResourceMigrator::checkIfComplete); updater->update(); // note that 'updater' will auto-delete when finished } namespace { /****************************************************************************** * Load and parse events from each file in a calendar directory. */ bool readDirectoryResource(const QString& dirPath, CalEvent::Types alarmTypes, QHash>& events) { if (dirPath.isEmpty()) return false; qCDebug(KALARM_LOG) << "FileResourceMigrator::readDirectoryResource:" << dirPath; const QDir dir(dirPath); if (!dir.exists()) return false; // Read and parse each file in turn QList files; QDirIterator it(dir); while (it.hasNext()) { it.next(); const QString file = it.fileName(); if (!file.isEmpty() && !file.startsWith(QLatin1Char('.')) && !file.endsWith(QLatin1Char('~')) && file != QLatin1String("WARNING_README.txt")) { const QString path = dirPath + QLatin1Char('/') + file; if (QFileInfo::exists(path) // a temporary file may no longer exist && QFileInfo(path).isFile()) { KAlarm::importCalendarFile(QUrl::fromLocalFile(path), alarmTypes, false, Desktop::mainWindow(), events); } } } return true; } } //#include "fileresourcemigrator.moc" // vim: et sw=4: diff --git a/src/resources/resourcedatamodelbase.cpp b/src/resources/resourcedatamodelbase.cpp index 9a9be80f..500a1f42 100644 --- a/src/resources/resourcedatamodelbase.cpp +++ b/src/resources/resourcedatamodelbase.cpp @@ -1,808 +1,808 @@ /* * resourcedatamodelbase.cpp - base for models containing calendars and events * Program: kalarm * Copyright © 2007-2020 David Jarvie * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "resourcedatamodelbase.h" #include "resources.h" #include "preferences.h" #include "lib/desktop.h" #include "lib/messagebox.h" #include "kalarm_debug.h" #include #include #include #include #include #include #include #include #include namespace { QString alarmTimeText(const DateTime& dateTime, char leadingZero = '\0'); QString timeToAlarmText(const DateTime& dateTime); } /*============================================================================= = Class: ResourceDataModelBase =============================================================================*/ ResourceDataModelBase* ResourceDataModelBase::mInstance = nullptr; QPixmap* ResourceDataModelBase::mTextIcon = nullptr; QPixmap* ResourceDataModelBase::mFileIcon = nullptr; QPixmap* ResourceDataModelBase::mCommandIcon = nullptr; QPixmap* ResourceDataModelBase::mEmailIcon = nullptr; QPixmap* ResourceDataModelBase::mAudioIcon = nullptr; QSize ResourceDataModelBase::mIconSize; /****************************************************************************** * Constructor. */ ResourceDataModelBase::ResourceDataModelBase() { if (!mTextIcon) { mTextIcon = new QPixmap(QIcon::fromTheme(QStringLiteral("dialog-information")).pixmap(16, 16)); mFileIcon = new QPixmap(QIcon::fromTheme(QStringLiteral("document-open")).pixmap(16, 16)); mCommandIcon = new QPixmap(QIcon::fromTheme(QStringLiteral("system-run")).pixmap(16, 16)); mEmailIcon = new QPixmap(QIcon::fromTheme(QStringLiteral("mail-unread")).pixmap(16, 16)); mAudioIcon = new QPixmap(QIcon::fromTheme(QStringLiteral("audio-x-generic")).pixmap(16, 16)); mIconSize = mTextIcon->size().expandedTo(mFileIcon->size()).expandedTo(mCommandIcon->size()).expandedTo(mEmailIcon->size()).expandedTo(mAudioIcon->size()); } } ResourceDataModelBase::~ResourceDataModelBase() { } /****************************************************************************** * Create a bulleted list of alarm types for insertion into .... */ QString ResourceDataModelBase::typeListForDisplay(CalEvent::Types alarmTypes) { QString list; if (alarmTypes & CalEvent::ACTIVE) - list += QLatin1String("") + i18nc("@info", "Active Alarms") + QLatin1String(""); + list += QLatin1String("") + i18nc("@item:intext", "Active Alarms") + QLatin1String(""); if (alarmTypes & CalEvent::ARCHIVED) - list += QLatin1String("") + i18nc("@info", "Archived Alarms") + QLatin1String(""); + list += QLatin1String("") + i18nc("@item:intext", "Archived Alarms") + QLatin1String(""); if (alarmTypes & CalEvent::TEMPLATE) - list += QLatin1String("") + i18nc("@info", "Alarm Templates") + QLatin1String(""); + list += QLatin1String("") + i18nc("@item:intext", "Alarm Templates") + QLatin1String(""); if (!list.isEmpty()) list = QLatin1String("") + list + QLatin1String(""); return list; } /****************************************************************************** * Return the read-only status tooltip for a collection, determined by the * read-write permissions and the KAlarm calendar format compatibility. * A null string is returned if the collection is read-write and compatible. */ QString ResourceDataModelBase::readOnlyTooltip(const Resource& resource) { switch (resource.compatibility()) { case KACalendar::Current: - return resource.readOnly() ? i18nc("@info", "Read-only") : QString(); + return resource.readOnly() ? i18nc("@item:intext Calendar status", "Read-only") : QString(); case KACalendar::Converted: case KACalendar::Convertible: - return i18nc("@info", "Read-only (old format)"); + return i18nc("@item:intext Calendar status", "Read-only (old format)"); case KACalendar::Incompatible: default: - return i18nc("@info", "Read-only (other format)"); + return i18nc("@item:intext Calendar status", "Read-only (other format)"); } } /****************************************************************************** * Return data for a column heading. */ QVariant ResourceDataModelBase::headerData(int section, Qt::Orientation orientation, int role, bool eventHeaders, bool& handled) { if (orientation == Qt::Horizontal) { handled = true; if (eventHeaders) { // Event column headers if (section < 0 || section >= ColumnCount) return QVariant(); if (role == Qt::DisplayRole || role == ColumnTitleRole) { switch (section) { case TimeColumn: return i18nc("@title:column", "Time"); case TimeToColumn: return i18nc("@title:column", "Time To"); case RepeatColumn: return i18nc("@title:column", "Repeat"); case ColourColumn: return (role == Qt::DisplayRole) ? QString() : i18nc("@title:column", "Color"); case TypeColumn: return (role == Qt::DisplayRole) ? QString() : i18nc("@title:column", "Type"); case TextColumn: return i18nc("@title:column", "Message, File or Command"); case TemplateNameColumn: return i18nc("@title:column Template name", "Name"); } } else if (role == Qt::WhatsThisRole) return whatsThisText(section); } else { // Calendar column headers if (section != 0) return QVariant(); if (role == Qt::DisplayRole) return i18nc("@title:column", "Calendars"); } } handled = false; return QVariant(); } /****************************************************************************** * Return whether resourceData() or eventData() handle a role. */ bool ResourceDataModelBase::roleHandled(int role) const { switch (role) { case Qt::WhatsThisRole: case Qt::ForegroundRole: case Qt::BackgroundRole: case Qt::DisplayRole: case Qt::TextAlignmentRole: case Qt::DecorationRole: case Qt::SizeHintRole: case Qt::AccessibleTextRole: case Qt::ToolTipRole: case ItemTypeRole: case ResourceIdRole: case BaseColourRole: case TimeDisplayRole: case SortRole: case StatusRole: case ValueRole: case EventIdRole: case ParentResourceIdRole: case EnabledRole: case AlarmActionsRole: case AlarmSubActionRole: case CommandErrorRole: return true; default: return false; } } /****************************************************************************** * Return the data for a given role, for a specified resource. */ QVariant ResourceDataModelBase::resourceData(int& role, const Resource& resource, bool& handled) const { if (roleHandled(role)) // Ensure that roleHandled() is coded correctly { handled = true; switch (role) { case Qt::DisplayRole: return resource.displayName(); case BaseColourRole: role = Qt::BackgroundRole; // use base model background colour break; case Qt::BackgroundRole: { const QColor colour = resource.backgroundColour(); if (colour.isValid()) return colour; break; // use base model background colour } case Qt::ForegroundRole: return resource.foregroundColour(); case Qt::ToolTipRole: return tooltip(resource, CalEvent::ACTIVE | CalEvent::ARCHIVED | CalEvent::TEMPLATE); case ItemTypeRole: return static_cast(Type::Resource); case ResourceIdRole: return resource.id(); default: break; } } handled = false; return QVariant(); } /****************************************************************************** * Return the data for a given role, for a specified event. */ QVariant ResourceDataModelBase::eventData(int role, int column, const KAEvent& event, const Resource& resource, bool& handled) const { if (roleHandled(role)) // Ensure that roleHandled() is coded correctly { handled = true; bool calendarColour = false; switch (role) { case Qt::WhatsThisRole: return whatsThisText(column); case ItemTypeRole: return static_cast(Type::Event); default: break; } if (!event.isValid()) return QVariant(); switch (role) { case EventIdRole: return event.id(); case StatusRole: return event.category(); case AlarmActionsRole: return event.actionTypes(); case AlarmSubActionRole: return event.actionSubType(); case CommandErrorRole: return event.commandError(); default: break; } switch (column) { case TimeColumn: switch (role) { case Qt::BackgroundRole: calendarColour = true; break; case Qt::DisplayRole: if (event.expired()) return alarmTimeText(event.startDateTime(), '0'); return alarmTimeText(event.nextTrigger(KAEvent::DISPLAY_TRIGGER), '0'); case TimeDisplayRole: if (event.expired()) return alarmTimeText(event.startDateTime(), '~'); return alarmTimeText(event.nextTrigger(KAEvent::DISPLAY_TRIGGER), '~'); case Qt::TextAlignmentRole: return Qt::AlignRight; case SortRole: { DateTime due; if (event.expired()) due = event.startDateTime(); else due = event.nextTrigger(KAEvent::DISPLAY_TRIGGER); return due.isValid() ? due.effectiveKDateTime().toUtc().qDateTime() : QDateTime(QDate(9999,12,31), QTime(0,0,0)); } default: break; } break; case TimeToColumn: switch (role) { case Qt::BackgroundRole: calendarColour = true; break; case Qt::DisplayRole: if (event.expired()) return QString(); return timeToAlarmText(event.nextTrigger(KAEvent::DISPLAY_TRIGGER)); case Qt::TextAlignmentRole: return Qt::AlignRight; case SortRole: { if (event.expired()) return -1; const DateTime due = event.nextTrigger(KAEvent::DISPLAY_TRIGGER); const KADateTime now = KADateTime::currentUtcDateTime(); if (due.isDateOnly()) return now.date().daysTo(due.date()) * 1440; return (now.secsTo(due.effectiveKDateTime()) + 59) / 60; } } break; case RepeatColumn: switch (role) { case Qt::BackgroundRole: calendarColour = true; break; case Qt::DisplayRole: return repeatText(event); case Qt::TextAlignmentRole: return Qt::AlignHCenter; case SortRole: return repeatOrder(event); } break; case ColourColumn: switch (role) { case Qt::BackgroundRole: { const KAEvent::Actions type = event.actionTypes(); if (type & KAEvent::ACT_DISPLAY) return event.bgColour(); if (type == KAEvent::ACT_COMMAND) { if (event.commandError() != KAEvent::CMD_NO_ERROR) return QColor(Qt::red); } break; } case Qt::ForegroundRole: if (event.commandError() != KAEvent::CMD_NO_ERROR) { if (event.actionTypes() == KAEvent::ACT_COMMAND) return QColor(Qt::white); QColor colour = Qt::red; int r, g, b; event.bgColour().getRgb(&r, &g, &b); if (r > 128 && g <= 128 && b <= 128) colour = QColor(Qt::white); return colour; } break; case Qt::DisplayRole: if (event.commandError() != KAEvent::CMD_NO_ERROR) return QLatin1String("!"); break; case SortRole: { const unsigned i = (event.actionTypes() == KAEvent::ACT_DISPLAY) ? event.bgColour().rgb() : 0; return QStringLiteral("%1").arg(i, 6, 10, QLatin1Char('0')); } default: break; } break; case TypeColumn: switch (role) { case Qt::BackgroundRole: calendarColour = true; break; case Qt::DecorationRole: { QVariant v; v.setValue(*eventIcon(event)); return v; } case Qt::TextAlignmentRole: return Qt::AlignHCenter; case Qt::SizeHintRole: return mIconSize; case Qt::AccessibleTextRole: //TODO: Implement accessibility return QString(); case ValueRole: return static_cast(event.actionSubType()); case SortRole: return QStringLiteral("%1").arg(event.actionSubType(), 2, 10, QLatin1Char('0')); } break; case TextColumn: switch (role) { case Qt::BackgroundRole: calendarColour = true; break; case Qt::DisplayRole: case SortRole: return AlarmText::summary(event, 1); case Qt::ToolTipRole: return AlarmText::summary(event, 10); default: break; } break; case TemplateNameColumn: switch (role) { case Qt::BackgroundRole: calendarColour = true; break; case Qt::DisplayRole: return event.templateName(); case SortRole: return event.templateName().toUpper(); } break; default: break; } if (calendarColour) { const QColor colour = resource.backgroundColour(); if (colour.isValid()) return colour; } switch (role) { case Qt::ForegroundRole: if (!event.enabled()) return Preferences::disabledColour(); if (event.expired()) return Preferences::archivedColour(); break; // use the default for normal active alarms case Qt::ToolTipRole: // Show the last command execution error message switch (event.commandError()) { case KAEvent::CMD_ERROR: return i18nc("@info:tooltip", "Command execution failed"); case KAEvent::CMD_ERROR_PRE: return i18nc("@info:tooltip", "Pre-alarm action execution failed"); case KAEvent::CMD_ERROR_POST: return i18nc("@info:tooltip", "Post-alarm action execution failed"); case KAEvent::CMD_ERROR_PRE_POST: return i18nc("@info:tooltip", "Pre- and post-alarm action execution failed"); default: case KAEvent::CMD_NO_ERROR: break; } break; case EnabledRole: return event.enabled(); default: break; } } handled = false; return QVariant(); } /****************************************************************************** * Return a resource's tooltip text. The resource's enabled status is * evaluated for specified alarm types. */ QString ResourceDataModelBase::tooltip(const Resource& resource, CalEvent::Types types) const { const QString name = QLatin1Char('@') + resource.displayName(); // insert markers for stripping out name const QString type = QLatin1Char('@') + resource.storageTypeString(false); // file/directory/URL etc. const QString locn = resource.displayLocation(); const bool inactive = !(resource.enabledTypes() & types); const QString readonly = readOnlyTooltip(resource); const bool writable = readonly.isEmpty(); - const QString disabled = i18nc("@info", "Disabled"); + const QString disabled = i18nc("@item:intext Calendar status", "Disabled"); if (inactive || !writable) return xi18nc("@info:tooltip", "%1" "%2: %3" "%4", name, type, locn, (inactive ? disabled : readonly)); return xi18nc("@info:tooltip", "%1" "%2: %3", name, type, locn); } /****************************************************************************** * Return the repetition text. */ QString ResourceDataModelBase::repeatText(const KAEvent& event) { const QString repText = event.recurrenceText(true); return repText.isEmpty() ? event.repetitionText(true) : repText; } /****************************************************************************** * Return a string for sorting the repetition column. */ QString ResourceDataModelBase::repeatOrder(const KAEvent& event) { int repOrder = 0; int repInterval = 0; if (event.repeatAtLogin()) repOrder = 1; else { repInterval = event.recurInterval(); switch (event.recurType()) { case KARecurrence::MINUTELY: repOrder = 2; break; case KARecurrence::DAILY: repOrder = 3; break; case KARecurrence::WEEKLY: repOrder = 4; break; case KARecurrence::MONTHLY_DAY: case KARecurrence::MONTHLY_POS: repOrder = 5; break; case KARecurrence::ANNUAL_DATE: case KARecurrence::ANNUAL_POS: repOrder = 6; break; case KARecurrence::NO_RECUR: default: break; } } return QStringLiteral("%1%2").arg(static_cast('0' + repOrder)).arg(repInterval, 8, 10, QLatin1Char('0')); } /****************************************************************************** * Returns the QWhatsThis text for a specified column. */ QString ResourceDataModelBase::whatsThisText(int column) { switch (column) { case TimeColumn: return i18nc("@info:whatsthis", "Next scheduled date and time of the alarm"); case TimeToColumn: return i18nc("@info:whatsthis", "How long until the next scheduled trigger of the alarm"); case RepeatColumn: return i18nc("@info:whatsthis", "How often the alarm recurs"); case ColourColumn: return i18nc("@info:whatsthis", "Background color of alarm message"); case TypeColumn: return i18nc("@info:whatsthis", "Alarm type (message, file, command or email)"); case TextColumn: return i18nc("@info:whatsthis", "Alarm message text, URL of text file to display, command to execute, or email subject line"); case TemplateNameColumn: return i18nc("@info:whatsthis", "Name of the alarm template"); default: return QString(); } } /****************************************************************************** * Return the icon associated with an event's action. */ QPixmap* ResourceDataModelBase::eventIcon(const KAEvent& event) { switch (event.actionTypes()) { case KAEvent::ACT_EMAIL: return mEmailIcon; case KAEvent::ACT_AUDIO: return mAudioIcon; case KAEvent::ACT_COMMAND: return mCommandIcon; case KAEvent::ACT_DISPLAY: if (event.actionSubType() == KAEvent::FILE) return mFileIcon; Q_FALLTHROUGH(); // fall through to ACT_DISPLAY_COMMAND case KAEvent::ACT_DISPLAY_COMMAND: default: return mTextIcon; } } /****************************************************************************** * Display a message to the user. */ void ResourceDataModelBase::handleResourceMessage(ResourceType::MessageType type, const QString& message, const QString& details) { if (type == ResourceType::MessageType::Error) { qCDebug(KALARM_LOG) << "Resource Error!" << message << details; KAMessageBox::detailedError(Desktop::mainWindow(), message, details); } else if (type == ResourceType::MessageType::Info) { qCDebug(KALARM_LOG) << "Resource user message:" << message << details; // KMessageBox::informationList looks bad, so use our own formatting. const QString msg = details.isEmpty() ? message : message + QStringLiteral("\n\n") + details; KAMessageBox::information(Desktop::mainWindow(), msg); } } bool ResourceDataModelBase::isMigrationComplete() const { return mMigrationStatus == 1; } bool ResourceDataModelBase::isMigrating() const { return mMigrationStatus == 0; } void ResourceDataModelBase::setMigrationInitiated(bool started) { mMigrationStatus = (started ? 0 : -1); } void ResourceDataModelBase::setMigrationComplete() { mMigrationStatus = 1; if (mCreationStatus) Resources::notifyResourcesCreated(); } void ResourceDataModelBase::setCalendarsCreated() { mCreationStatus = true; if (mMigrationStatus == 1) Resources::notifyResourcesCreated(); } namespace { /****************************************************************************** * Return the alarm time text in the form "date time". * Parameters: * dateTime = the date/time to format. * leadingZero = the character to represent a leading zero, or '\0' for no leading zeroes. */ QString alarmTimeText(const DateTime& dateTime, char leadingZero) { // Whether the date and time contain leading zeroes. static bool leadingZeroesChecked = false; static QString dateFormat; // date format for current locale static QString timeFormat; // time format for current locale static QString timeFullFormat; // time format with leading zero, if different from 'timeFormat' static int hourOffset = 0; // offset within time string to the hour if (!dateTime.isValid()) return i18nc("@info Alarm never occurs", "Never"); if (!leadingZeroesChecked && QApplication::isLeftToRight()) // don't try to align right-to-left languages { // Check whether the day number and/or hour have no leading zeroes, if // they are at the start of the date/time. If no leading zeroes, they // will need to be padded when displayed, so that displayed dates/times // can be aligned with each other. // Note that if leading zeroes are not included in other components, no // alignment will be attempted. QLocale locale; { // Check the date format. 'dd' provides leading zeroes; single 'd' // provides no leading zeroes. dateFormat = locale.dateFormat(QLocale::ShortFormat); } { // Check the time format. // Remove all but hours, minutes and AM/PM, since alarms are on minute // boundaries. Preceding separators are also removed. timeFormat = locale.timeFormat(QLocale::ShortFormat); for (int del = 0, predel = 0, c = 0; c < timeFormat.size(); ++c) { char ch = timeFormat.at(c).toLatin1(); switch (ch) { case 'H': case 'h': case 'm': case 'a': case 'A': if (predel == 1) { timeFormat.remove(del, c - del); c = del; } del = c + 1; // start deleting from the next character if ((ch == 'A' && del < timeFormat.size() && timeFormat.at(del).toLatin1() == 'P') || (ch == 'a' && del < timeFormat.size() && timeFormat.at(del).toLatin1() == 'p')) ++c, ++del; predel = -1; break; case 's': case 'z': case 't': timeFormat.remove(del, c + 1 - del); c = del - 1; if (!predel) predel = 1; break; default: break; } } // 'HH' and 'hh' provide leading zeroes; single 'H' or 'h' provide no // leading zeroes. int i = timeFormat.indexOf(QRegExp(QLatin1String("[hH]"))); int first = timeFormat.indexOf(QRegExp(QLatin1String("[hHmaA]"))); if (i >= 0 && i == first && (i == timeFormat.size() - 1 || timeFormat.at(i) != timeFormat.at(i + 1))) { timeFullFormat = timeFormat; timeFullFormat.insert(i, timeFormat.at(i)); // Find index to hour in formatted times const QTime t(1,30,30); const QString nozero = t.toString(timeFormat); const QString zero = t.toString(timeFullFormat); for (int i = 0; i < nozero.size(); ++i) if (nozero[i] != zero[i]) { hourOffset = i; break; } } } } leadingZeroesChecked = true; const KADateTime kdt = dateTime.effectiveKDateTime().toTimeSpec(Preferences::timeSpec()); QString dateTimeText = kdt.date().toString(dateFormat); if (!dateTime.isDateOnly() || kdt.utcOffset() != dateTime.utcOffset()) { // Display the time of day if it's a date/time value, or if it's // a date-only value but it's in a different time zone dateTimeText += QLatin1Char(' '); bool useFullFormat = leadingZero && !timeFullFormat.isEmpty(); QString timeText = kdt.time().toString(useFullFormat ? timeFullFormat : timeFormat); if (useFullFormat && leadingZero != '0' && timeText.at(hourOffset) == QLatin1Char('0')) timeText[hourOffset] = leadingZero; dateTimeText += timeText; } return dateTimeText + QLatin1Char(' '); } /****************************************************************************** * Return the time-to-alarm text. */ QString timeToAlarmText(const DateTime& dateTime) { if (!dateTime.isValid()) return i18nc("@info Alarm never occurs", "Never"); KADateTime now = KADateTime::currentUtcDateTime(); if (dateTime.isDateOnly()) { int days = now.date().daysTo(dateTime.date()); // xgettext: no-c-format return i18nc("@info n days", "%1d", days); } int mins = (now.secsTo(dateTime.effectiveKDateTime()) + 59) / 60; if (mins < 0) return QString(); char minutes[3] = "00"; minutes[0] = (mins%60) / 10 + '0'; minutes[1] = (mins%60) % 10 + '0'; if (mins < 24*60) return i18nc("@info hours:minutes", "%1:%2", mins/60, QLatin1String(minutes)); // If we render a day count, then we zero-pad the hours, to make the days line up and be more scanable. int hrs = mins / 60; char hours[3] = "00"; hours[0] = (hrs%24) / 10 + '0'; hours[1] = (hrs%24) % 10 + '0'; int days = hrs / 24; return i18nc("@info days hours:minutes", "%1d %2:%3", days, QLatin1String(hours), QLatin1String(minutes)); } } // vim: et sw=4: diff --git a/src/resources/resources.cpp b/src/resources/resources.cpp index 7c1570c5..d21faf5f 100644 --- a/src/resources/resources.cpp +++ b/src/resources/resources.cpp @@ -1,625 +1,625 @@ /* * resource.cpp - generic class containing an alarm calendar resource * Program: kalarm * Copyright © 2019-2020 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 "resources.h" #include "resource.h" #include "resourcedatamodelbase.h" #include "resourcemodel.h" #include "resourceselectdialog.h" #include "preferences.h" #include "lib/autoqpointer.h" #include "lib/messagebox.h" #include "kalarm_debug.h" #include Resources* Resources::mInstance {nullptr}; // Copy of all ResourceType instances with valid ID, wrapped in the Resource // container which manages the instance. QHash Resources::mResources; bool Resources::mCreated {false}; bool Resources::mPopulated {false}; Resources* Resources::instance() { if (!mInstance) mInstance = new Resources; return mInstance; } Resources::Resources() { qRegisterMetaType(); } Resources::~Resources() { qCDebug(KALARM_LOG) << "Resources::~Resources"; for (auto it = mResources.begin(); it != mResources.end(); ++it) it.value().close(); } Resource Resources::resource(ResourceId id) { return mResources.value(id, Resource::null()); } /****************************************************************************** * Return the resources which are enabled for a specified alarm type. * If 'writable' is true, only writable resources are included. */ QVector Resources::enabledResources(CalEvent::Type type, bool writable) { const CalEvent::Types types = (type == CalEvent::EMPTY) ? CalEvent::ACTIVE | CalEvent::ARCHIVED | CalEvent::TEMPLATE : type; QVector result; for (auto it = mResources.constBegin(); it != mResources.constEnd(); ++it) { const Resource& res = it.value(); if (writable && !res.isWritable()) continue; if (res.enabledTypes() & types) result += res; } return result; } /****************************************************************************** * Return the standard resource for an alarm type. */ Resource Resources::getStandard(CalEvent::Type type) { Resources* manager = instance(); bool wantDefaultArchived = (type == CalEvent::ARCHIVED); Resource defaultArchived; for (auto it = manager->mResources.constBegin(); it != manager->mResources.constEnd(); ++it) { const Resource& res = it.value(); if (res.isWritable(type)) { if (res.configIsStandard(type)) return res; if (wantDefaultArchived) { if (defaultArchived.isValid()) wantDefaultArchived = false; // found two archived alarm resources else defaultArchived = res; // this is the first archived alarm resource } } } if (wantDefaultArchived && defaultArchived.isValid()) { // There is no resource specified as the standard archived alarm // resource, but there is exactly one writable archived alarm // resource. Set that resource to be the standard. defaultArchived.configSetStandard(CalEvent::ARCHIVED, true); return defaultArchived; } return Resource(); } /****************************************************************************** * Return whether a collection is the standard collection for a specified * mime type. */ bool Resources::isStandard(const Resource& resource, CalEvent::Type type) { // If it's for archived alarms, get and also set the standard resource if // necessary. if (type == CalEvent::ARCHIVED) return getStandard(type) == resource; return resource.configIsStandard(type) && resource.isWritable(type); } /****************************************************************************** * Return the alarm types for which a resource is the standard resource. */ CalEvent::Types Resources::standardTypes(const Resource& resource, bool useDefault) { if (!resource.isWritable()) return CalEvent::EMPTY; Resources* manager = instance(); auto it = manager->mResources.constFind(resource.id()); if (it == manager->mResources.constEnd()) return CalEvent::EMPTY; CalEvent::Types stdTypes = resource.configStandardTypes() & resource.enabledTypes(); if (useDefault) { // Also return alarm types for which this is the only resource. // Check if it is the only writable resource for these type(s). if (!(stdTypes & CalEvent::ARCHIVED) && resource.isEnabled(CalEvent::ARCHIVED)) { // If it's the only enabled archived alarm resource, set it as standard. getStandard(CalEvent::ARCHIVED); stdTypes = resource.configStandardTypes() & resource.enabledTypes(); } CalEvent::Types enabledNotStd = resource.enabledTypes() & ~stdTypes; if (enabledNotStd) { // The resource is enabled for type(s) for which it is not the standard. for (auto itr = manager->mResources.constBegin(); itr != manager->mResources.constEnd() && enabledNotStd; ++itr) { const Resource& res = itr.value(); if (res != resource && res.isWritable()) { const CalEvent::Types en = res.enabledTypes() & enabledNotStd; if (en) enabledNotStd &= ~en; // this resource handles the same alarm type } } } stdTypes |= enabledNotStd; } return stdTypes; } /****************************************************************************** * Set or clear the standard status for a resource. */ void Resources::setStandard(Resource& resource, CalEvent::Type type, bool standard) { if (!(type & resource.enabledTypes())) return; Resources* manager = instance(); auto it = manager->mResources.find(resource.id()); if (it == manager->mResources.end()) return; resource = it.value(); // just in case it's a different object! if (standard == resource.configIsStandard(type)) return; if (!standard) resource.configSetStandard(type, false); else if (resource.isWritable(type)) { // Clear the standard status for any other resources. for (auto itr = manager->mResources.begin(); itr != manager->mResources.end(); ++itr) { Resource& res = itr.value(); if (res != resource) res.configSetStandard(type, false); } resource.configSetStandard(type, true); } } /****************************************************************************** * Set the alarm types for which a resource the standard resource. */ void Resources::setStandard(Resource& resource, CalEvent::Types types) { types &= resource.enabledTypes(); Resources* manager = instance(); auto it = manager->mResources.find(resource.id()); if (it == manager->mResources.end()) return; resource = it.value(); // just in case it's a different object! if (types != resource.configStandardTypes() && (!types || resource.isWritable())) { if (types) { // Clear the standard status for any other resources. for (auto itr = manager->mResources.begin(); itr != manager->mResources.end(); ++itr) { Resource& res = itr.value(); if (res != resource) { const CalEvent::Types rtypes = res.configStandardTypes(); if (rtypes & types) res.configSetStandard(rtypes & ~types); } } } resource.configSetStandard(types); } } /****************************************************************************** * Find the resource to be used to store an event of a given type. * This will be the standard resource for the type, but if this is not valid, * the user will be prompted to select a resource. */ Resource Resources::destination(CalEvent::Type type, QWidget* promptParent, bool noPrompt, bool* cancelled) { if (cancelled) *cancelled = false; Resource 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 || (!Preferences::askResource() && standard.isValid())) return standard; // Prompt for which collection to use ResourceListModel* model = DataModel::createResourceListModel(promptParent); model->setFilterWritable(true); model->setFilterEnabled(true); model->setEventTypeFilter(type); model->useResourceColour(false); Resource res; switch (model->rowCount()) { case 0: break; case 1: res = model->resource(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 ResourceSelectDialog(model, promptParent); dlg->setWindowTitle(i18nc("@title:window", "Choose Calendar")); dlg->setDefaultResource(standard); if (dlg->exec()) res = dlg->selectedResource(); if (!res.isValid() && cancelled) *cancelled = true; } } return res; } /****************************************************************************** * Return whether all configured resources have been created. */ bool Resources::allCreated() { return instance()->mCreated; } /****************************************************************************** * Return whether all configured resources have been loaded at least once. */ bool Resources::allPopulated() { return instance()->mPopulated; } /****************************************************************************** * Return the resource which an event belongs to, provided its alarm type is * enabled. */ Resource Resources::resourceForEvent(const QString& eventId) { for (auto it = mResources.constBegin(); it != mResources.constEnd(); ++it) { const Resource& res = it.value(); if (res.containsEvent(eventId)) return res; } return Resource::null(); } /****************************************************************************** * Return the resource which an event belongs to, and the event, provided its * alarm type is enabled. */ Resource Resources::resourceForEvent(const QString& eventId, KAEvent& event) { for (auto it = mResources.constBegin(); it != mResources.constEnd(); ++it) { const Resource& res = it.value(); event = res.event(eventId); if (event.isValid()) return res; } if (mResources.isEmpty()) // otherwise, 'event' was set invalid in the loop event = KAEvent(); return Resource::null(); } /****************************************************************************** * Return the resource which has a given configuration identifier. */ Resource Resources::resourceForConfigName(const QString& configName) { for (auto it = mResources.constBegin(); it != mResources.constEnd(); ++it) { const Resource& res = it.value(); if (res.configName() == configName) return res; } return Resource::null(); } /****************************************************************************** * Called after a new resource has been created, when it has completed its * initialisation. */ void Resources::notifyNewResourceInitialised(Resource& res) { if (res.isValid()) Q_EMIT instance()->resourceAdded(res); } /****************************************************************************** * Called when all configured resources have been created for the first time. */ void Resources::notifyResourcesCreated() { qCDebug(KALARM_LOG) << "Resources::notifyResourcesCreated"; mCreated = true; Q_EMIT instance()->resourcesCreated(); checkResourcesPopulated(); } /****************************************************************************** * Called when a resource's events have been loaded. * Emits a signal if all collections have been populated. */ void Resources::notifyResourcePopulated(const ResourceType* res) { if (res) { Resource r = resource(res->id()); if (r.isValid()) Q_EMIT instance()->resourcePopulated(r); } // Check whether all resources have now loaded at least once. checkResourcesPopulated(); } /****************************************************************************** * Called to notify that a resource is about to be removed. */ void Resources::notifyResourceToBeRemoved(ResourceType* res) { if (res) { Resource r = resource(res->id()); if (r.isValid()) Q_EMIT instance()->resourceToBeRemoved(r); } } /****************************************************************************** * Called by a resource to notify that its settings have changed. * Emits the settingsChanged() signal. * If the resource is now read-only and was standard, clear its standard status. * If the resource has newly enabled alarm types, ensure that it doesn't * duplicate any existing standard setting. */ void Resources::notifySettingsChanged(ResourceType* res, ResourceType::Changes change, CalEvent::Types oldEnabled) { if (!res) return; Resource r = resource(res->id()); if (!r.isValid()) return; Resources* manager = instance(); if (change & ResourceType::Enabled) { ResourceType::Changes change = ResourceType::Enabled; // Find which alarm types (if any) have been newly enabled. const CalEvent::Types extra = res->enabledTypes() & ~oldEnabled; CalEvent::Types std = res->configStandardTypes(); const CalEvent::Types extraStd = std & extra; if (extraStd && res->isWritable()) { // Alarm type(s) have been newly enabled, and are set as standard. // Don't allow the resource to be set as standard for those types if // another resource is already the standard. CalEvent::Types disallowedStdTypes{}; for (auto it = manager->mResources.constBegin(); it != manager->mResources.constEnd(); ++it) { const Resource& resit = it.value(); if (resit.id() != res->id() && resit.isWritable()) { disallowedStdTypes |= extraStd & resit.configStandardTypes() & resit.enabledTypes(); if (extraStd == disallowedStdTypes) break; // all the resource's newly enabled standard types are disallowed } } if (disallowedStdTypes) { std &= ~disallowedStdTypes; res->configSetStandard(std); } } if (std) change |= ResourceType::Standard; } Q_EMIT manager->settingsChanged(r, change); if ((change & ResourceType::ReadOnly) && res->readOnly()) { qCDebug(KALARM_LOG) << "Resources::notifySettingsChanged:" << res->displayId() << "ReadOnly"; // A read-only resource can't be the default for any alarm type const CalEvent::Types std = standardTypes(r, false); if (std != CalEvent::EMPTY) { setStandard(r, CalEvent::EMPTY); bool singleType = true; QString msg; switch (std) { case CalEvent::ACTIVE: - msg = xi18n("The calendar %1 has been made read-only. " - "This was the default calendar for active alarms.", - res->displayName()); + msg = xi18nc("@info", "The calendar %1 has been made read-only. " + "This was the default calendar for active alarms.", + res->displayName()); break; case CalEvent::ARCHIVED: - msg = xi18n("The calendar %1 has been made read-only. " - "This was the default calendar for archived alarms.", - res->displayName()); + msg = xi18nc("@info", "The calendar %1 has been made read-only. " + "This was the default calendar for archived alarms.", + res->displayName()); break; case CalEvent::TEMPLATE: - msg = xi18n("The calendar %1 has been made read-only. " - "This was the default calendar for alarm templates.", - res->displayName()); + msg = xi18nc("@info", "The calendar %1 has been made read-only. " + "This was the default calendar for alarm templates.", + res->displayName()); 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.", - res->displayName(), ResourceDataModelBase::typeListForDisplay(std)); + "This was the default calendar for:%2" + "Please select new default calendars.", + res->displayName(), ResourceDataModelBase::typeListForDisplay(std)); singleType = false; break; } if (singleType) msg = xi18nc("@info", "%1Please select a new default calendar.", msg); notifyResourceMessage(res->id(), ResourceType::MessageType::Info, msg, QString()); } } } void Resources::notifyResourceMessage(ResourceType* res, ResourceType::MessageType type, const QString& message, const QString& details) { if (res) notifyResourceMessage(res->id(), type, message, details); } void Resources::notifyResourceMessage(ResourceId id, ResourceType::MessageType type, const QString& message, const QString& details) { if (resource(id).isValid()) Q_EMIT instance()->resourceMessage(type, message, details); } void Resources::notifyEventsAdded(ResourceType* res, const QList& events) { if (res) { Resource r = resource(res->id()); if (r.isValid()) Q_EMIT instance()->eventsAdded(r, events); } } void Resources::notifyEventUpdated(ResourceType* res, const KAEvent& event) { if (res) { Resource r = resource(res->id()); if (r.isValid()) Q_EMIT instance()->eventUpdated(r, event); } } void Resources::notifyEventsToBeRemoved(ResourceType* res, const QList& events) { if (res) { Resource r = resource(res->id()); if (r.isValid()) Q_EMIT instance()->eventsToBeRemoved(r, events); } } bool Resources::addResource(ResourceType* instance, Resource& resource) { if (!instance || instance->id() < 0) { // Instance is invalid - return an invalid resource. delete instance; resource = Resource::null(); return false; } auto it = mResources.constFind(instance->id()); if (it != mResources.constEnd()) { // Instance ID already exists - return the existing resource. delete instance; resource = it.value(); return false; } // Add a new resource. resource = Resource(instance); mResources[instance->id()] = resource; return true; } void Resources::removeResource(ResourceId id) { if (mResources.remove(id) > 0) Q_EMIT instance()->resourceRemoved(id); } /****************************************************************************** * To be called when a resource has been created or loaded. * If all resources have now loaded for the first time, emit signal. */ void Resources::checkResourcesPopulated() { if (!mPopulated && mCreated) { // Check whether all resources have now loaded at least once. for (auto it = mResources.constBegin(); it != mResources.constEnd(); ++it) { const Resource& res = it.value(); if (res.isEnabled(CalEvent::EMPTY) && !res.isPopulated()) return; } mPopulated = true; Q_EMIT instance()->resourcesPopulated(); } } #if 0 /****************************************************************************** * Return whether one or all enabled collections have been loaded. */ bool Resources::isPopulated(ResourceId id) { if (id >= 0) { const Resource res = resource(id); return res.isPopulated() || res.enabledTypes() == CalEvent::EMPTY; } for (auto it = mResources.constBegin(); it != mResources.constEnd(); ++it) { const Resource& res = it.value(); if (!res.isPopulated() && res.enabledTypes() != CalEvent::EMPTY) return false; } return true; } #endif // vim: et sw=4: diff --git a/src/resources/resourceselectdialog.cpp b/src/resources/resourceselectdialog.cpp index 4208adc0..1d90338d 100644 --- a/src/resources/resourceselectdialog.cpp +++ b/src/resources/resourceselectdialog.cpp @@ -1,105 +1,105 @@ /* * resourceselectdialog.cpp - Resource selection dialog * Program: kalarm - * Copyright © 2019 David Jarvie + * Copyright © 2019-2020 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 "resourceselectdialog.h" #include "resourcemodel.h" #include "lib/config.h" #include #include #include #include #include #include namespace { static const char DialogName[] = "ResourceSelectDialog"; } ResourceSelectDialog::ResourceSelectDialog(ResourceListModel* model, QWidget* parent) : QDialog(parent) , mModel(model) { QVBoxLayout* layout = new QVBoxLayout(this); QLineEdit* filterEdit = new QLineEdit(this); filterEdit->setClearButtonEnabled(true); - filterEdit->setPlaceholderText(i18nc("@info A prompt for user to enter what to search for", "Search")); + filterEdit->setPlaceholderText(xi18nc("@info A prompt for user to enter what to search for", "Search")); layout->addWidget(filterEdit); mResourceList = new QListView(this); mResourceList->setModel(model); layout->addWidget(mResourceList); mButtonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this); connect(mButtonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); connect(mButtonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); layout->addWidget(mButtonBox); mButtonBox->button(QDialogButtonBox::Ok)->setEnabled(false); connect(filterEdit, &QLineEdit::textChanged, this, &ResourceSelectDialog::slotFilterText); connect(mResourceList->selectionModel(), &QItemSelectionModel::selectionChanged, this, &ResourceSelectDialog::slotSelectionChanged); connect(mResourceList, &QAbstractItemView::doubleClicked, this, &ResourceSelectDialog::slotDoubleClicked); QSize s; if (Config::readWindowSize(DialogName, s)) resize(s); } ResourceSelectDialog::~ResourceSelectDialog() { Config::writeWindowSize(DialogName, size()); } void ResourceSelectDialog::setDefaultResource(const Resource& resource) { const QModelIndex index = mModel->resourceIndex(resource); mResourceList->selectionModel()->select(index, QItemSelectionModel::SelectCurrent); } Resource ResourceSelectDialog::selectedResource() const { const QModelIndexList indexes = mResourceList->selectionModel()->selectedRows(); if (indexes.isEmpty()) return Resource(); return mModel->resource(indexes[0].row()); } void ResourceSelectDialog::slotFilterText(const QString& text) { mModel->setFilterText(text); } void ResourceSelectDialog::slotDoubleClicked() { if (!mResourceList->selectionModel()->selectedIndexes().isEmpty()) accept(); } void ResourceSelectDialog::slotSelectionChanged() { mButtonBox->button(QDialogButtonBox::Ok)->setEnabled(!mResourceList->selectionModel()->selectedIndexes().isEmpty()); } // vim: et sw=4: diff --git a/src/resources/resourcetype.cpp b/src/resources/resourcetype.cpp index 597d0416..9deebd64 100644 --- a/src/resources/resourcetype.cpp +++ b/src/resources/resourcetype.cpp @@ -1,354 +1,354 @@ /* * resourcetype.cpp - base class for an alarm calendar resource type * Program: kalarm * Copyright © 2019-2020 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 "resourcetype.h" #include "resources.h" #include "preferences.h" #include "kalarm_debug.h" #include #include #include ResourceType::ResourceType(ResourceId id) : mId(id) { } ResourceType::~ResourceType() { } bool ResourceType::failed() const { return mFailed || !isValid(); } ResourceId ResourceType::displayId() const { return id(); } bool ResourceType::isEnabled(CalEvent::Type type) const { return (type == CalEvent::EMPTY) ? enabledTypes() : enabledTypes() & type; } bool ResourceType::isWritable(CalEvent::Type type) const { return writableStatus(type) == 1; } /****************************************************************************** * Return the foreground colour for displaying a resource, based on the alarm * types which it contains, and on whether it is fully writable. */ QColor ResourceType::foregroundColour(CalEvent::Types types) const { if (types == CalEvent::EMPTY) types = alarmTypes(); else types &= alarmTypes(); // Find the highest priority alarm type. // Note that resources currently only contain a single alarm type. CalEvent::Type type; QColor colour; // default to invalid colour if (types & CalEvent::ACTIVE) { type = CalEvent::ACTIVE; colour = KColorScheme(QPalette::Active).foreground(KColorScheme::NormalText).color(); } else if (types & CalEvent::ARCHIVED) { type = CalEvent::ARCHIVED; colour = Preferences::archivedColour(); } else if (types & CalEvent::TEMPLATE) { type = CalEvent::TEMPLATE; colour = KColorScheme(QPalette::Active).foreground(KColorScheme::LinkText).color(); } else type = CalEvent::EMPTY; if (colour.isValid() && !isWritable(type)) return KColorUtils::lighten(colour, 0.2); return colour; } bool ResourceType::isCompatible() const { return compatibility() == KACalendar::Current; } KACalendar::Compat ResourceType::compatibility() const { QString versionString; return compatibilityVersion(versionString); } /****************************************************************************** * Return all events belonging to this resource, for enabled alarm types. */ QList ResourceType::events() const { // Remove any events with disabled alarm types. const CalEvent::Types types = enabledTypes(); QList events; for (auto it = mEvents.begin(); it != mEvents.end(); ++it) { if (it.value().category() & types) events += it.value(); } return events; } /****************************************************************************** * Return the event with the given ID, provided its alarm type is enabled for * the resource. */ KAEvent ResourceType::event(const QString& eventId) const { auto it = mEvents.constFind(eventId); if (it != mEvents.constEnd() && (it.value().category() & enabledTypes())) return it.value(); return KAEvent(); } /****************************************************************************** * Return whether the resource contains the event whose ID is given, and if the * event's alarm type is enabled for the resource. */ bool ResourceType::containsEvent(const QString& eventId) const { auto it = mEvents.constFind(eventId); return it != mEvents.constEnd() && (it.value().category() & enabledTypes()); } void ResourceType::notifyDeletion() { mBeingDeleted = true; } bool ResourceType::isBeingDeleted() const { return mBeingDeleted; } bool ResourceType::addResource(ResourceType* type, Resource& resource) { return Resources::addResource(type, resource); } void ResourceType::removeResource(ResourceId id) { // Set the resource instance invalid, to ensure that any other references // to it now see an invalid resource. Resource res = Resources::resource(id); ResourceType* tres = res.resource(); if (tres) tres->mId = -1; // set the resource instance invalid Resources::removeResource(id); } /****************************************************************************** * To be called when the resource has loaded, to update the list of loaded * events for the resource. * Added, updated and deleted events are notified, only for enabled alarm types. */ void ResourceType::setLoadedEvents(QHash& newEvents) { qCDebug(KALARM_LOG) << "ResourceType::setLoadedEvents: count" << newEvents.count(); const CalEvent::Types types = enabledTypes(); // Replace existing events with the new ones, and find events which no // longer exist. QStringList eventsToDelete; QList eventsToNotifyDelete; QVector iteratorsToDelete; for (auto it = mEvents.begin(); it != mEvents.end(); ++it) { const QString& id = it.key(); auto newit = newEvents.find(id); if (newit == newEvents.end()) { eventsToDelete << id; if (it.value().category() & types) eventsToNotifyDelete << it.value(); // this event no longer exists } else { KAEvent& event = it.value(); bool changed = !event.compare(newit.value(), KAEvent::Compare::Id | KAEvent::Compare::CurrentState); event = newit.value(); // update existing event newEvents.erase(newit); if (changed && (event.category() & types)) Resources::notifyEventUpdated(this, event); } } // Delete events which no longer exist. Resources::notifyEventsToBeRemoved(this, eventsToNotifyDelete); for (const QString& id : qAsConst(eventsToDelete)) mEvents.remove(id); // Add new events. for (auto newit = newEvents.begin(); newit != newEvents.end(); ) { mEvents[newit.key()] = newit.value(); if (newit.value().category() & types) ++newit; else newit = newEvents.erase(newit); // remove disabled event from notification list } Resources::notifyEventsAdded(this, newEvents.values()); newEvents.clear(); setLoaded(true); } /****************************************************************************** * To be called when events have been created or updated, to amend them in the * resource's list. */ void ResourceType::setUpdatedEvents(const QList& events, bool notify) { const CalEvent::Types types = enabledTypes(); mEventsAdded.clear(); mEventsUpdated.clear(); for (const KAEvent& event : events) { auto it = mEvents.find(event.id()); if (it == mEvents.end()) { mEvents[event.id()] = event; if (event.category() & types) mEventsAdded += event; } else { KAEvent& ev = it.value(); bool changed = !ev.compare(event, KAEvent::Compare::Id | KAEvent::Compare::CurrentState); ev = event; // update existing event if (changed && (event.category() & types)) { if (notify) Resources::notifyEventUpdated(this, event); else mEventsUpdated += event; } } } if (notify && !mEventsAdded.isEmpty()) Resources::notifyEventsAdded(this, mEventsAdded); } /****************************************************************************** * Notifies added and updated events, after setUpdatedEvents() was called with * notify = false. */ void ResourceType::notifyUpdatedEvents() { for (const KAEvent& event : qAsConst(mEventsUpdated)) Resources::notifyEventUpdated(this, event); mEventsUpdated.clear(); if (!mEventsAdded.isEmpty()) Resources::notifyEventsAdded(this, mEventsAdded); mEventsAdded.clear(); } /****************************************************************************** * To be called when events have been deleted, to delete them from the * resource's list. */ void ResourceType::setDeletedEvents(const QList& events) { const CalEvent::Types types = enabledTypes(); QStringList eventsToDelete; QList eventsToNotify; for (const KAEvent& event : events) { QHash::iterator it = mEvents.find(event.id()); if (it != mEvents.end()) { eventsToDelete += event.id(); if (event.category() & types) eventsToNotify += event; } } if (!eventsToNotify.isEmpty()) Resources::notifyEventsToBeRemoved(this, eventsToNotify); for (const QString& id : eventsToDelete) mEvents.remove(id); } void ResourceType::setLoaded(bool loaded) const { if (loaded != mLoaded) { mLoaded = loaded; if (loaded) Resources::notifyResourcePopulated(this); } } void ResourceType::setFailed() { mFailed = true; } QString ResourceType::storageTypeString(StorageType type) { switch (type) { case File: case Directory: return storageTypeStr(true, (type == File), true); default: return QString(); } } QString ResourceType::storageTypeStr(bool description, bool file, bool local) { if (description) - return file ? i18nc("@info", "KAlarm Calendar File") : i18nc("@info", "KAlarm Calendar Directory"); - return (file && local) ? i18nc("@info", "File") - : (file && !local) ? i18nc("@info", "URL") - : (!file && local) ? i18nc("@info Directory in filesystem", "Directory") + return file ? i18nc("@item:inlistbox", "KAlarm Calendar File") : i18nc("@item:inlistbox", "KAlarm Calendar Directory"); + return (file && local) ? i18nc("@item:intext What a resource is stored in", "File") + : (file && !local) ? i18nc("@item:intext What a resource is stored in", "URL") + : (!file && local) ? i18nc("@item:intext What a resource is stored in (directory in filesystem)", "Directory") : QString(); } ResourceType* ResourceType::data(Resource& resource) { return resource.mResource.data(); } const ResourceType* ResourceType::data(const Resource& resource) { return resource.mResource.data(); } // vim: et sw=4: diff --git a/src/resources/singlefileresource.cpp b/src/resources/singlefileresource.cpp index 1d80d125..19fd8a8f 100644 --- a/src/resources/singlefileresource.cpp +++ b/src/resources/singlefileresource.cpp @@ -1,831 +1,830 @@ /* * singlefileresource.cpp - calendar resource held in a single file * Program: kalarm * Partly based on ICalResourceBase and SingleFileResource in kdepim-runtime. * Copyright © 2009-2020 David Jarvie * Copyright (c) 2008 Bertjan Broeksema * Copyright (c) 2008 Volker Krause * Copyright (c) 2006 Till Adam * * This library is free software; you can redistribute it and/or modify it * under the terms of the GNU Library General Public License as published by * the Free Software Foundation; either version 2 of the License, or (at your * option) any later version. * * This library 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 Library General Public * License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to the * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * 02110-1301, USA. */ #include "singlefileresource.h" #include "resources.h" #include "kalarm_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace KCalendarCore; using namespace KAlarmCal; Q_DECLARE_METATYPE(QEventLoopLocker*) namespace { -const QString reloadMessage = i18n("Calendar '%1' has been reloaded."); const int SAVE_TIMER_DELAY = 1000; // 1 second } Resource SingleFileResource::create(FileResourceSettings* settings) { if (!settings || !settings->isValid()) return Resource::null(); // return invalid Resource Resource resource = Resources::resource(settings->id()); if (!resource.isValid()) { // A resource with this ID doesn't exist, so create a new resource. addResource(new SingleFileResource(settings), resource); } return resource; } /****************************************************************************** * Constructor. */ SingleFileResource::SingleFileResource(FileResourceSettings* settings) : FileResource(settings) , mSaveTimer(new QTimer(this)) { qCDebug(KALARM_LOG) << "SingleFileResource: Starting" << mSettings->displayName(); if (!load()) setFailed(); else { // Monitor local file changes (but not cache files). connect(KDirWatch::self(), &KDirWatch::dirty, this, &SingleFileResource::localFileChanged); connect(KDirWatch::self(), &KDirWatch::created, this, &SingleFileResource::localFileChanged); mSaveTimer->setSingleShot(true); mSaveTimer->setInterval(SAVE_TIMER_DELAY); connect(mSaveTimer, &QTimer::timeout, this, &SingleFileResource::slotSave); } } /****************************************************************************** * Destructor. */ SingleFileResource::~SingleFileResource() { qCDebug(KALARM_LOG) << "SingleFileResource::~SingleFileResource" << displayName(); close(); } bool SingleFileResource::readOnly() const { if (mFileReadOnly) return true; return FileResource::readOnly(); } /****************************************************************************** * Return whether the resource can be written to. */ int SingleFileResource::writableStatus(CalEvent::Type type) const { if (mFileReadOnly) return -1; return FileResource::writableStatus(type); } /****************************************************************************** * Update the backend calendar storage format to the current KAlarm format. */ bool SingleFileResource::updateStorageFmt() { if (failed() || readOnly() || enabledTypes() == CalEvent::EMPTY) return false; if (!mFileStorage) { qCCritical(KALARM_LOG) << "SingleFileResource::updateStorageFormat:" << displayId() << "Calendar not open"; return false; } QString versionString; if (KACalendar::updateVersion(mFileStorage, versionString) != KACalendar::CurrentFormat) { qCWarning(KALARM_LOG) << "SingleFileResource::updateStorageFormat:" << displayId() << "Cannot convert calendar to current storage format"; return false; } qCDebug(KALARM_LOG) << "SingleFileResource::updateStorageFormat: Updating storage for" << mSettings->displayName(); mCompatibility = KACalendar::Current; mVersion = KACalendar::CurrentFormat; save(true, true); mSettings->setUpdateFormat(false); mSettings->save(); return true; } /****************************************************************************** * Re-read the file, ignoring saved hash or cache. */ bool SingleFileResource::reload() { mCurrentHash.clear(); // ensure that load() re-reads the file mLoadedEvents.clear(); if (!isEnabled(CalEvent::EMPTY)) return false; qCDebug(KALARM_LOG) << "SingleFileResource::reload()" << displayName(); // If it has been modified since its last load, write it back to save the changes. if (mCalendar && mCalendar->isModified() && !mSaveUrl.isEmpty() && isWritable(CalEvent::EMPTY)) { if (!save()) return false; // No need to load again, since that would re-read what has just been saved. return true; } return load(); } bool SingleFileResource::isSaving() const { return mUploadJob; } /****************************************************************************** * Read the backend file and fetch its calendar data. */ int SingleFileResource::doLoad(QHash& newEvents, bool readThroughCache, QString& errorMessage) { newEvents.clear(); if (mDownloadJob) { qCWarning(KALARM_LOG) << "SingleFileResource::load:" << displayId() << "Another download is still in progress"; - errorMessage = i18n("A previous load is still in progress."); + errorMessage = i18nc("@info", "A previous load is still in progress."); return -1; } if (mUploadJob) { qCWarning(KALARM_LOG) << "SingleFileResource::load:" << displayId() << "Another file upload is still in progress."; - errorMessage = i18n("A previous save is still in progress."); + errorMessage = i18nc("@info", "A previous save is still in progress."); return -1; } const bool isSettingsLocalFile = mSettings->url().isLocalFile(); const QString settingsLocalFileName = isSettingsLocalFile ? mSettings->url().toLocalFile() : QString(); if (isSettingsLocalFile) KDirWatch::self()->removeFile(settingsLocalFileName); mSaveUrl = mSettings->url(); if (mCurrentHash.isEmpty()) { // This is the first call to load(). If the saved hash matches the // file's hash, there will be no need to load the file again. mCurrentHash = mSettings->hash(); } QString localFileName; if (isSettingsLocalFile) { // It's a local file. // Cache file name, because readLocalFile() will clear mSaveUrl on failure. localFileName = settingsLocalFileName; if (mFileStorage && localFileName != mFileStorage->fileName()) { // The resource's location should never change, so this code should // never be reached! qCWarning(KALARM_LOG) << "SingleFileResource::load:" << displayId() << "Error? File location changed to" << localFileName; setLoadFailure(); mFileStorage.clear(); mCalendar.clear(); mLoadedEvents.clear(); } // Check if the file exists, and if not, create it QFile f(localFileName); if (!f.exists()) { // First try to create the directory the file should be located in QDir dir = QFileInfo(f).dir(); if (!dir.exists()) dir.mkpath(dir.path()); if (!f.open(QIODevice::WriteOnly) || !f.resize(0)) { const QString path = mSettings->displayLocation(); qCWarning(KALARM_LOG) << "SingleFileResource::load:" << displayId() << "Could not create file" << path; - errorMessage = i18n("Could not create calendar file '%1'.", path); + errorMessage = xi18nc("@info", "Could not create calendar file %1.", path); mStatus = Status::Broken; mSaveUrl.clear(); setLoadFailure(); return -1; } mFileReadOnly = false; mStatus = Status::Ready; } else mFileReadOnly = !QFileInfo(localFileName).isWritable(); } else { // It's a remote file. const QString cachePath = cacheFilePath(); if (!QFile::exists(cachePath)) readThroughCache = true; if (readThroughCache) { auto ref = new QEventLoopLocker(); // NOTE: Test what happens with remotefile -> save, close before save is finished. mDownloadJob = KIO::file_copy(mSettings->url(), QUrl::fromLocalFile(cacheFilePath()), -1, KIO::Overwrite | KIO::DefaultFlags | KIO::HideProgressInfo); mDownloadJob->setProperty("QEventLoopLocker", QVariant::fromValue(ref)); connect(mDownloadJob, &KJob::result, this, &SingleFileResource::slotDownloadJobResult); // connect(mDownloadJob, SIGNAL(percent(KJob*,ulong)), SLOT(handleProgress(KJob*,ulong))); mStatus = Status::Loading; return 0; // loading initiated } // Load from cache, without downloading again. localFileName = cachePath; } // It's a local file (or we're reading the cache file). if (!readLocalFile(localFileName, errorMessage)) { qCWarning(KALARM_LOG) << "SingleFileResource::load:" << displayId() << "Could not read file" << localFileName; // A user error message has been set by readLocalFile(). mStatus = Status::Broken; setLoadFailure(); return -1; } if (isSettingsLocalFile) KDirWatch::self()->addFile(settingsLocalFileName); newEvents = mLoadedEvents; mStatus = Status::Ready; return 1; // success } void SingleFileResource::setLoadFailure() { mLoadedEvents.clear(); QHash events; setLoadedEvents(events); setLoaded(false); } /****************************************************************************** * Write the current mCalendar to the backend file, if it has changed since the * last load() or save(). * The file location to write to is given by mSaveUrl. This is for two reasons: * 1) to ensure that if the file location has changed (which shouldn't happen!), * the contents will not accidentally overwrite the new file. * 2) to allow the save location to be parameterised. */ int SingleFileResource::doSave(bool writeThroughCache, bool force, QString& errorMessage) { mSaveTimer->stop(); if (!force && mCalendar && !mCalendar->isModified()) return 1; // there are no changes to save if (mSaveUrl.isEmpty()) { qCWarning(KALARM_LOG) << "SingleFileResource::save:" << displayId() << "No file specified"; mStatus = Status::Broken; return -1; } bool isLocalFile = mSaveUrl.isLocalFile(); QString localFileName; if (isLocalFile) { // It's a local file. localFileName = mSaveUrl.toLocalFile(); KDirWatch::self()->removeFile(localFileName); writeThroughCache = false; } else { // It's a remote file. // Check if there is a download or an upload in progress. if (mDownloadJob) { qCWarning(KALARM_LOG) << "SingleFileResource::save:" << displayId() << "A download is still in progress."; - errorMessage = i18n("A previous load is still in progress."); + errorMessage = i18nc("@info", "A previous load is still in progress."); return -1; } if (mUploadJob) { qCWarning(KALARM_LOG) << "SingleFileResource::save:" << displayId() << "Another file upload is still in progress."; - errorMessage = i18n("A previous save is still in progress."); + errorMessage = i18nc("@info", "A previous save is still in progress."); return -1; } localFileName = cacheFilePath(); } // Write to the local file or the cache file. // This sets the 'modified' status of mCalendar to false. const bool writeResult = writeToFile(localFileName, errorMessage); // Update the hash so we can detect at localFileChanged() if the file actually // did change. mCurrentHash = calculateHash(localFileName); saveHash(mCurrentHash); if (isLocalFile) { if (!KDirWatch::self()->contains(localFileName)) KDirWatch::self()->addFile(localFileName); } if (!writeResult) { qCWarning(KALARM_LOG) << "SingleFileResource::save:" << displayId() << "Error writing to file" << localFileName; // A user error message has been set by writeToFile(). mStatus = Status::Broken; return -1; } if (!isLocalFile && writeThroughCache) { // Write the cache file to the remote file. auto ref = new QEventLoopLocker(); // Start a job to upload the locally cached file to the remote location. mUploadJob = KIO::file_copy(QUrl::fromLocalFile(cacheFilePath()), mSaveUrl, -1, KIO::Overwrite | KIO::DefaultFlags | KIO::HideProgressInfo); mUploadJob->setProperty("QEventLoopLocker", QVariant::fromValue(ref)); connect(mUploadJob, &KJob::result, this, &SingleFileResource::slotUploadJobResult); // connect(mUploadJob, SIGNAL(percent(KJob*,ulong)), SLOT(handleProgress(KJob*,ulong))); mStatus = Status::Saving; return 0; } mStatus = Status::Ready; return 1; } /****************************************************************************** * Schedule the resource for saving after a delay, so as to enable multiple * changes to be saved together. */ bool SingleFileResource::scheduleSave(bool writeThroughCache) { qCDebug(KALARM_LOG) << "SingleFileResource::scheduleSave:" << displayId() << writeThroughCache; if (!checkSave()) return false; if (mSaveTimer->isActive()) { mSavePendingCache = mSavePendingCache || writeThroughCache; return false; } mSaveTimer->start(); mSavePendingCache = writeThroughCache; return true; } /****************************************************************************** * Close the resource. */ void SingleFileResource::close() { qCDebug(KALARM_LOG) << "SingleFileResource::close" << displayId(); if (mDownloadJob) mDownloadJob->kill(); save(true); // write through cache // If a remote file upload job has been started, the use of // QEventLoopLocker should ensure that it continues to completion even if // the destructor for this instance is executed. if (mSettings->url().isLocalFile()) KDirWatch::self()->removeFile(mSettings->url().toLocalFile()); mCalendar.clear(); mFileStorage.clear(); mStatus = Status::Closed; } /****************************************************************************** * Called from addEvent() to add an event to the resource. */ bool SingleFileResource::doAddEvent(const KAEvent& event) { if (!mCalendar) { qCCritical(KALARM_LOG) << "SingleFileResource::addEvent:" << displayId() << "Calendar not open"; return false; } KCalendarCore::Event::Ptr kcalEvent(new KCalendarCore::Event); event.updateKCalEvent(kcalEvent, KAEvent::UID_SET); if (!mCalendar->addEvent(kcalEvent)) { qCCritical(KALARM_LOG) << "SingleFileResource::addEvent:" << displayId() << "Error adding event with id" << event.id(); return false; } return addLoadedEvent(kcalEvent); } /****************************************************************************** * Add an event to the list of loaded events. */ bool SingleFileResource::addLoadedEvent(const KCalendarCore::Event::Ptr& kcalEvent) { KAEvent event(kcalEvent); if (!event.isValid()) { qCDebug(KALARM_LOG) << "SingleFileResource::addLoadedEvent:" << displayId() << "Invalid event:" << kcalEvent->uid(); return false; } event.setResourceId(mSettings->id()); event.setCompatibility(mCompatibility); mLoadedEvents[event.id()] = event; return true; } /****************************************************************************** * Called when an event has been updated. Its UID must be unchanged. * Store the updated event in the calendar. */ bool SingleFileResource::doUpdateEvent(const KAEvent& event) { if (!mCalendar) { qCCritical(KALARM_LOG) << "SingleFileResource::updateEvent:" << displayId() << "Calendar not open"; return false; } KCalendarCore::Event::Ptr calEvent = mCalendar->event(event.id()); if (!calEvent) { qCWarning(KALARM_LOG) << "SingleFileResource::doUpdateEvent:" << displayId() << "Event not found" << event.id(); return false; } if (calEvent->isReadOnly()) { qCWarning(KALARM_LOG) << "SingleFileResource::updateEvent:" << displayId() << "Event is read only:" << event.id(); return false; } // Update the event in place. mCalendar->deleteEventInstances(calEvent); event.updateKCalEvent(calEvent, KAEvent::UID_SET); mCalendar->setModified(true); return true; } /****************************************************************************** * Called from deleteEvent() to delete an event from the resource. */ bool SingleFileResource::doDeleteEvent(const KAEvent& event) { if (!mCalendar) { qCCritical(KALARM_LOG) << "SingleFileResource::doDeleteEvent:" << displayId() << "Calendar not open"; return false; } bool found = false; const KCalendarCore::Event::Ptr calEvent = mCalendar->event(event.id()); if (calEvent) { if (calEvent->isReadOnly()) { qCWarning(KALARM_LOG) << "SingleFileResource::updateEvent:" << displayId() << "Event is read only:" << event.id(); return false; } found = mCalendar->deleteEvent(calEvent); mCalendar->deleteEventInstances(calEvent); } mLoadedEvents.remove(event.id()); if (!found) { qCWarning(KALARM_LOG) << "SingleFileResource::doDeleteEvent:" << displayId() << "Event not found" << event.id(); return false; } return true; } /****************************************************************************** * Update the hash of the file, and read it if the hash has changed. */ bool SingleFileResource::readLocalFile(const QString& fileName, QString& errorMessage) { const QByteArray newHash = calculateHash(fileName); if (newHash == mCurrentHash) qCDebug(KALARM_LOG) << "SingleFileResource::readLocalFile:" << displayId() << "hash unchanged"; else { if (!readFromFile(fileName, errorMessage)) { mCurrentHash.clear(); mSaveUrl.clear(); // reset so we don't accidentally overwrite the file return false; } if (mCurrentHash.isEmpty()) { // This is the very first time the file has been read, so store // the hash as save() might not be called at all (e.g. in case of // read only resources). saveHash(newHash); } mCurrentHash = newHash; } return true; } /****************************************************************************** * Read calendar data from the given file. * Find the calendar file's compatibility with the current KAlarm format. * The file is always local; loading from the network is done automatically if * needed. */ bool SingleFileResource::readFromFile(const QString& fileName, QString& errorMessage) { qCDebug(KALARM_LOG) << "SingleFileResource::readFromFile:" << fileName; mLoadedEvents.clear(); mCalendar.reset(new KCalendarCore::MemoryCalendar(QTimeZone::utc())); mFileStorage.reset(new KCalendarCore::FileStorage(mCalendar, fileName, new KCalendarCore::ICalFormat())); const bool result = mFileStorage->load(); if (!result) { qCCritical(KALARM_LOG) << "SingleFileResource::readFromFile: Error loading file " << fileName; - errorMessage = i18n("Could not load file '%1'.", fileName); + errorMessage = xi18nc("@info", "Could not load file %1.", fileName); return false; } if (mCalendar->incidences().isEmpty()) { // It's a new file. Set up the KAlarm custom property. KACalendar::setKAlarmVersion(mCalendar); } mCompatibility = getCompatibility(mFileStorage, mVersion); // Retrieve events from the calendar const KCalendarCore::Event::List events = mCalendar->events(); for (const KCalendarCore::Event::Ptr& kcalEvent : qAsConst(events)) { if (kcalEvent->alarms().isEmpty()) qCDebug(KALARM_LOG) << "SingleFileResource::readFromFile:" << displayId() << "KCalendarCore::Event has no alarms:" << kcalEvent->uid(); else addLoadedEvent(kcalEvent); } mCalendar->setModified(false); return true; } /****************************************************************************** * Write calendar data to the given file. */ bool SingleFileResource::writeToFile(const QString& fileName, QString& errorMessage) { qCDebug(KALARM_LOG) << "SingleFileResource::writeToFile:" << fileName; if (!mCalendar) { qCCritical(KALARM_LOG) << "SingleFileResource::writeToFile:" << displayId() << "mCalendar is null!"; - errorMessage = i18n("Calendar not open."); + errorMessage = i18nc("@info", "Calendar not open."); return false; } KACalendar::setKAlarmVersion(mCalendar); // write the application ID into the calendar KCalendarCore::FileStorage* fileStorage = mFileStorage.data(); if (!mFileStorage || fileName != mFileStorage->fileName()) fileStorage = new KCalendarCore::FileStorage(mCalendar, fileName, new KCalendarCore::ICalFormat()); bool success = true; if (!fileStorage->save()) // this sets mCalendar->modified to false { qCCritical(KALARM_LOG) << "SingleFileResource::writeToFile:" << displayId() << "Failed to save calendar to file " << fileName; - errorMessage = i18n("Could not save file '%1'.", fileName); + errorMessage = xi18nc("@info", "Could not save file %1.", fileName); success = false; } if (fileStorage != mFileStorage.data()) delete fileStorage; return success; } /****************************************************************************** * Return the path of the cache file to use. Its directory is created if needed. */ QString SingleFileResource::cacheFilePath() const { static QString cacheDir; if (cacheDir.isEmpty()) { cacheDir = QStandardPaths::writableLocation(QStandardPaths::CacheLocation); QDir().mkpath(cacheDir); } return cacheDir + QLatin1Char('/') + identifier(); } /****************************************************************************** * Calculate the hash of a file. */ QByteArray SingleFileResource::calculateHash(const QString& fileName) const { QFile file(fileName); if (file.exists()) { if (file.open(QIODevice::ReadOnly)) { const qint64 blockSize = 512 * 1024; // Read blocks of 512K QCryptographicHash hash(QCryptographicHash::Md5); while (!file.atEnd()) hash.addData(file.read(blockSize)); file.close(); return hash.result(); } } return QByteArray(); } /****************************************************************************** * Save a hash value into the resource's config. */ void SingleFileResource::saveHash(const QByteArray& hash) const { mSettings->setHash(hash.toHex()); mSettings->save(); } /****************************************************************************** * Called when the local file has changed. * Not applicable to remote files or their cache files. */ void SingleFileResource::localFileChanged(const QString& fileName) { if (fileName != mSettings->url().toLocalFile()) return; // not the calendar file for this resource const QByteArray newHash = calculateHash(fileName); // Only need to synchronise when the file was changed by another process. if (newHash == mCurrentHash) return; qCWarning(KALARM_LOG) << "SingleFileResource::localFileChanged:" << displayId() << "Calendar" << mSaveUrl.toDisplayString(QUrl::PreferLocalFile) << "changed by another process: reloading"; load(); } /****************************************************************************** * Called when download of the remote to the cache file has completed. */ void SingleFileResource::slotDownloadJobResult(KJob* job) { bool success = true; QString errorMessage; if (job->error() && job->error() != KIO::ERR_DOES_NOT_EXIST) { if (mStatus != Status::Closed) mStatus = Status::Broken; setLoadFailure(); const QString path = mSettings->displayLocation(); qCWarning(KALARM_LOG) << "SingleFileResource::slotDownloadJobResult:" << displayId() << "Could not load file" << path << job->errorString(); - errorMessage = i18n("Could not load file '%1'. (%2)", path, job->errorString()); + errorMessage = xi18nc("@info", "Could not load file %1. (%2)", path, job->errorString()); success = false; } else { const QString localFileName = QUrl::fromLocalFile(cacheFilePath()).toLocalFile(); if (!readLocalFile(localFileName, errorMessage)) { qCWarning(KALARM_LOG) << "SingleFileResource::slotDownloadJobResult:" << displayId() << "Could not load local file" << localFileName; // A user error message has been set by readLocalFile(). if (mStatus != Status::Closed) mStatus = Status::Broken; setLoadFailure(); success = false; } else if (mStatus != Status::Closed) mStatus = Status::Ready; } mDownloadJob = nullptr; auto ref = job->property("QEventLoopLocker").value(); if (ref) delete ref; FileResource::loaded(success, mLoadedEvents, errorMessage); } /****************************************************************************** * Called when upload of the cache file to the remote has completed. */ void SingleFileResource::slotUploadJobResult(KJob* job) { QString errorMessage; bool success = true; if (job->error()) { if (mStatus != Status::Closed) mStatus = Status::Broken; const QString path = mSettings->displayLocation(); qCWarning(KALARM_LOG) << "SingleFileResource::slotDownloadJobResult:" << displayId() << "Could not save file" << path << job->errorString(); - errorMessage = i18n("Could not save file '%1'. (%2)", path, job->errorString()); + errorMessage = xi18nc("@info", "Could not save file %1. (%2)", path, job->errorString()); success = false; } else if (mStatus != Status::Closed) mStatus = Status::Ready; mUploadJob = nullptr; auto ref = job->property("QEventLoopLocker").value(); if (ref) delete ref; FileResource::saved(success, errorMessage); } /****************************************************************************** * Called when the resource settings have changed. */ void SingleFileResource::handleSettingsChange(Changes change) { qCDebug(KALARM_LOG) << "SingleFileResource::handleSettingsChange:" << displayId(); if (change & AlarmTypes) { qCDebug(KALARM_LOG) << "SingleFileResource::handleSettingsChange:" << displayId() << "Update alarm types"; load(); } if (change & Enabled) { qCDebug(KALARM_LOG) << "SingleFileResource::handleSettingsChange:" << displayId() << "Update enabled status"; if (mSettings->enabledTypes()) load(); } if (change & UpdateFormat) { if (mSettings->updateFormat()) { // This is a request to update the backend calendar storage format // to the current KAlarm format. qCDebug(KALARM_LOG) << "SingleFileResource::handleSettingsChange:" << displayId() << "Update storage format"; switch (mCompatibility) { case KACalendar::Current: qCWarning(KALARM_LOG) << "SingleFileResource::handleSettingsChange:" << displayId() << "Already current storage format"; break; case KACalendar::Incompatible: default: qCWarning(KALARM_LOG) << "SingleFileResource::handleSettingsChange:" << displayId() << "Incompatible storage format: compat=" << mCompatibility; break; case KACalendar::Converted: case KACalendar::Convertible: { if (!isEnabled(CalEvent::EMPTY)) { qCWarning(KALARM_LOG) << "SingleFileResource::handleSettingsChange:" << displayId() << "Cannot update storage format for a disabled resource"; break; } if (mSettings->readOnly() || mFileReadOnly) { qCWarning(KALARM_LOG) << "SingleFileResource::handleSettingsChange:" << displayId() << "Cannot update storage format for a read-only resource"; break; } // Update the backend storage format to the current KAlarm format QTimer::singleShot(0, this, [this] { updateFormat(); }); return; } } mSettings->setUpdateFormat(false); mSettings->save(); } } } // vim: et sw=4: diff --git a/src/resources/singlefileresourceconfigdialog.cpp b/src/resources/singlefileresourceconfigdialog.cpp index 05c9bc89..81a6fc20 100644 --- a/src/resources/singlefileresourceconfigdialog.cpp +++ b/src/resources/singlefileresourceconfigdialog.cpp @@ -1,319 +1,319 @@ /* * singlefileresourceconfigdialog.cpp - configuration dialog for single file resources. * Program: kalarm * Copyright © 2020 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 "singlefileresourceconfigdialog.h" #include "ui_singlefileresourceconfigdialog.h" #include #include #include #include #include using namespace KAlarmCal; namespace { void setTextEditHeight(QTextEdit*, QGroupBox*); } SingleFileResourceConfigDialog::SingleFileResourceConfigDialog(bool create, QWidget* parent) : QDialog(parent) , mCreating(create) { mUi = new Ui_SingleFileResourceConfigWidget; mUi->setupUi(this); - setWindowTitle(i18n("Configure Calendar")); + setWindowTitle(i18nc("@title:window", "Configure Calendar")); if (mCreating) { mUi->pathText->setVisible(false); mUi->alarmTypeLabel->setVisible(false); mUi->pathRequester->setMode(KFile::File); - mUi->pathRequester->setFilter(QStringLiteral("*.ics|%1").arg(i18nc("@info", "Calendar Files"))); + mUi->pathRequester->setFilter(QStringLiteral("*.ics|%1").arg(i18nc("@item:inlistbox", "Calendar Files"))); mUi->pathRequester->setFocus(); mUi->statusLabel->setText(QString()); connect(mUi->pathRequester, &KUrlRequester::textChanged, this, &SingleFileResourceConfigDialog::validate); connect(mUi->alarmTypeGroup, QOverload::of(&QButtonGroup::buttonToggled), this, &SingleFileResourceConfigDialog::validate); } else { mUi->pathRequester->setVisible(false); mUi->statusLabel->setVisible(false); mUi->pathDescription->setVisible(false); mUi->activeRadio->setVisible(false); mUi->archivedRadio->setVisible(false); mUi->templateRadio->setVisible(false); mUi->alarmTypeDescription->setVisible(false); mUi->displayNameText->setFocus(); } connect(mUi->displayNameText, &QLineEdit::textChanged, this, &SingleFileResourceConfigDialog::validate); connect(mUi->buttonBox, &QDialogButtonBox::rejected, this, &SingleFileResourceConfigDialog::close); connect(mUi->buttonBox, &QDialogButtonBox::accepted, this, &SingleFileResourceConfigDialog::accept); QTimer::singleShot(0, this, &SingleFileResourceConfigDialog::validate); } SingleFileResourceConfigDialog::~SingleFileResourceConfigDialog() { delete mUi; } void SingleFileResourceConfigDialog::setUrl(const QUrl& url, bool readOnly) { if (mCreating) { mUi->pathRequester->setUrl(url); if (readOnly) { mUi->pathRequester->lineEdit()->setEnabled(false); mUi->pathRequester->button()->setVisible(false); mUi->statusLabel->setVisible(false); mUi->activeRadio->setEnabled(false); mUi->archivedRadio->setEnabled(false); mUi->templateRadio->setEnabled(false); enableOkButton(); } } else { mUi->pathText->setText(url.toDisplayString(QUrl::PrettyDecoded | QUrl::PreferLocalFile)); } } QUrl SingleFileResourceConfigDialog::url() const { return mCreating ? mUi->pathRequester->url() : QUrl(); } QString SingleFileResourceConfigDialog::displayName() const { return mUi->displayNameText->text(); } void SingleFileResourceConfigDialog::setDisplayName(const QString& name) { mUi->displayNameText->setText(name); } bool SingleFileResourceConfigDialog::readOnly() const { return mUi->readOnlyCheckbox->isChecked(); } void SingleFileResourceConfigDialog::setReadOnly(bool readonly) { mUi->readOnlyCheckbox->setChecked(readonly); } CalEvent::Type SingleFileResourceConfigDialog::alarmType() const { if (mCreating) { if (mUi->activeRadio->isChecked()) return CalEvent::ACTIVE; if (mUi->archivedRadio->isChecked()) return CalEvent::ARCHIVED; if (mUi->templateRadio->isChecked()) return CalEvent::TEMPLATE; } return CalEvent::EMPTY; } void SingleFileResourceConfigDialog::setAlarmType(CalEvent::Type type) { switch (type) { case CalEvent::ACTIVE: if (mCreating) mUi->activeRadio->setChecked(true); else mUi->activeAlarmsText->setVisible(true); break; case CalEvent::ARCHIVED: if (mCreating) mUi->archivedRadio->setChecked(true); else mUi->archivedAlarmsText->setVisible(true); break; case CalEvent::TEMPLATE: if (mCreating) mUi->templateRadio->setChecked(true); else mUi->templateAlarmsText->setVisible(true); break; default: break; } } void SingleFileResourceConfigDialog::setUrlValidation(QString (*func)(const QUrl&)) { if (mCreating) mUrlValidationFunc = func; } /****************************************************************************** * Validate the current user input. If invalid, disable the OK button. */ void SingleFileResourceConfigDialog::validate() { // Validate URL first, in order to display any error message. const QUrl currentUrl = mUi->pathRequester->url(); if (mUi->pathRequester->text().trimmed().isEmpty() || currentUrl.isEmpty()) { disableOkButton(QString()); return; } if (mUrlValidationFunc) { const QString error = (*mUrlValidationFunc)(currentUrl); if (!error.isEmpty()) { disableOkButton(error, true); return; } } if (mUi->displayNameText->text().trimmed().isEmpty() || (mCreating && !mUi->alarmTypeGroup->checkedButton())) { disableOkButton(QString()); return; } if (!mCreating) { enableOkButton(); return; } if (currentUrl.isLocalFile()) enableOkButton(); else { // It's a remote file. // Check whether the file can be read or written. if (mStatJob) mStatJob->kill(); mCheckingDir = false; initiateUrlStatusCheck(currentUrl); // Disable the OK button until the file's status is determined. disableOkButton(i18nc("@info:status", "Checking file information...")); } } /****************************************************************************** * Called when the status of the remote URL has been determined. * Checks whether the URL is accessible. */ void SingleFileResourceConfigDialog::slotStatJobResult(KJob* job) { if (job->error()) { if (job->error() == KIO::ERR_DOES_NOT_EXIST && !mCheckingDir) { // The file doesn't exist, so check if the file's directory is writable. mCheckingDir = true; initiateUrlStatusCheck(KIO::upUrl(mUi->pathRequester->url())); return; } // Can't read or write the URL. disableOkButton(QString()); } else enableOkButton(); mCheckingDir = false; mStatJob = nullptr; } /****************************************************************************** * Creates a job to check the status of a remote URL. */ void SingleFileResourceConfigDialog::initiateUrlStatusCheck(const QUrl& url) { mStatJob = KIO::statDetails(url, KIO::StatJob::SourceSide, KIO::StatDetail::StatDefaultDetails, KIO::HideProgressInfo); connect(mStatJob, &KIO::StatJob::result, this, &SingleFileResourceConfigDialog::slotStatJobResult); } /****************************************************************************** * Enable the OK button, and clear the URL status message. */ void SingleFileResourceConfigDialog::enableOkButton() { mUi->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(true); mUi->statusLabel->setText(QString()); } /****************************************************************************** * Disable the OK button, and set the URL status message. */ void SingleFileResourceConfigDialog::disableOkButton(const QString& statusMessage, bool errorColour) { mUi->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false); QPalette pal = mUi->pathLabel->palette(); if (errorColour) pal.setColor(QPalette::WindowText, KColorScheme(QPalette::Active).foreground(KColorScheme::NegativeText).color()); mUi->statusLabel->setPalette(pal); mUi->statusLabel->setText(statusMessage); } /****************************************************************************** * When the dialog is displayed, set appropriate heights for QTextEdit elements, * and then remove empty space between widgets. * By default, QTextEdit has a minimum height of 4 text lines, and calling * setMinimumHeight() doesn't affect this. */ void SingleFileResourceConfigDialog::showEvent(QShowEvent* se) { setTextEditHeight(mUi->nameDescription, mUi->nameGroupBox); setTextEditHeight(mUi->readOnlyDescription, mUi->readOnlyGroupBox); if (mCreating) { setTextEditHeight(mUi->pathDescription, mUi->pathGroupBox); setTextEditHeight(mUi->alarmTypeDescription, mUi->alarmTypeGroupBox); } else mUi->pathDescription->setFixedHeight(1); setFixedHeight(sizeHint().height()); QDialog::showEvent(se); } namespace { void setTextEditHeight(QTextEdit* textEdit, QGroupBox* groupBox) { const QSize size = textEdit->document()->size().toSize(); const int margin = textEdit->height() - textEdit->viewport()->height(); textEdit->setFixedHeight(size.height() + margin); groupBox->setFixedHeight(groupBox->sizeHint().height()); } } // vim: et sw=4: diff --git a/src/resources/singlefileresourceconfigdialog.ui b/src/resources/singlefileresourceconfigdialog.ui index e9057fea..9df79627 100644 --- a/src/resources/singlefileresourceconfigdialog.ui +++ b/src/resources/singlefileresourceconfigdialog.ui @@ -1,283 +1,283 @@ SingleFileResourceConfigWidget 0 0 580 0 - Calendar File + Calendar File - Filename: + Filename: pathRequester true - Status: + Status: true QAbstractScrollArea::AdjustToContents Select the file whose contents should be represented by this resource. If the file does not exist, it will be created. The URL of a remote file can also be specified, but note that monitoring for file changes will not work in this case. - Alarm Type + Alarm Type - Acti&ve Alarms + Acti&ve Alarms false alarmTypeGroup - Archived Alarms + Archived Alarms false alarmTypeGroup - Alarm &Templates + Alarm &Templates false alarmTypeGroup true QAbstractScrollArea::AdjustToContents Select which alarm type this resource should contain. - Alarm type: + Alarm type: true false - Active Alarms + Active Alarms true false - Archived Alarms + Archived Alarms true false - Alarm Templates + Alarm Templates - Display Name + Display Name - &Name: + &Name: displayNameText true QAbstractScrollArea::AdjustToContents Enter the name used to identify this resource in displays. - Access Rights + Access Rights - Read only + Read only false true QAbstractScrollArea::AdjustToContents If read-only mode is enabled, no changes will be written to the file selected above. Read-only mode will be automatically enabled if you do not have write access to the file or the file is on a remote server that does not support write access. QDialogButtonBox::Ok|QDialogButtonBox::Cancel KUrlRequester QWidget
kurlrequester.h
KLineEdit QLineEdit
klineedit.h