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 }