diff --git a/src/resources/fileresourcemigrator.h b/src/resources/fileresourcemigrator.h index f661eea8..ad77e338 100644 --- a/src/resources/fileresourcemigrator.h +++ b/src/resources/fileresourcemigrator.h @@ -1,80 +1,80 @@ /* * fileresourcemigrator.h - migrates or creates KAlarm non-Akonadi 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. */ #ifndef FILERESOURCEMIGRATOR_H #define FILERESOURCEMIGRATOR_H #include #include class KJob; namespace Akonadi { class CollectionFetchJob; } class Resource; /** * Class to migrate Akonadi or KResources alarm calendars from previous * versions of KAlarm, and to create default calendar resources if none exist. */ class FileResourceMigrator : public QObject { Q_OBJECT public: ~FileResourceMigrator(); /** Return the unique instance, creating it if necessary. * Note that the instance will be destroyed once migration has completed. * @return Unique instance, or null if migration is not required or has * already been done. */ static FileResourceMigrator* instance(); /** Initiate resource migration and default resource creation. * When execution is complete, the unique instance will be destroyed. * Connect to the QObject::destroyed() signal to determine when * execution has completed. */ void execute(); static bool completed() { return mCompleted; } private Q_SLOTS: void collectionFetchResult(KJob*); void checkIfComplete(); private: - FileResourceMigrator(QObject* parent = nullptr); + explicit FileResourceMigrator(QObject* parent = nullptr); void migrateAkonadiResources(Akonadi::ServerManager::State); void migrateKResources(); void createDefaultResources(); void createCalendar(KAlarmCal::CalEvent::Type alarmType, const QString& file, const QString& name); static FileResourceMigrator* mInstance; QList mFetchesPending; // pending collection fetch jobs for existing resources KAlarmCal::CalEvent::Types mExistingAlarmTypes {KAlarmCal::CalEvent::EMPTY}; // alarm types provided by existing non-Akonadi resources bool mMigratingAkonadi {false}; // attempting to migrate Akonadi resources bool mMigrateKResources {true}; // need to migrate KResource resources static bool mCompleted; // execute() has completed }; #endif // FILERESOURCEMIGRATOR_H // vim: et sw=4: diff --git a/src/resources/singlefileresource.cpp b/src/resources/singlefileresource.cpp index 19fd8a8f..6272fdf6 100644 --- a/src/resources/singlefileresource.cpp +++ b/src/resources/singlefileresource.cpp @@ -1,830 +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 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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. + // Only need to synchronize 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 = 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 = 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 81a6fc20..94da95f5 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*); +void setTextEditHeight(KTextEdit*, QGroupBox*); } SingleFileResourceConfigDialog::SingleFileResourceConfigDialog(bool create, QWidget* parent) : QDialog(parent) , mCreating(create) { mUi = new Ui_SingleFileResourceConfigWidget; mUi->setupUi(this); 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("@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, +* When the dialog is displayed, set appropriate heights for KTextEdit elements, * and then remove empty space between widgets. -* By default, QTextEdit has a minimum height of 4 text lines, and calling +* By default, KTextEdit 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) +void setTextEditHeight(KTextEdit* 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 9df79627..8d16a573 100644 --- a/src/resources/singlefileresourceconfigdialog.ui +++ b/src/resources/singlefileresourceconfigdialog.ui @@ -1,283 +1,288 @@ SingleFileResourceConfigWidget 0 0 580 0 Calendar File Filename: pathRequester true 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 Acti&ve Alarms false alarmTypeGroup Archived Alarms false alarmTypeGroup Alarm &Templates false alarmTypeGroup - + true QAbstractScrollArea::AdjustToContents Select which alarm type this resource should contain. Alarm type: true false Active Alarms true false Archived Alarms true false Alarm Templates Display Name &Name: displayNameText - + true QAbstractScrollArea::AdjustToContents Enter the name used to identify this resource in displays. Access Rights 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
+ + KTextEdit + QTextEdit +
ktextedit.h
+