[Companion] SD Sync reworked: (#4906)
* Complete re-write of SyncProcess, now more robust, UI-independent, runs in own thread, large speed boost;
* Use file hash comparisons instead of byte-level compare with string methods (probable fixes for #4837 and #4848);
* New pre-sync options dialog with selectable folders and other settings (settings are remembered until app exit);
* Option to choose sync direction(s) (up/down/bi) and the order in which bi-directional sync runs;
* Option to choose file comparison mode (timestamp/hash/both) or force overwrite (for one-way sync);
* Option to set a maximum file size to copy;
* Option to do a test run w/out actually copying any files;
* Better reporting of progress, total counts (copied/updated/skipped), and errors, with color coding;
* Much more reliable to cancel long-running/hung copy process;
* ProgressWidget can now display rich-text in the text browser, added some new functions for usage;
* Misc. small layout tweaks to ProgressDialog and ProgressWidget to make them behave/look better;
This commit is contained in:
committed by
Bertrand Songis
parent
ca25696d1a
commit
c1586b72bb
+245
-14
@@ -44,6 +44,7 @@
|
||||
#include "process_sync.h"
|
||||
#include "radiointerface.h"
|
||||
#include "progressdialog.h"
|
||||
#include "progresswidget.h"
|
||||
#include "storage.h"
|
||||
#include "translations.h"
|
||||
|
||||
@@ -735,24 +736,254 @@ void MainWindow::contributors()
|
||||
dialog->deleteLater();
|
||||
}
|
||||
|
||||
// Create a widget with a line edit and folder select button and handles all interactions. Features autosuggest
|
||||
// path hints while typing, invalid paths shown in red. The label string is only for dialog title, not a QLabel.
|
||||
// This should probably be moved some place more reusable, esp. the QFileSystemModel.
|
||||
QWidget * folderSelectorWidget(QString * path, const QString & label, QWidget * parent)
|
||||
{
|
||||
static QFileSystemModel fileModel;
|
||||
static bool init = false;
|
||||
if (!init) {
|
||||
init = true;
|
||||
fileModel.setFilter(QDir::Dirs);
|
||||
fileModel.setRootPath("/");
|
||||
}
|
||||
|
||||
QWidget * fsw = new QWidget(parent);
|
||||
QLineEdit * le = new QLineEdit(parent);
|
||||
QCompleter * fsc = new QCompleter(fsw);
|
||||
fsc->setModel(&fileModel);
|
||||
//fsc->setCompletionMode(QCompleter::InlineCompletion);
|
||||
le->setCompleter(fsc);
|
||||
|
||||
QToolButton * btn = new QToolButton(fsw);
|
||||
btn->setIcon(CompanionIcon("open.png"));
|
||||
QHBoxLayout * l = new QHBoxLayout(fsw);
|
||||
l->setContentsMargins(0,0,0,0);
|
||||
l->setSpacing(3);
|
||||
l->addWidget(le);
|
||||
l->addWidget(btn);
|
||||
|
||||
QObject::connect(btn, &QToolButton::clicked, [=]() {
|
||||
QString dir = QFileDialog::getExistingDirectory(parent, label, le->text(), 0);
|
||||
if (!dir.isEmpty()) {
|
||||
le->setText(QDir::toNativeSeparators(dir));
|
||||
le->setFocus();
|
||||
}
|
||||
});
|
||||
|
||||
QObject::connect(le, &QLineEdit::textChanged, [=](const QString & text) {
|
||||
*path = text;
|
||||
if (QFile::exists(text))
|
||||
le->setStyleSheet("");
|
||||
else
|
||||
le->setStyleSheet("QLineEdit {color: red;}");
|
||||
});
|
||||
le->setText(QDir::toNativeSeparators(*path));
|
||||
|
||||
return fsw;
|
||||
}
|
||||
|
||||
void MainWindow::sdsync()
|
||||
{
|
||||
QString sdPath = g.profile[g.id()].sdPath();
|
||||
if (sdPath.isEmpty()) {
|
||||
QMessageBox::warning(this, QObject::tr("Synchronization error"), QObject::tr("No SD directory configured!"));
|
||||
const QString dlgTtl = tr("Synchronize SD");
|
||||
const QIcon dlgIcn = CompanionIcon("sdsync.png");
|
||||
const QString srcArw = CPN_STR_SW_INDICATOR_UP % " ";
|
||||
const QString dstArw = CPN_STR_SW_INDICATOR_DN % " ";
|
||||
QStringList errorMsgs;
|
||||
|
||||
// remember user-selectable options for duration of session
|
||||
static QString sourcePath;
|
||||
static QString destPath;
|
||||
static int syncDirection = SyncProcess::SYNC_A2B_B2A;
|
||||
static int compareType = SyncProcess::OVERWR_NEWER_IF_DIFF;
|
||||
static int maxFileSize = 2 * 1024 * 1024; // Bytes
|
||||
static bool dryRun = false;
|
||||
|
||||
if (sourcePath.isEmpty())
|
||||
sourcePath = g.profile[g.id()].sdPath();
|
||||
if (destPath.isEmpty())
|
||||
destPath = findMassstoragePath("SOUNDS").replace(QRegExp("[/\\\\]?SOUNDS"), "");
|
||||
|
||||
if (sourcePath.isEmpty())
|
||||
errorMsgs << tr("No local SD structure path configured!");
|
||||
if (destPath.isEmpty())
|
||||
errorMsgs << tr("No Radio or SD card detected!");
|
||||
|
||||
QDialog dlg(this);
|
||||
dlg.setWindowTitle(dlgTtl % tr(" :: Options"));
|
||||
dlg.setWindowIcon(dlgIcn);
|
||||
dlg.setSizeGripEnabled(true);
|
||||
dlg.setWindowFlags(dlg.windowFlags() & ~Qt::WindowContextHelpButtonHint);
|
||||
|
||||
QLabel * lblSrc = new QLabel(tr("Local Folder:"), &dlg);
|
||||
QWidget * wdgSrc = folderSelectorWidget(&sourcePath, lblSrc->text(), &dlg);
|
||||
|
||||
QLabel * lblDst = new QLabel(tr("Radio Folder:"), &dlg);
|
||||
QWidget * wdgDst = folderSelectorWidget(&destPath, lblDst->text(), &dlg);
|
||||
|
||||
QLabel * lbDir = new QLabel(tr("Sync. Direction:"), &dlg);
|
||||
QComboBox * syncDir = new QComboBox(&dlg);
|
||||
syncDir->addItem(tr("%1%2 Both directions, to radio folder first").arg(dstArw, srcArw), SyncProcess::SYNC_A2B_B2A);
|
||||
syncDir->addItem(tr("%1%2 Both directions, to local folder first").arg(srcArw, dstArw), SyncProcess::SYNC_B2A_A2B);
|
||||
syncDir->addItem(tr(" %1 Only from local folder to radio folder").arg(dstArw), SyncProcess::SYNC_A2B);
|
||||
syncDir->addItem(tr(" %1 Only from radio folder to local folder").arg(srcArw), SyncProcess::SYNC_B2A);
|
||||
syncDir->setCurrentIndex(-1); // we set the default option later
|
||||
|
||||
QLabel * lbMode = new QLabel(tr("Existing Files:"), &dlg);
|
||||
QComboBox * copyMode = new QComboBox(&dlg);
|
||||
copyMode->setToolTip(tr("How to handle overwriting files which already exist in the destination folder."));
|
||||
copyMode->addItem(tr("Copy only if newer and different (compare contents)"), SyncProcess::OVERWR_NEWER_IF_DIFF);
|
||||
copyMode->addItem(tr("Copy only if newer (do not compare contents)"), SyncProcess::OVERWR_NEWER_ALWAYS);
|
||||
copyMode->addItem(tr("Copy only if different (ignore file time stamps)"), SyncProcess::OVERWR_IF_DIFF);
|
||||
copyMode->addItem(tr("Always copy (force overwite existing files)"), SyncProcess::OVERWR_ALWAYS);
|
||||
|
||||
QLabel * lbSize = new QLabel(tr("Max. File Size:"), &dlg);
|
||||
QSpinBox * maxSize = new QSpinBox(&dlg);
|
||||
maxSize->setRange(0, 100 * 1024);
|
||||
maxSize->setAccelerated(true);
|
||||
maxSize->setSpecialValueText(tr("Any size"));
|
||||
maxSize->setToolTip(tr("Skip files larger than this size. Enter zero for unlimited."));
|
||||
#if (QT_VERSION >= QT_VERSION_CHECK(5, 3, 0))
|
||||
maxSize->setGroupSeparatorShown(true);
|
||||
#endif
|
||||
|
||||
QCheckBox * testRun = new QCheckBox(tr("Test-run only"), &dlg);
|
||||
testRun->setToolTip(tr("Run as normal but do not actually copy anything. Useful for verifying results before real sync."));
|
||||
connect(testRun, &QCheckBox::toggled, [=](bool on) { dryRun = on; });
|
||||
|
||||
// layout to hold size spinbox and checkbox option(s)
|
||||
QHBoxLayout * hlay1 = new QHBoxLayout();
|
||||
hlay1->addWidget(maxSize, 1);
|
||||
hlay1->addWidget(testRun);
|
||||
|
||||
// dialog OK/Cancel buttons
|
||||
QDialogButtonBox * bb = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, &dlg);
|
||||
|
||||
// Create main layout and add everything
|
||||
QGridLayout * dlgL = new QGridLayout(&dlg);
|
||||
dlgL->setSizeConstraint(QLayout::SetFixedSize);
|
||||
int row = 0;
|
||||
if (errorMsgs.size()) {
|
||||
QLabel * lblWarn = new QLabel(QString(errorMsgs.join('\n')), &dlg);
|
||||
lblWarn->setStyleSheet("QLabel { color: red; }");
|
||||
dlgL->addWidget(lblWarn, row++, 0, 1, 2);
|
||||
}
|
||||
dlgL->addWidget(lblSrc, row, 0);
|
||||
dlgL->addWidget(wdgSrc, row++, 1);
|
||||
dlgL->addWidget(lblDst, row, 0);
|
||||
dlgL->addWidget(wdgDst, row++, 1);
|
||||
dlgL->addWidget(lbDir, row, 0);
|
||||
dlgL->addWidget(syncDir, row++, 1);
|
||||
dlgL->addWidget(lbMode, row, 0);
|
||||
dlgL->addWidget(copyMode, row++, 1);
|
||||
dlgL->addWidget(lbSize, row, 0);
|
||||
dlgL->addLayout(hlay1, row++, 1);
|
||||
dlgL->addWidget(bb, row++, 0, 1, 2);
|
||||
dlgL->setRowStretch(row, 1);
|
||||
|
||||
connect(copyMode, static_cast<void(QComboBox::*)(int)>(&QComboBox::currentIndexChanged), [=](int) {
|
||||
compareType = copyMode->currentData().toInt();
|
||||
});
|
||||
|
||||
// function to dis/enable the OVERWR_ALWAYS option depending on sync direction
|
||||
connect(syncDir, static_cast<void(QComboBox::*)(int)>(&QComboBox::currentIndexChanged), [=](int) {
|
||||
int dir = syncDir->currentData().toInt();
|
||||
int idx = copyMode->findData(SyncProcess::OVERWR_ALWAYS);
|
||||
int flg = (dir == SyncProcess::SYNC_A2B || dir == SyncProcess::SYNC_B2A) ? 33 : 0;
|
||||
if (!flg && idx == copyMode->currentIndex())
|
||||
copyMode->setCurrentIndex(copyMode->findData(SyncProcess::OVERWR_NEWER_IF_DIFF));
|
||||
copyMode->setItemData(idx, flg, Qt::UserRole - 1);
|
||||
syncDirection = dir;
|
||||
});
|
||||
|
||||
// function to set magnitude of file size spinbox, KB or MB
|
||||
connect(maxSize, static_cast<void(QSpinBox::*)(int)>(&QSpinBox::valueChanged), [=](int value) {
|
||||
int multi = maxSize->property("multi").isValid() ? maxSize->property("multi").toInt() : 0;
|
||||
maxSize->blockSignals(true);
|
||||
if (value >= 10 * 1024 && multi != 1024 * 1024) {
|
||||
// KB -> MB
|
||||
multi = 1024 * 1024;
|
||||
maxSize->setValue(value / 1024);
|
||||
maxSize->setMaximum(100);
|
||||
maxSize->setSingleStep(1);
|
||||
maxSize->setSuffix(tr(" MB"));
|
||||
}
|
||||
else if ((value < 10 && multi != 1024) || !multi) {
|
||||
// MB -> KB
|
||||
if (multi)
|
||||
value *= 1024;
|
||||
multi = 1024;
|
||||
if (value == 9 * 1024)
|
||||
value += 1024 - 32; // avoid large jump when stepping from 10MB to 10,208KB
|
||||
maxSize->setMaximum(100 * 1024);
|
||||
maxSize->setValue(value);
|
||||
maxSize->setSingleStep(32);
|
||||
maxSize->setSuffix(tr(" KB"));
|
||||
}
|
||||
maxSize->setProperty("multi", multi);
|
||||
maxSize->blockSignals(false);
|
||||
maxFileSize = value * multi;
|
||||
});
|
||||
|
||||
copyMode->setCurrentIndex(copyMode->findData(compareType));
|
||||
syncDir->setCurrentIndex(syncDir->findData(syncDirection));
|
||||
maxSize->setValue(maxFileSize / 1024);
|
||||
testRun->setChecked(dryRun);
|
||||
|
||||
connect(bb, &QDialogButtonBox::accepted, &dlg, &QDialog::accept);
|
||||
connect(bb, &QDialogButtonBox::rejected, &dlg, &QDialog::reject);
|
||||
|
||||
// to restart dialog on error/etc
|
||||
openDialog:
|
||||
|
||||
// show the modal options dialog
|
||||
if (dlg.exec() == QDialog::Rejected)
|
||||
return;
|
||||
|
||||
// validate
|
||||
errorMsgs.clear();
|
||||
if (sourcePath == destPath)
|
||||
errorMsgs << tr("Source and destination folders are the same!");
|
||||
if (sourcePath.isEmpty() || !QFile::exists(sourcePath))
|
||||
errorMsgs << tr("Source folder not found: %1").arg(sourcePath);
|
||||
if (destPath.isEmpty() || !QFile::exists(destPath))
|
||||
errorMsgs << tr("Destination folder not found: %1").arg(destPath);
|
||||
|
||||
if (!errorMsgs.isEmpty()) {
|
||||
QMessageBox::warning(this, dlgTtl % tr(" :: Error"), errorMsgs.join('\n'));
|
||||
goto openDialog;
|
||||
}
|
||||
QString massstoragePath = findMassstoragePath("SOUNDS");
|
||||
if (massstoragePath.isEmpty()) {
|
||||
QMessageBox::warning(this, QObject::tr("Synchronization error"), QObject::tr("No Radio connected!"));
|
||||
return;
|
||||
}
|
||||
massstoragePath = massstoragePath.left(massstoragePath.length() - 7);
|
||||
ProgressDialog progressDialog(this, tr("Synchronize SD"), CompanionIcon("sdsync.png"));
|
||||
SyncProcess syncProcess(massstoragePath, g.profile[g.id()].sdPath(), progressDialog.progress());
|
||||
if (!syncProcess.run()) {
|
||||
progressDialog.exec();
|
||||
}
|
||||
|
||||
// set up the progress dialog and the sync process worker
|
||||
ProgressDialog * progressDlg = new ProgressDialog(this, dlgTtl % tr(" :: Progress"), dlgIcn);
|
||||
progressDlg->setAttribute(Qt::WA_DeleteOnClose, true);
|
||||
ProgressWidget * progWidget = progressDlg->progress();
|
||||
SyncProcess * syncProcess = new SyncProcess(sourcePath, destPath, syncDirection, compareType, maxFileSize, dryRun);
|
||||
|
||||
// move sync process to separate thread, we only use signals/slots from here on...
|
||||
QThread * syncThread = new QThread(this);
|
||||
syncProcess->moveToThread(syncThread);
|
||||
|
||||
// ...and quite a few of them!
|
||||
connect(this, &MainWindow::startSync, syncProcess, &SyncProcess::run);
|
||||
connect(syncThread, &QThread::finished, syncProcess, &SyncProcess::deleteLater);
|
||||
connect(syncProcess, &SyncProcess::finished, syncThread, &QThread::quit);
|
||||
connect(syncProcess, &SyncProcess::destroyed, syncThread, &QThread::quit);
|
||||
connect(syncProcess, &SyncProcess::destroyed, syncThread, &QThread::deleteLater);
|
||||
connect(syncProcess, &SyncProcess::fileCountChanged, progWidget, &ProgressWidget::setMaximum);
|
||||
connect(syncProcess, &SyncProcess::progressStep, progWidget, &ProgressWidget::setValue);
|
||||
connect(syncProcess, &SyncProcess::progressMessage, progWidget, &ProgressWidget::addMessage);
|
||||
connect(syncProcess, &SyncProcess::statusMessage, progWidget, &ProgressWidget::setInfo);
|
||||
connect(syncProcess, &SyncProcess::started, progressDlg, &ProgressDialog::setProcessStarted);
|
||||
connect(syncProcess, &SyncProcess::finished, progressDlg, &ProgressDialog::setProcessStopped);
|
||||
connect(syncProcess, &SyncProcess::finished, [=]() { QApplication::alert(this); });
|
||||
connect(progressDlg, &ProgressDialog::rejected, syncProcess, &SyncProcess::stop);
|
||||
connect(progressDlg, &ProgressDialog::rejected, syncProcess, &SyncProcess::deleteLater);
|
||||
|
||||
// go (finally)
|
||||
syncThread->start();
|
||||
emit startSync();
|
||||
}
|
||||
|
||||
void MainWindow::changelog()
|
||||
|
||||
Reference in New Issue
Block a user