File indexing completed on 2024-03-24 17:23:00

0001 // SPDX-FileCopyrightText: 2020 Simon Persson <simon.persson@mykolab.com>
0002 //
0003 // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0004 
0005 #include "backupplanwidget.h"
0006 #include "backupplan.h"
0007 #include "folderselectionmodel.h"
0008 #include "dirselector.h"
0009 #include "driveselection.h"
0010 #include "kbuttongroup.h"
0011 
0012 #include <QAction>
0013 #include <QBoxLayout>
0014 #include <QCheckBox>
0015 #include <QCoreApplication>
0016 #include <QDialogButtonBox>
0017 #include <QEvent>
0018 #include <QIcon>
0019 #include <QLabel>
0020 #include <QPushButton>
0021 #include <QRadioButton>
0022 #include <QSpinBox>
0023 #include <QThread>
0024 #include <QTimer>
0025 #include <QTreeView>
0026 
0027 #include <KComboBox>
0028 #include <KConfigDialogManager>
0029 #include <KConfigGroup>
0030 #include <KIO/OpenUrlJob>
0031 #include <KLineEdit>
0032 #include <KLocalizedString>
0033 #include <KMessageWidget>
0034 #include <KPageWidget>
0035 #include <KUrlCompletion>
0036 #include <KUrlRequester>
0037 #include <utility>
0038 
0039 class ScanFolderEvent : public QEvent {
0040 public:
0041     explicit ScanFolderEvent(QString pPath): QEvent(eventType), mPath(std::move(pPath)) {}
0042     QString mPath;
0043     static const QEvent::Type eventType = static_cast<QEvent::Type>(QEvent::User + 1);
0044 };
0045 
0046 FileScanner::FileScanner() {
0047     // create a timer that will call a slot to send the pending updates to UI, one second
0048     // after the last update comes in, just to minimize risk of showing incomplete
0049     // information to the user.
0050     mUnreadablesTimer = new QTimer(this);
0051     mUnreadablesTimer->setSingleShot(true);
0052     mUnreadablesTimer->setInterval(1000);
0053     connect(mUnreadablesTimer, &QTimer::timeout, this, &FileScanner::sendPendingUnreadables);
0054 
0055     mSymlinkTimer = new QTimer(this);
0056     mSymlinkTimer->setSingleShot(true);
0057     mSymlinkTimer->setInterval(1000);
0058     connect(mSymlinkTimer, &QTimer::timeout, this, &FileScanner::sendPendingSymlinks);
0059 }
0060 
0061 bool FileScanner::event(QEvent *pEvent) {
0062     if(pEvent->type() == ScanFolderEvent::eventType) {
0063         auto lEvent = dynamic_cast<ScanFolderEvent *>(pEvent);
0064         if(isPathIncluded(lEvent->mPath)) {
0065             scanFolder(lEvent->mPath);
0066         }
0067         return true;
0068     }
0069     return QObject::event(pEvent);
0070 }
0071 
0072 void FileScanner::includePath(const QString &pPath) {
0073     if(!mExcludedFolders.remove(pPath)) {
0074         mIncludedFolders += pPath;
0075     }
0076     checkPathForProblems(QFileInfo(pPath));
0077 
0078     QMutableHashIterator<QString, QString> i(mSymlinksNotOk);
0079     while(i.hasNext()) {
0080         i.next();
0081         if(isPathIncluded(i.value())) {
0082             mSymlinksOk.insert(i.key(), i.value());
0083             i.remove();
0084             mSymlinkTimer->start();
0085         }
0086     }
0087 
0088 }
0089 
0090 void FileScanner::excludePath(const QString &pPath) {
0091     if(!mIncludedFolders.remove(pPath)) {
0092         mExcludedFolders += pPath;
0093     }
0094     QString lPath = pPath + QStringLiteral("/");
0095     QSet<QString>::iterator it = mUnreadableFiles.begin();
0096     while(it != mUnreadableFiles.end()) {
0097         if(it->startsWith(lPath)) {
0098             mUnreadablesTimer->start();
0099             it = mUnreadableFiles.erase(it);
0100         } else {
0101             ++it;
0102         }
0103     }
0104     it = mUnreadableFolders.begin();
0105     while(it != mUnreadableFolders.end()) {
0106         if(it->startsWith(lPath) || *it == pPath) {
0107             mUnreadablesTimer->start();
0108             it = mUnreadableFolders.erase(it);
0109         } else {
0110             ++it;
0111         }
0112     }
0113 
0114     QMutableHashIterator<QString, QString> i(mSymlinksNotOk);
0115     while(i.hasNext()) {
0116         if(!isPathIncluded(i.next().key())) {
0117             i.remove();
0118             mSymlinkTimer->start();
0119         }
0120     }
0121 
0122     i = mSymlinksOk;
0123     while(i.hasNext()) {
0124         i.next();
0125         if(!isPathIncluded(i.key())) {
0126             i.remove();
0127         } else if(isSymlinkProblematic(i.value())) {
0128             mSymlinksNotOk.insert(i.key(), i.value());
0129             mSymlinkTimer->start();
0130             i.remove();
0131         }
0132     }
0133 }
0134 
0135 void FileScanner::sendPendingUnreadables() {
0136     emit unreadablesChanged(QPair<QSet<QString>, QSet<QString>>(mUnreadableFolders, mUnreadableFiles));
0137 }
0138 
0139 void FileScanner::sendPendingSymlinks() {
0140     emit symlinkProblemsChanged(mSymlinksNotOk);
0141 }
0142 
0143 bool FileScanner::isPathIncluded(const QString &pPath) {
0144     int lLongestInclude = 0;
0145     foreach(const QString &lPath, mIncludedFolders) {
0146         bool lMatches = pPath == lPath || pPath.startsWith(lPath + QStringLiteral("/"));
0147         if(lMatches && lPath.length() > lLongestInclude) {
0148             lLongestInclude = lPath.length();
0149         }
0150     }
0151     int lLongestExclude = 0;
0152     foreach(const QString &lPath, mExcludedFolders) {
0153         bool lMatches = pPath == lPath || pPath.startsWith(lPath + QStringLiteral("/"));
0154         if(lMatches && lPath.length() > lLongestExclude) {
0155             lLongestExclude = lPath.length();
0156         }
0157     }
0158     return lLongestInclude > lLongestExclude;
0159 }
0160 
0161 void FileScanner::checkPathForProblems(const QFileInfo &pFileInfo) {
0162     if(pFileInfo.isSymLink()) {
0163         if(isSymlinkProblematic(pFileInfo.symLinkTarget())) {
0164             mSymlinksNotOk.insert(pFileInfo.absoluteFilePath(), pFileInfo.symLinkTarget());
0165             mSymlinkTimer->start();
0166         } else {
0167             mSymlinksOk.insert(pFileInfo.absoluteFilePath(), pFileInfo.symLinkTarget());
0168         }
0169     } else if(pFileInfo.isDir()) {
0170         QCoreApplication::postEvent(this, new ScanFolderEvent(pFileInfo.absoluteFilePath()),
0171                                     Qt::LowEventPriority);
0172     } else {
0173         if(!pFileInfo.isReadable()) {
0174             mUnreadableFiles += pFileInfo.absoluteFilePath();
0175             mUnreadablesTimer->start();
0176         }
0177     }
0178 }
0179 
0180 bool FileScanner::isSymlinkProblematic(const QString &pTarget) {
0181     QFileInfo lTargetInfo(pTarget);
0182     return lTargetInfo.exists() && !isPathIncluded(pTarget)
0183             && pTarget.startsWith(QStringLiteral("/home/"));
0184 }
0185 
0186 void FileScanner::scanFolder(const QString &pPath) {
0187     QDir lDir(pPath);
0188     if(!lDir.isReadable()) {
0189         mUnreadableFolders += pPath;
0190         mUnreadablesTimer->start();
0191     } else {
0192         QFileInfoList lInfoList = lDir.entryInfoList(QDir::Files | QDir::Dirs | QDir::Hidden| QDir::NoDotAndDotDot);
0193         foreach(const QFileInfo &lFileInfo, lInfoList) {
0194             checkPathForProblems(lFileInfo);
0195         }
0196     }
0197 }
0198 
0199 ConfigIncludeDummy::ConfigIncludeDummy(FolderSelectionModel *pModel, FolderSelectionWidget *pParent)
0200    : QWidget(pParent), mModel(pModel), mTreeView(pParent)
0201 {
0202     connect(mModel, &FolderSelectionModel::includedPathAdded, this, &ConfigIncludeDummy::includeListChanged);
0203     connect(mModel, &FolderSelectionModel::includedPathRemoved, this, &ConfigIncludeDummy::includeListChanged);
0204 }
0205 
0206 QStringList ConfigIncludeDummy::includeList() {
0207     QStringList lList = mModel->includedPaths().values();
0208     lList.sort();
0209     return lList;
0210 }
0211 
0212 void ConfigIncludeDummy::setIncludeList(QStringList pIncludeList) {
0213     for(int i = 0; i < pIncludeList.count(); ++i) {
0214         if(!QFile::exists(pIncludeList.at(i))) {
0215             pIncludeList.removeAt(i--);
0216         }
0217     }
0218     mModel->setIncludedPaths(QSet<QString>(pIncludeList.begin(), pIncludeList.end()));
0219     mTreeView->expandToShowSelections();
0220 }
0221 
0222 ConfigExcludeDummy::ConfigExcludeDummy(FolderSelectionModel *pModel, FolderSelectionWidget *pParent)
0223    : QWidget(pParent), mModel(pModel), mTreeView(pParent)
0224 {
0225     connect(mModel, &FolderSelectionModel::excludedPathAdded, this, &ConfigExcludeDummy::excludeListChanged);
0226     connect(mModel, &FolderSelectionModel::excludedPathRemoved, this, &ConfigExcludeDummy::excludeListChanged);
0227 }
0228 
0229 QStringList ConfigExcludeDummy::excludeList() {
0230     QStringList lList = mModel->excludedPaths().values();
0231     lList.sort();
0232     return lList;
0233 }
0234 
0235 void ConfigExcludeDummy::setExcludeList(QStringList pExcludeList) {
0236     for(int i = 0; i < pExcludeList.count(); ++i) {
0237         if(!QFile::exists(pExcludeList.at(i))) {
0238             pExcludeList.removeAt(i--);
0239         }
0240     }
0241     mModel->setExcludedPaths(QSet<QString>(pExcludeList.begin(), pExcludeList.end()));
0242     mTreeView->expandToShowSelections();
0243 }
0244 
0245 FolderSelectionWidget::FolderSelectionWidget(FolderSelectionModel *pModel, QWidget *pParent)
0246    : QWidget(pParent), mModel(pModel)
0247 {
0248     mMessageWidget = new KMessageWidget(this);
0249     mMessageWidget->setCloseButtonVisible(false);
0250     mMessageWidget->setWordWrap(true);
0251     mMessageWidget->hide();
0252     mTreeView = new QTreeView(this);
0253     auto lVLayout = new QVBoxLayout;
0254     lVLayout->addWidget(mMessageWidget);
0255     lVLayout->addWidget(mTreeView, 1);
0256     setLayout(lVLayout);
0257 
0258     connect(mMessageWidget, &KMessageWidget::hideAnimationFinished,
0259             this, &FolderSelectionWidget::updateMessage);
0260 
0261     mExcludeAction = new QAction(xi18nc("@action:button", "Exclude Folder"), this);
0262     connect(mExcludeAction, &QAction::triggered, this, &FolderSelectionWidget::executeExcludeAction);
0263 
0264     mIncludeAction = new QAction(xi18nc("@action:button", "Include Folder"), this);
0265     connect(mIncludeAction, &QAction::triggered, this, &FolderSelectionWidget::executeIncludeAction);
0266 
0267     mModel->setRootPath(QStringLiteral("/"));
0268     mModel->setParent(this);
0269     mTreeView->setAnimated(true);
0270     mTreeView->setModel(mModel);
0271     mTreeView->setHeaderHidden(true);
0272     // always expand the home folder, prevents problem with empty include&exclude lists.
0273     QModelIndex lIndex = mModel->index(QDir::homePath());
0274     while(lIndex.isValid()) {
0275         mTreeView->expand(lIndex);
0276         lIndex = lIndex.parent();
0277     }
0278 
0279     auto lIncludeDummy = new ConfigIncludeDummy(mModel, this);
0280     lIncludeDummy->setObjectName(QStringLiteral("kcfg_Paths included"));
0281     auto lExcludeDummy = new ConfigExcludeDummy(mModel, this);
0282     lExcludeDummy->setObjectName(QStringLiteral("kcfg_Paths excluded"));
0283 
0284     qRegisterMetaType<QPair<QSet<QString>,QSet<QString>>>("QPair<QSet<QString>,QSet<QString>>");
0285     qRegisterMetaType<QHash<QString,QString>>("QHash<QString,QString>");
0286 
0287     mWorkerThread = new QThread(this);
0288     auto lFileScanner = new FileScanner;
0289     lFileScanner->moveToThread(mWorkerThread);
0290     connect(mWorkerThread, &QThread::finished, lFileScanner, &QObject::deleteLater);
0291 
0292     connect(mModel, &FolderSelectionModel::includedPathAdded,
0293             lFileScanner, &FileScanner::includePath);
0294     connect(mModel, &FolderSelectionModel::excludedPathRemoved,
0295             lFileScanner, &FileScanner::includePath);
0296     connect(mModel, &FolderSelectionModel::excludedPathAdded,
0297             lFileScanner, &FileScanner::excludePath);
0298     connect(mModel, &FolderSelectionModel::includedPathRemoved,
0299             lFileScanner, &FileScanner::excludePath);
0300     connect(lFileScanner, &FileScanner::unreadablesChanged,
0301             this, &FolderSelectionWidget::setUnreadables);
0302     connect(lFileScanner, &FileScanner::symlinkProblemsChanged,
0303             this, &FolderSelectionWidget::setSymlinks);
0304     mWorkerThread->start();
0305 }
0306 
0307 FolderSelectionWidget::~FolderSelectionWidget() {
0308     mWorkerThread->quit();
0309     mWorkerThread->wait();
0310 }
0311 
0312 void FolderSelectionWidget::setHiddenFoldersVisible(bool pVisible) {
0313     mModel->setHiddenFoldersVisible(pVisible);
0314     // give the filesystem model some time to refresh after changing filtering
0315     // before expanding folders again.
0316     if(pVisible) {
0317         QTimer::singleShot(2000, this, SLOT(expandToShowSelections()));
0318     }
0319 }
0320 
0321 void FolderSelectionWidget::expandToShowSelections() {
0322     foreach(const QString& lFolder,  mModel->includedPaths() + mModel->excludedPaths()) {
0323         QFileInfo lFolderInfo(lFolder);
0324         bool lShouldBeShown = true;
0325         while(lFolderInfo.absoluteFilePath() != QStringLiteral("/")) {
0326             if(lFolderInfo.isHidden() && !mModel->hiddenFoldersVisible()) {
0327                 lShouldBeShown = false; // skip if this folder should not be shown.
0328                 break;
0329             }
0330             lFolderInfo = QFileInfo(lFolderInfo.absolutePath()); // move up one level
0331         }
0332         if(lShouldBeShown) {
0333             QModelIndex lIndex = mModel->index(lFolder).parent();
0334             while(lIndex.isValid()) {
0335                 mTreeView->expand(lIndex);
0336                 lIndex = lIndex.parent();
0337             }
0338         }
0339     }
0340 }
0341 
0342 void FolderSelectionWidget::setUnreadables(const QPair<QSet<QString>, QSet<QString> > &pUnreadables) {
0343     mUnreadableFolders = pUnreadables.first.values();
0344     mUnreadableFiles = pUnreadables.second.values();
0345     updateMessage();
0346 }
0347 
0348 void FolderSelectionWidget::setSymlinks(QHash<QString, QString> pSymlinks) {
0349     mSymlinkProblems = std::move(pSymlinks);
0350     updateMessage();
0351 }
0352 
0353 void FolderSelectionWidget::updateMessage() {
0354     if(mMessageWidget->isVisible() || mMessageWidget->isHideAnimationRunning()) {
0355         mMessageWidget->animatedHide();
0356         return;
0357     }
0358 
0359     mMessageWidget->removeAction(mExcludeAction);
0360     mMessageWidget->removeAction(mIncludeAction);
0361 
0362     if(!mUnreadableFolders.isEmpty()) {
0363         mMessageWidget->setMessageType(KMessageWidget::Error);
0364         mMessageWidget->setText(xi18nc("@info message bar appearing on top",
0365                                       "You don't have permission to read this folder: <filename>%1</filename><nl/>"
0366                                       "It cannot be included in the source selection. "
0367                                       "If it does not contain anything important to you, one possible "
0368                                       "solution is to exclude the folder from the backup plan.",
0369                                       mUnreadableFolders.first()));
0370         mExcludeActionPath = mUnreadableFolders.first();
0371         mMessageWidget->addAction(mExcludeAction);
0372         mMessageWidget->animatedShow();
0373     } else if(!mUnreadableFiles.isEmpty()) {
0374         mMessageWidget->setMessageType(KMessageWidget::Error);
0375         mMessageWidget->setText(xi18nc("@info message bar appearing on top",
0376                                       "You don't have permission to read this file: <filename>%1</filename><nl/>"
0377                                       "It cannot be included in the source selection. "
0378                                       "If the file is not important to you, one possible solution is "
0379                                       "to exclude the whole folder where the file is stored from the backup plan.",
0380                                       mUnreadableFiles.first()));
0381         QFileInfo lFileInfo(mUnreadableFiles.first());
0382         mExcludeActionPath = lFileInfo.absolutePath();
0383         mMessageWidget->addAction(mExcludeAction);
0384         mMessageWidget->animatedShow();
0385     } else if(!mSymlinkProblems.isEmpty()) {
0386         mMessageWidget->setMessageType(KMessageWidget::Warning);
0387         QHashIterator<QString,QString> i(mSymlinkProblems);
0388         i.next();
0389         QFileInfo lFileInfo(i.value());
0390         if(lFileInfo.isDir()) {
0391             mMessageWidget->setText(xi18nc("@info message bar appearing on top",
0392                                           "The symbolic link <filename>%1</filename> is currently included but it points "
0393                                           "to a folder which is not: <filename>%2</filename>.<nl/>That is probably not "
0394                                           "what you want. One solution is to simply include the target folder in the "
0395                                           "backup plan.",
0396                                           i.key(), i.value()));
0397             mIncludeActionPath = i.value();
0398         } else {
0399             mMessageWidget->setText(xi18nc("@info message bar appearing on top",
0400                                           "The symbolic link <filename>%1</filename> is currently included but it points "
0401                                           "to a file which is not: <filename>%2</filename>.<nl/>That is probably not "
0402                                           "what you want. One solution is to simply include the folder where the file "
0403                                           "is stored in the backup plan.",
0404                                           i.key(), i.value()));
0405             mIncludeActionPath = lFileInfo.absolutePath();
0406         }
0407         mMessageWidget->addAction(mIncludeAction);
0408         mMessageWidget->animatedShow();
0409     }
0410 }
0411 
0412 void FolderSelectionWidget::executeExcludeAction() {
0413     mModel->excludePath(mExcludeActionPath);
0414 }
0415 
0416 void FolderSelectionWidget::executeIncludeAction() {
0417     mModel->includePath(mIncludeActionPath);
0418 }
0419 
0420 DirDialog::DirDialog(const QUrl &pRootDir, const QString &pStartSubDir, QWidget *pParent)
0421    : QDialog(pParent)
0422 {
0423     setWindowTitle(xi18nc("@title:window","Select Folder"));
0424 
0425     mDirSelector = new DirSelector(this);
0426     mDirSelector->setRootUrl(pRootDir);
0427     QUrl lSubUrl = QUrl::fromLocalFile(pRootDir.adjusted(QUrl::StripTrailingSlash).path() + '/' +
0428                                         pStartSubDir);
0429     mDirSelector->expandToUrl(lSubUrl);
0430 
0431     auto lButtonBox = new QDialogButtonBox(QDialogButtonBox::Ok|QDialogButtonBox::Cancel);
0432     connect(lButtonBox, SIGNAL(accepted()), this, SLOT(accept()));
0433     connect(lButtonBox, SIGNAL(rejected()), this, SLOT(reject()));
0434     auto lNewFolderButton = new QPushButton(xi18nc("@action:button", "New Folder..."));
0435     connect(lNewFolderButton, SIGNAL(clicked()), mDirSelector, SLOT(createNewFolder()));
0436     lButtonBox->addButton(lNewFolderButton, QDialogButtonBox::ActionRole);
0437     QPushButton *lOkButton = lButtonBox->button(QDialogButtonBox::Ok);
0438     lOkButton->setDefault(true);
0439     lOkButton->setShortcut(Qt::Key_Return);
0440 
0441     auto lMainLayout = new QVBoxLayout;
0442     lMainLayout->addWidget(mDirSelector);
0443     lMainLayout->addWidget(lButtonBox);
0444     setLayout(lMainLayout);
0445 
0446     mDirSelector->setFocus();
0447 }
0448 
0449 QUrl DirDialog::url() const {
0450     return mDirSelector->url();
0451 }
0452 
0453 
0454 BackupPlanWidget::BackupPlanWidget(BackupPlan *pBackupPlan, const QString &pBupVersion,
0455                                    const QString &pRsyncVersion, bool pPar2Available)
0456    : mBackupPlan(pBackupPlan)
0457 {
0458     mDescriptionEdit = new KLineEdit;
0459     mDescriptionEdit->setObjectName(QStringLiteral("kcfg_Description"));
0460     mDescriptionEdit->setClearButtonEnabled(true);
0461     auto lDescriptionLabel = new QLabel(xi18nc("@label", "Description:"));
0462     lDescriptionLabel->setBuddy(mDescriptionEdit);
0463     mConfigureButton = new QPushButton(QIcon::fromTheme(QStringLiteral("go-previous-view")),
0464                                        xi18nc("@action:button", "Back to overview"));
0465     connect(mConfigureButton, SIGNAL(clicked()), this, SIGNAL(requestOverviewReturn()));
0466 
0467     mConfigPages = new KPageWidget;
0468     mConfigPages->addPage(createTypePage(pBupVersion, pRsyncVersion));
0469     mSourcePage = createSourcePage();
0470     mConfigPages->addPage(mSourcePage);
0471     mConfigPages->addPage(createDestinationPage());
0472     mConfigPages->addPage(createSchedulePage());
0473     mConfigPages->addPage(createAdvancedPage(pPar2Available));
0474 
0475     auto lHLayout1 = new QHBoxLayout;
0476     lHLayout1->addWidget(mConfigureButton);
0477     lHLayout1->addStretch();
0478     lHLayout1->addWidget(lDescriptionLabel);
0479     lHLayout1->addWidget(mDescriptionEdit);
0480 
0481     auto lVLayout1 = new QVBoxLayout;
0482     lVLayout1->addLayout(lHLayout1);
0483     lVLayout1->addWidget(mConfigPages);
0484     lVLayout1->setSpacing(0);
0485     setLayout(lVLayout1);
0486 }
0487 
0488 void BackupPlanWidget::saveExtraData() {
0489     mDriveSelection->saveExtraData();
0490 }
0491 
0492 void BackupPlanWidget::showSourcePage() {
0493     mConfigPages->setCurrentPage(mSourcePage);
0494 }
0495 
0496 KPageWidgetItem *BackupPlanWidget::createTypePage(const QString &pBupVersion, const QString &pRsyncVersion) {
0497     mVersionedRadio = new QRadioButton;
0498     QString lVersionedInfo = xi18nc("@info", "This type of backup is an <emphasis>archive</emphasis>. It contains both "
0499                                    "the latest version of your files and earlier backed up versions. "
0500                                    "Using this type of backup allows you to recover older versions of your "
0501                                    "files, or files which were deleted on your computer at a later time. "
0502                                    "The storage space needed is minimized by looking for common parts of "
0503                                    "your files between versions and only storing those parts once. "
0504                                    "Nevertheless, the backup archive will keep growing in size as time goes by.<nl/>"
0505                                    "Also important to know is that the files in the archive can not be accessed "
0506                                    "directly with a general file manager, a special program is needed.");
0507     auto lVersionedInfoLabel = new QLabel(lVersionedInfo);
0508     lVersionedInfoLabel->setWordWrap(true);
0509     auto lVersionedWidget = new QWidget;
0510     lVersionedWidget->setVisible(false);
0511     QObject::connect(mVersionedRadio, SIGNAL(toggled(bool)), lVersionedWidget, SLOT(setVisible(bool)));
0512     if(pBupVersion.isEmpty()) {
0513         mVersionedRadio->setText(xi18nc("@option:radio", "Versioned Backup (not available "
0514                                                          "because <application>bup</application> is not installed)"));
0515         mVersionedRadio->setEnabled(false);
0516         lVersionedWidget->setEnabled(false);
0517     } else {
0518         mVersionedRadio->setText(xi18nc("@option:radio", "Versioned Backup (recommended)"));
0519     }
0520 
0521     mSyncedRadio = new QRadioButton;
0522     QString lSyncedInfo = xi18nc("@info",
0523                                 "This type of backup is a folder which is synchronized with your "
0524                                 "selected source folders. Saving a backup simply means making the "
0525                                 "backup destination contain an exact copy of your source folders as "
0526                                 "they are now and nothing else. If a file has been deleted in a "
0527                                 "source folder it will get deleted from the backup folder.<nl/>"
0528                                 "This type of backup can protect you against data loss due to a broken "
0529                                 "hard drive but it does not help you to recover from your own mistakes.");
0530     auto lSyncedInfoLabel = new QLabel(lSyncedInfo);
0531     lSyncedInfoLabel->setWordWrap(true);
0532     auto lSyncedWidget = new QWidget;
0533     lSyncedWidget->setVisible(false);
0534     QObject::connect(mSyncedRadio, SIGNAL(toggled(bool)), lSyncedWidget, SLOT(setVisible(bool)));
0535     if(pRsyncVersion.isEmpty()) {
0536         mSyncedRadio->setText(xi18nc("@option:radio", "Synchronized Backup (not available "
0537                                                       "because <application>rsync</application> is not installed)"));
0538         mSyncedRadio->setEnabled(false);
0539         lSyncedWidget->setEnabled(false);
0540     } else {
0541         mSyncedRadio->setText(xi18nc("@option:radio", "Synchronized Backup"));
0542     }
0543     auto lButtonGroup = new KButtonGroup;
0544     lButtonGroup->setObjectName(QStringLiteral("kcfg_Backup type"));
0545     lButtonGroup->setFlat(true);
0546     int lIndentation = lButtonGroup->style()->pixelMetric(QStyle::PM_ExclusiveIndicatorWidth) +
0547                        lButtonGroup->style()->pixelMetric(QStyle::PM_RadioButtonLabelSpacing);
0548 
0549     auto lVersionedVLayout = new QGridLayout;
0550     lVersionedVLayout->setColumnMinimumWidth(0, lIndentation);
0551     lVersionedVLayout->setContentsMargins(0, 0, 0, 0);
0552     lVersionedVLayout->addWidget(lVersionedInfoLabel, 0, 1);
0553     lVersionedWidget->setLayout(lVersionedVLayout);
0554 
0555     auto lSyncedVLayout = new QGridLayout;
0556     lSyncedVLayout->setColumnMinimumWidth(0, lIndentation);
0557     lSyncedVLayout->setContentsMargins(0, 0, 0, 0);
0558     lSyncedVLayout->addWidget(lSyncedInfoLabel, 0, 1);
0559     lSyncedWidget->setLayout(lSyncedVLayout);
0560 
0561     auto lVLayout = new QVBoxLayout;
0562     lVLayout->addWidget(mVersionedRadio);
0563     lVLayout->addWidget(lVersionedWidget);
0564     lVLayout->addWidget(mSyncedRadio);
0565     lVLayout->addWidget(lSyncedWidget);
0566     lVLayout->addStretch();
0567     lButtonGroup->setLayout(lVLayout);
0568     auto lPage = new KPageWidgetItem(lButtonGroup);
0569     lPage->setName(xi18nc("@title", "Backup Type"));
0570     lPage->setHeader(xi18nc("@label", "Select what type of backup you want"));
0571     lPage->setIcon(QIcon::fromTheme(QStringLiteral("folder-sync")));
0572     return lPage;
0573 }
0574 
0575 KPageWidgetItem *BackupPlanWidget::createSourcePage() {
0576     mSourceSelectionWidget = new FolderSelectionWidget(new FolderSelectionModel(mBackupPlan->mShowHiddenFolders), this);
0577     auto lPage = new KPageWidgetItem(mSourceSelectionWidget);
0578     lPage->setName(xi18nc("@title", "Sources"));
0579     lPage->setHeader(xi18nc("@label", "Select which folders to include in backup"));
0580     lPage->setIcon(QIcon::fromTheme(QStringLiteral("cloud-upload")));
0581     return lPage;
0582 }
0583 
0584 KPageWidgetItem *BackupPlanWidget::createDestinationPage() {
0585     auto lButtonGroup = new KButtonGroup(this);
0586     lButtonGroup->setObjectName(QStringLiteral("kcfg_Destination type"));
0587     lButtonGroup->setFlat(true);
0588 
0589     int lIndentation = lButtonGroup->style()->pixelMetric(QStyle::PM_ExclusiveIndicatorWidth) +
0590                        lButtonGroup->style()->pixelMetric(QStyle::PM_RadioButtonLabelSpacing);
0591 
0592     auto lVLayout = new QVBoxLayout;
0593     auto lFileSystemRadio = new QRadioButton(xi18nc("@option:radio", "Filesystem Path"));
0594     auto lDriveRadio = new QRadioButton(xi18nc("@option:radio", "External Storage"));
0595 
0596     auto lFileSystemWidget = new QWidget;
0597     lFileSystemWidget->setVisible(false);
0598     QObject::connect(lFileSystemRadio, SIGNAL(toggled(bool)), lFileSystemWidget, SLOT(setVisible(bool)));
0599     auto lFileSystemInfoLabel = new QLabel(xi18nc("@info",
0600                                                   "You can use this option for backing up to a secondary "
0601                                                   "internal harddrive, an external eSATA drive or networked "
0602                                                   "storage. The requirement is just that you always mount "
0603                                                   "it at the same path in the filesystem. The path "
0604                                                   "specified here does not need to exist at all times, its "
0605                                                   "existence will be monitored."));
0606     lFileSystemInfoLabel->setWordWrap(true);
0607     auto lFileSystemLabel = new QLabel(xi18nc("@label:textbox", "Destination Path for Backup:"));
0608     auto lFileSystemUrlEdit = new KUrlRequester;
0609     lFileSystemUrlEdit->setMode(KFile::Directory | KFile::LocalOnly);
0610     lFileSystemUrlEdit->setObjectName(QStringLiteral("kcfg_Filesystem destination path"));
0611     lFileSystemUrlEdit->setStartDir(QUrl::fromLocalFile(QDir::homePath()));
0612     QObject::connect(lFileSystemUrlEdit, &KUrlRequester::textChanged, this, &BackupPlanWidget::checkFilesystemDestination);
0613 
0614     mLocalMessage = new KMessageWidget(this);
0615     mLocalMessage->setCloseButtonVisible(false);
0616     mLocalMessage->setWordWrap(true);
0617     mLocalMessage->setMessageType(KMessageWidget::Warning);
0618     mLocalMessage->setText(xi18nc("@info message bar near text edit",
0619                                   "Only local filesystem paths will work!"));
0620     mLocalMessage->hide();
0621 
0622     mExistMessage = new KMessageWidget(this);
0623     mExistMessage->setCloseButtonVisible(false);
0624     mExistMessage->setWordWrap(true);
0625     mExistMessage->setMessageType(KMessageWidget::Warning);
0626     mExistMessage->setText(xi18nc("@info message bar near text edit",
0627                                   "Folder does not exist! No backups will be saved until you create it."));
0628     mExistMessage->hide();
0629 
0630     auto lFileSystemVLayout = new QGridLayout;
0631     lFileSystemVLayout->setColumnMinimumWidth(0, lIndentation);
0632     lFileSystemVLayout->setContentsMargins(0, 0, 0, 0);
0633     lFileSystemVLayout->addWidget(lFileSystemInfoLabel, 0, 1);
0634     auto lFileSystemHLayout = new QHBoxLayout;
0635     lFileSystemHLayout->addWidget(lFileSystemLabel);
0636     lFileSystemHLayout->addWidget(lFileSystemUrlEdit, 1);
0637     lFileSystemVLayout->addLayout(lFileSystemHLayout, 1, 1);
0638     lFileSystemVLayout->addWidget(mLocalMessage, 2, 1);
0639     lFileSystemVLayout->addWidget(mExistMessage, 3, 1);
0640     lFileSystemWidget->setLayout(lFileSystemVLayout);
0641 
0642     auto lDriveWidget = new QWidget;
0643     lDriveWidget->setVisible(false);
0644     QObject::connect(lDriveRadio, SIGNAL(toggled(bool)), lDriveWidget, SLOT(setVisible(bool)));
0645     auto lDriveInfoLabel = new QLabel(xi18nc("@info",
0646                                              "Use this option if you want to backup your "
0647                                              "files on an external storage that can be plugged in "
0648                                              "to this computer, such as a USB hard drive or memory "
0649                                              "stick."));
0650     lDriveInfoLabel->setWordWrap(true);
0651     mDriveSelection = new DriveSelection(mBackupPlan);
0652     mDriveSelection->setObjectName(QStringLiteral("kcfg_External drive UUID"));
0653     mDriveDestEdit = new KLineEdit;
0654     mDriveDestEdit->setObjectName(QStringLiteral("kcfg_External drive destination path"));
0655     mDriveDestEdit->setToolTip(xi18nc("@info:tooltip",
0656                                       "The specified folder will be created if it does not exist."));
0657     mDriveDestEdit->setClearButtonEnabled(true);
0658     auto lDriveDestLabel = new QLabel(xi18nc("@label:textbox", "Folder on Destination Drive:"));
0659     lDriveDestLabel->setToolTip(xi18nc("@info:tooltip",
0660                                        "The specified folder will be created if it does not exist."));
0661     lDriveDestLabel->setBuddy(mDriveDestEdit);
0662     auto lDriveDestButton = new QPushButton;
0663     lDriveDestButton->setIcon(QIcon::fromTheme(QStringLiteral("document-open")));
0664     int lButtonSize = lDriveDestButton->sizeHint().expandedTo(mDriveDestEdit->sizeHint()).height();
0665     lDriveDestButton->setFixedSize(lButtonSize, lButtonSize);
0666     lDriveDestButton->setToolTip(xi18nc("@info:tooltip", "Open dialog to select a folder"));
0667     lDriveDestButton->setEnabled(false);
0668     connect(mDriveSelection, SIGNAL(selectedDriveIsAccessibleChanged(bool)), lDriveDestButton, SLOT(setEnabled(bool)));
0669     connect(lDriveDestButton, SIGNAL(clicked()), SLOT(openDriveDestDialog()));
0670     auto lDriveDestWidget = new QWidget;
0671     lDriveDestWidget->setVisible(false);
0672     connect(mDriveSelection, SIGNAL(driveIsSelectedChanged(bool)), lDriveDestWidget, SLOT(setVisible(bool)));
0673     connect(mSyncedRadio, SIGNAL(toggled(bool)), mDriveSelection, SLOT(updateSyncWarning(bool)));
0674 
0675     auto lDriveVLayout = new QGridLayout;
0676     lDriveVLayout->setColumnMinimumWidth(0, lIndentation);
0677     lDriveVLayout->setContentsMargins(0, 0, 0, 0);
0678     lDriveVLayout->addWidget(lDriveInfoLabel, 0, 1);
0679     lDriveVLayout->addWidget(mDriveSelection, 1, 1);
0680     auto lDriveHLayout = new QHBoxLayout;
0681     lDriveHLayout->addWidget(lDriveDestLabel);
0682     lDriveHLayout->addWidget(mDriveDestEdit, 1);
0683     lDriveHLayout->addWidget(lDriveDestButton);
0684     lDriveDestWidget->setLayout(lDriveHLayout);
0685     lDriveVLayout->addWidget(lDriveDestWidget, 2, 1);
0686     lDriveWidget->setLayout(lDriveVLayout);
0687 
0688     lVLayout->addWidget(lFileSystemRadio);
0689     lVLayout->addWidget(lFileSystemWidget);
0690     lVLayout->addWidget(lDriveRadio);
0691     lVLayout->addWidget(lDriveWidget, 1);
0692     lVLayout->addStretch();
0693     lButtonGroup->setLayout(lVLayout);
0694 
0695     auto lPage = new KPageWidgetItem(lButtonGroup);
0696     lPage->setName(xi18nc("@title", "Destination"));
0697     lPage->setHeader(xi18nc("@label", "Select the backup destination"));
0698     lPage->setIcon(QIcon::fromTheme(QStringLiteral("cloud-download")));
0699     return lPage;
0700 }
0701 
0702 KPageWidgetItem *BackupPlanWidget::createSchedulePage() {
0703     auto lTopWidget = new QWidget(this);
0704     auto lTopLayout = new QVBoxLayout;
0705     auto lButtonGroup = new KButtonGroup;
0706     lButtonGroup->setObjectName(QStringLiteral("kcfg_Schedule type"));
0707     lButtonGroup->setFlat(true);
0708 
0709     int lIndentation = lButtonGroup->style()->pixelMetric(QStyle::PM_ExclusiveIndicatorWidth) +
0710                        lButtonGroup->style()->pixelMetric(QStyle::PM_RadioButtonLabelSpacing);
0711 
0712     auto lVLayout = new QVBoxLayout;
0713     lVLayout->setContentsMargins(0, 0, 0, 0);
0714     auto lManualRadio = new QRadioButton(xi18nc("@option:radio", "Manual Activation"));
0715     auto lIntervalRadio = new QRadioButton(xi18nc("@option:radio", "Interval"));
0716     auto lUsageRadio = new QRadioButton(xi18nc("@option:radio", "Active Usage Time"));
0717 
0718     auto lManualLabel = new QLabel(xi18nc("@info", "Backups are only saved when manually requested. "
0719                                                    "This can be done by using the popup menu from "
0720                                                    "the backup system tray icon."));
0721     lManualLabel->setVisible(false);
0722     lManualLabel->setWordWrap(true);
0723     connect(lManualRadio, SIGNAL(toggled(bool)), lManualLabel, SLOT(setVisible(bool)));
0724     auto lManualLayout = new QGridLayout;
0725     lManualLayout->setColumnMinimumWidth(0, lIndentation);
0726     lManualLayout->setContentsMargins(0, 0, 0, 0);
0727     lManualLayout->addWidget(lManualLabel, 0, 1);
0728 
0729     auto lIntervalWidget = new QWidget;
0730     lIntervalWidget->setVisible(false);
0731     connect(lIntervalRadio, SIGNAL(toggled(bool)), lIntervalWidget, SLOT(setVisible(bool)));
0732     auto lIntervalLabel = new QLabel(xi18nc("@info", "New backup will be triggered when backup "
0733                                                      "destination becomes available and more than "
0734                                                      "the configured interval has passed since the "
0735                                                      "last backup was saved."));
0736     lIntervalLabel->setWordWrap(true);
0737     auto lIntervalVertLayout = new QGridLayout;
0738     lIntervalVertLayout->setColumnMinimumWidth(0, lIndentation);
0739     lIntervalVertLayout->setContentsMargins(0, 0, 0, 0);
0740     lIntervalVertLayout->addWidget(lIntervalLabel, 0, 1);
0741     auto lIntervalLayout = new QHBoxLayout;
0742     lIntervalLayout->setContentsMargins(0, 0, 0, 0);
0743     auto lIntervalSpinBox = new QSpinBox;
0744     lIntervalSpinBox->setObjectName(QStringLiteral("kcfg_Schedule interval"));
0745     lIntervalSpinBox->setMinimum(1);
0746     lIntervalLayout->addWidget(lIntervalSpinBox);
0747     auto lIntervalUnit = new KComboBox;
0748     lIntervalUnit->setObjectName(QStringLiteral("kcfg_Schedule interval unit"));
0749     lIntervalUnit->addItem(xi18nc("@item:inlistbox", "Minutes"));
0750     lIntervalUnit->addItem(xi18nc("@item:inlistbox", "Hours"));
0751     lIntervalUnit->addItem(xi18nc("@item:inlistbox", "Days"));
0752     lIntervalUnit->addItem(xi18nc("@item:inlistbox", "Weeks"));
0753     lIntervalLayout->addWidget(lIntervalUnit);
0754     lIntervalLayout->addStretch();
0755     lIntervalVertLayout->addLayout(lIntervalLayout, 1, 1);
0756     lIntervalWidget->setLayout(lIntervalVertLayout);
0757 
0758     auto lUsageWidget = new QWidget;
0759     lUsageWidget->setVisible(false);
0760     connect(lUsageRadio, SIGNAL(toggled(bool)), lUsageWidget, SLOT(setVisible(bool)));
0761     auto lUsageLabel = new QLabel(xi18nc("@info",
0762                                          "New backup will be triggered when backup destination "
0763                                          "becomes available and you have been using your "
0764                                          "computer actively for more than the configured "
0765                                          "time limit since the last backup was saved."));
0766     lUsageLabel->setWordWrap(true);
0767     auto lUsageVertLayout = new QGridLayout;
0768     lUsageVertLayout->setColumnMinimumWidth(0, lIndentation);
0769     lUsageVertLayout->setContentsMargins(0, 0, 0, 0);
0770     lUsageVertLayout->addWidget(lUsageLabel, 0, 1);
0771     auto lUsageLayout = new QHBoxLayout;
0772     lUsageLayout->setContentsMargins(0, 0, 0, 0);
0773     auto lUsageSpinBox = new QSpinBox;
0774     lUsageSpinBox->setObjectName(QStringLiteral("kcfg_Usage limit"));
0775     lUsageSpinBox->setMinimum(1);
0776     lUsageLayout->addWidget(lUsageSpinBox);
0777     lUsageLayout->addWidget(new QLabel(xi18nc("@item:inlistbox", "Hours")));
0778     lUsageLayout->addStretch();
0779     lUsageVertLayout->addLayout(lUsageLayout, 1, 1);
0780     lUsageWidget->setLayout(lUsageVertLayout);
0781 
0782     auto lAskFirstCheckBox = new QCheckBox(xi18nc("@option:check",
0783                                                         "Ask for confirmation before saving backup"));
0784     lAskFirstCheckBox->setObjectName(QStringLiteral("kcfg_Ask first"));
0785     connect(lManualRadio, SIGNAL(toggled(bool)), lAskFirstCheckBox, SLOT(setHidden(bool)));
0786 
0787     lVLayout->addWidget(lManualRadio);
0788     lVLayout->addLayout(lManualLayout);
0789     lVLayout->addWidget(lIntervalRadio);
0790     lVLayout->addWidget(lIntervalWidget);
0791     lVLayout->addWidget(lUsageRadio);
0792     lVLayout->addWidget(lUsageWidget);
0793     lButtonGroup->setLayout(lVLayout);
0794 
0795     lTopLayout->addWidget(lButtonGroup);
0796     lTopLayout->addSpacing(lAskFirstCheckBox->fontMetrics().height());
0797     lTopLayout->addWidget(lAskFirstCheckBox);
0798     lTopLayout->addStretch();
0799     lTopWidget->setLayout(lTopLayout);
0800 
0801     auto lPage = new KPageWidgetItem(lTopWidget);
0802     lPage->setName(xi18nc("@title", "Schedule"));
0803     lPage->setHeader(xi18nc("@label", "Specify the backup schedule"));
0804     lPage->setIcon(QIcon::fromTheme(QStringLiteral("view-calendar")));
0805     return lPage;
0806 }
0807 
0808 KPageWidgetItem *BackupPlanWidget::createAdvancedPage(bool pPar2Available) {
0809     auto lAdvancedWidget = new QWidget(this);
0810     auto lAdvancedLayout = new QVBoxLayout;
0811 
0812     int lIndentation = lAdvancedWidget->style()->pixelMetric(QStyle::PM_IndicatorWidth) +
0813                        lAdvancedWidget->style()->pixelMetric(QStyle::PM_CheckBoxLabelSpacing);
0814 
0815     auto lShowHiddenWidget = new QWidget;
0816     auto lShowHiddenCheckBox = new QCheckBox(xi18nc("@option:check",
0817                                                     "Show hidden folders in source selection"));
0818     lShowHiddenCheckBox->setObjectName(QStringLiteral("kcfg_Show hidden folders"));
0819     connect(lShowHiddenCheckBox, SIGNAL(toggled(bool)),
0820             mSourceSelectionWidget, SLOT(setHiddenFoldersVisible(bool)));
0821     auto lShowHiddenLabel = new QLabel(xi18nc("@info",
0822                                               "This makes it possible to explicitly include or "
0823                                               "exclude hidden folders in the backup source "
0824                                               "selection. Hidden folders have a name that starts "
0825                                               "with a dot. They are typically located in your home "
0826                                               "folder and are used to store settings and temporary "
0827                                               "files for your applications."));
0828     lShowHiddenLabel->setWordWrap(true);
0829     auto lShowHiddenLayout = new QGridLayout;
0830     lShowHiddenLayout->setContentsMargins(0, 0, 0, 0);
0831     lShowHiddenLayout->setSpacing(0);
0832     lShowHiddenLayout->setColumnMinimumWidth(0, lIndentation);
0833     lShowHiddenLayout->addWidget(lShowHiddenCheckBox, 0, 0, 1, 2);
0834     lShowHiddenLayout->addWidget(lShowHiddenLabel, 1, 1);
0835     lShowHiddenWidget->setLayout(lShowHiddenLayout);
0836 
0837     auto lRecoveryWidget = new QWidget;
0838     lRecoveryWidget->setVisible(false);
0839     auto lRecoveryCheckBox = new QCheckBox;
0840     lRecoveryCheckBox->setObjectName(QStringLiteral("kcfg_Generate recovery info"));
0841 
0842     auto lRecoveryLabel = new QLabel(xi18nc("@info",
0843                                             "This will make your backups use around 10% more storage "
0844                                             "space and saving backups will take slightly longer time. In "
0845                                             "return it will be possible to recover from a partially corrupted "
0846                                             "backup."));
0847     lRecoveryLabel->setWordWrap(true);
0848     if(pPar2Available) {
0849         lRecoveryCheckBox->setText(xi18nc("@option:check", "Generate recovery information"));
0850     } else {
0851         lRecoveryCheckBox->setText(xi18nc("@option:check", "Generate recovery information (not available "
0852                                                            "because <application>par2</application> is not installed)"));
0853         lRecoveryCheckBox->setEnabled(false);
0854         lRecoveryLabel->setEnabled(false);
0855     }
0856     auto lRecoveryLayout = new QGridLayout;
0857     lRecoveryLayout->setContentsMargins(0, 0, 0, 0);
0858     lRecoveryLayout->setSpacing(0);
0859     lRecoveryLayout->setColumnMinimumWidth(0, lIndentation);
0860     lRecoveryLayout->addWidget(lRecoveryCheckBox,0, 0, 1, 2);
0861     lRecoveryLayout->addWidget(lRecoveryLabel, 1, 1);
0862     lRecoveryWidget->setLayout(lRecoveryLayout);
0863     connect(mVersionedRadio, SIGNAL(toggled(bool)), lRecoveryWidget, SLOT(setVisible(bool)));
0864 
0865     auto lVerificationWidget = new QWidget;
0866     lVerificationWidget->setVisible(false);
0867     auto lVerificationCheckBox = new QCheckBox(xi18nc("@option:check", "Verify integrity of backups"));
0868     lVerificationCheckBox->setObjectName(QStringLiteral("kcfg_Check backups"));
0869 
0870     auto lVerificationLabel = new QLabel(xi18nc("@info",
0871                                                 "Checks the whole backup archive for corruption "
0872                                                 "every time you save new data. Saving backups will take a "
0873                                                 "little bit longer time but it allows you to catch corruption "
0874                                                 "problems sooner than at the time you need to use a backup, "
0875                                                 "at that time it could be too late."));
0876     lVerificationLabel->setWordWrap(true);
0877     auto lVerificationLayout = new QGridLayout;
0878     lVerificationLayout->setContentsMargins(0, 0, 0, 0);
0879     lVerificationLayout->setSpacing(0);
0880     lVerificationLayout->setColumnMinimumWidth(0, lIndentation);
0881     lVerificationLayout->addWidget(lVerificationCheckBox,0, 0, 1, 2);
0882     lVerificationLayout->addWidget(lVerificationLabel, 1, 1);
0883     lVerificationWidget->setLayout(lVerificationLayout);
0884     connect(mVersionedRadio, SIGNAL(toggled(bool)), lVerificationWidget, SLOT(setVisible(bool)));
0885 
0886     auto lExcludesWidget = new QWidget;
0887     auto lExcludesCheckBox = new QCheckBox(xi18nc("@option:check", "Exclude files and folders based on patterns"));
0888     lExcludesCheckBox->setObjectName(QStringLiteral("kcfg_Exclude patterns"));
0889 
0890     auto lExcludesLabel = new QLabel();
0891     lExcludesLabel->setWordWrap(true);
0892     lExcludesLabel->setTextFormat(Qt::RichText);
0893     lExcludesLabel->setTextInteractionFlags(Qt::TextBrowserInteraction);
0894     connect(lExcludesLabel, &QLabel::linkActivated, this, [](QString pLink){
0895         // open the URL ourselves instead of QLabel and QDesktopService doing it, just
0896         // so that we can give a .html file ending to the temp file. Required for QWebEngine
0897         // to understand that the file has html content. Firefox works either way.
0898         auto *job = new KIO::OpenUrlJob(QUrl(pLink));
0899         job->setSuggestedFileName("manpage.html");
0900         job->start();
0901     });
0902     auto lLabelUpdater = [lExcludesLabel](bool pVersioned){
0903         QString lHelpUrl = pVersioned ? QStringLiteral("man:///bup-index") : QStringLiteral("man:///rsync");
0904         lExcludesLabel->setText(xi18nc("@info",
0905                                        "Patterns need to be listed in a text file with one pattern per line. "
0906                                        "Files and folders with names that match any of the patterns will be "
0907                                        "excluded from the backup. The pattern format is documented <link url='%1'>here</link>.",
0908                                        lHelpUrl));
0909     };
0910     lLabelUpdater(false);
0911     connect(mVersionedRadio, &QCheckBox::toggled, this, lLabelUpdater);
0912     auto lExcludesEdit = new KLineEdit;
0913     lExcludesEdit->setObjectName(QStringLiteral("kcfg_Exclude patterns file path"));
0914     lExcludesEdit->setEnabled(false);
0915     lExcludesEdit->setClearButtonEnabled(true);
0916     lExcludesEdit->setCompletionObject(new KUrlCompletion);
0917     lExcludesEdit->setAutoDeleteCompletionObject(true);
0918     auto lExcludesButton = new QPushButton;
0919     lExcludesButton->setIcon(QIcon::fromTheme(QStringLiteral("document-open")));
0920     int lButtonSize = lExcludesButton->sizeHint().expandedTo(lExcludesEdit->sizeHint()).height();
0921     lExcludesButton->setFixedSize(lButtonSize, lButtonSize);
0922     lExcludesButton->setEnabled(false);
0923     lExcludesButton->setToolTip(xi18nc("@info:tooltip", "Open dialog to select a file"));
0924     connect(lExcludesButton, &QPushButton::clicked, this, [this,lExcludesEdit]{
0925         QString lNewFilePath = QFileDialog::getOpenFileName(this, i18n("Select pattern file"), lExcludesEdit->text());
0926         if(!lNewFilePath.isEmpty()) {
0927             lExcludesEdit->setText(lNewFilePath);
0928         }
0929     });
0930     connect(lExcludesCheckBox, &QCheckBox::toggled, lExcludesEdit, &KLineEdit::setEnabled);
0931     connect(lExcludesCheckBox, &QCheckBox::toggled, lExcludesButton, &QPushButton::setEnabled);
0932     auto lExcludesLayout = new QGridLayout;
0933     lExcludesLayout->setContentsMargins(0, 0, 0, 0);
0934     lExcludesLayout->setSpacing(0);
0935     lExcludesLayout->setColumnMinimumWidth(0, lIndentation);
0936     lExcludesLayout->addWidget(lExcludesCheckBox, 0, 0, 1, 3);
0937     lExcludesLayout->addWidget(lExcludesLabel, 1, 1, 1, 2);
0938     lExcludesLayout->addWidget(lExcludesEdit, 2, 1);
0939     lExcludesLayout->addWidget(lExcludesButton, 2, 2);
0940     lExcludesWidget->setLayout(lExcludesLayout);
0941 
0942     lAdvancedLayout->addWidget(lShowHiddenWidget);
0943     lAdvancedLayout->addWidget(lVerificationWidget);
0944     lAdvancedLayout->addWidget(lRecoveryWidget);
0945     lAdvancedLayout->addWidget(lExcludesWidget);
0946     lAdvancedLayout->addStretch();
0947     lAdvancedLayout->setSpacing(lIndentation);
0948     lAdvancedWidget->setLayout(lAdvancedLayout);
0949     auto lPage = new KPageWidgetItem(lAdvancedWidget);
0950     lPage->setName(xi18nc("@title", "Advanced"));
0951     lPage->setHeader(xi18nc("@label", "Extra options for advanced users"));
0952     lPage->setIcon(QIcon::fromTheme(QStringLiteral("preferences-other")));
0953     return lPage;
0954 }
0955 
0956 void BackupPlanWidget::openDriveDestDialog() {
0957     QString lMountPoint = mDriveSelection->mountPathOfSelectedDrive();
0958     QPointer<DirDialog> lDirDialog = new DirDialog(QUrl::fromLocalFile(lMountPoint), mDriveDestEdit->text(), this);
0959     if(lDirDialog->exec() == QDialog::Accepted) {
0960         QString lSelectedPath = lDirDialog->url().path();
0961         lSelectedPath.remove(0, lMountPoint.length());
0962         while(lSelectedPath.startsWith(QLatin1Char('/'))) {
0963             lSelectedPath.remove(0, 1);
0964         }
0965         mDriveDestEdit->setText(lSelectedPath);
0966     }
0967     delete lDirDialog;
0968 }
0969 
0970 void BackupPlanWidget::checkFilesystemDestination(const QString &pDestination) {
0971     if(!pDestination.startsWith("/") && !pDestination.startsWith("file:") &&
0972             pDestination.contains(":/")) {
0973         mLocalMessage->animatedShow();
0974     } else {
0975         mLocalMessage->animatedHide();
0976     }
0977 
0978     QDir lDestinationDir(QDir::home().absoluteFilePath(pDestination));
0979     if(!lDestinationDir.exists()) {
0980         auto lAction = new QAction(xi18nc("@action:button", "Create Folder"), this);
0981         connect(lAction, &QAction::triggered, this, [this, lDestinationDir](){
0982             lDestinationDir.mkpath(lDestinationDir.absolutePath());
0983             checkFilesystemDestination(lDestinationDir.absolutePath());
0984         });
0985         mExistMessage->clearActions();
0986         mExistMessage->addAction(lAction);
0987         if(mExistMessage->isHidden()) {
0988             mExistMessage->animatedShow();
0989         } else {
0990             // Work around for buggy layout when removing and adding action.
0991             mExistMessage->hide();
0992             mExistMessage->show();
0993         }
0994     } else {
0995         mExistMessage->animatedHide();
0996     }
0997 }