Merge pull request #6009 from opentx/mpaperno/cpn_fsync_v2
SD Sync "version 3"
This commit is contained in:
@@ -246,6 +246,7 @@ set(companion_SRCS
|
||||
wizarddata.cpp
|
||||
wizarddialog.cpp
|
||||
styleeditdialog.cpp
|
||||
dialogs/filesyncdialog.cpp
|
||||
)
|
||||
|
||||
set(companion_MOC_HDRS
|
||||
@@ -279,6 +280,7 @@ set(companion_MOC_HDRS
|
||||
modelslist.h
|
||||
styleeditdialog.h
|
||||
helpers_html.h
|
||||
dialogs/filesyncdialog.h
|
||||
)
|
||||
|
||||
set(companion_UIS
|
||||
|
||||
@@ -0,0 +1,502 @@
|
||||
/*
|
||||
* Copyright (C) OpenTX
|
||||
*
|
||||
* Based on code named
|
||||
* th9x - http://code.google.com/p/th9x
|
||||
* er9x - http://code.google.com/p/er9x
|
||||
* gruvin9x - http://code.google.com/p/gruvin9x
|
||||
*
|
||||
* License GPLv2: http://www.gnu.org/licenses/gpl-2.0.html
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License version 2 as
|
||||
* published by the Free Software Foundation.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*/
|
||||
|
||||
#include "filesyncdialog.h"
|
||||
|
||||
#include "autobitsetcheckbox.h"
|
||||
#include "autocheckbox.h"
|
||||
#include "autocombobox.h"
|
||||
#include "autolineedit.h"
|
||||
#include "constants.h"
|
||||
#include "helpers.h"
|
||||
#include "progresswidget.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QComboBox>
|
||||
#include <QCompleter>
|
||||
#include <QCheckBox>
|
||||
#include <QDir>
|
||||
#include <QFileDialog>
|
||||
#include <QFileSystemModel>
|
||||
#include <QLayout>
|
||||
#include <QLineEdit>
|
||||
#include <QMenu>
|
||||
#include <QSpinBox>
|
||||
|
||||
FileSyncDialog::FileSyncDialog(QWidget * parent, const SyncProcess::SyncOptions & syncOptions) :
|
||||
QDialog(parent),
|
||||
m_syncOptions(syncOptions),
|
||||
m_running(false)
|
||||
{
|
||||
setWindowTitle(tr("Synchronize Files"));
|
||||
setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
|
||||
setSizeGripEnabled(true);
|
||||
|
||||
setupUi();
|
||||
toggleExtraOptions(false);
|
||||
validate();
|
||||
updateRunningState();
|
||||
}
|
||||
|
||||
void FileSyncDialog::reject()
|
||||
{
|
||||
// override close event via window system close or esc. key
|
||||
if (m_running) {
|
||||
if (QMessageBox::question(this, CPN_STR_APP_NAME, tr("Are you sure you wish to abort the sync?")) == QMessageBox::No)
|
||||
return;
|
||||
emit stopSync();
|
||||
// Force termination if the sync hasn't finished within 5s
|
||||
QElapsedTimer tim;
|
||||
tim.start();
|
||||
while (!tim.hasExpired(5000) && m_running)
|
||||
QApplication::processEvents(QEventLoop::ExcludeUserInputEvents, 10);
|
||||
if (m_running)
|
||||
qWarning() << "Terminating thread while Sync process was still running.";
|
||||
}
|
||||
QDialog::reject();
|
||||
}
|
||||
|
||||
// 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.
|
||||
// This should probably be moved some place more reusable, esp. the QFileSystemModel.
|
||||
static QWidget * folderSelectorWidget(QLineEdit * le, QWidget * parent)
|
||||
{
|
||||
static QFileSystemModel fileModel;
|
||||
static bool init = false;
|
||||
if (!init) {
|
||||
init = true;
|
||||
fileModel.setFilter(QDir::Dirs);
|
||||
fileModel.setRootPath("/");
|
||||
}
|
||||
|
||||
QWidget * fsw = new QWidget(parent);
|
||||
//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, fsw->property("fileDialogTitle").toString(), le->text(), 0);
|
||||
if (!dir.isEmpty()) {
|
||||
le->setText(QDir::toNativeSeparators(dir));
|
||||
le->setFocus();
|
||||
}
|
||||
});
|
||||
|
||||
QObject::connect(le, &QLineEdit::textChanged, [=](const QString & text) {
|
||||
if (QFile::exists(text))
|
||||
le->setStyleSheet("");
|
||||
else
|
||||
le->setStyleSheet("QLineEdit {color: red;}");
|
||||
});
|
||||
|
||||
return fsw;
|
||||
}
|
||||
|
||||
void FileSyncDialog::setupUi()
|
||||
{
|
||||
const QString srcArw = CPN_STR_SW_INDICATOR_UP % " ";
|
||||
const QString dstArw = CPN_STR_SW_INDICATOR_DN % " ";
|
||||
int row = 0;
|
||||
|
||||
CompanionIcon playIcon("play.png");
|
||||
playIcon.addImage("stop.png", QIcon::Normal, QIcon::On);
|
||||
|
||||
QLabel * lblSrc = new QLabel(tr("Source Folder:"), this);
|
||||
AutoLineEdit * leSrc = new AutoLineEdit(this, true);
|
||||
leSrc->setField(m_syncOptions.folderA);
|
||||
leSrc->setText(QDir::toNativeSeparators(m_syncOptions.folderA));
|
||||
QWidget * wdgSrc = folderSelectorWidget(leSrc, this);
|
||||
|
||||
QLabel * lblDst = new QLabel(tr("Destination Folder:"), this);
|
||||
AutoLineEdit * leDst = new AutoLineEdit(this, true);
|
||||
leDst->setField(m_syncOptions.folderB);
|
||||
leDst->setText(QDir::toNativeSeparators(m_syncOptions.folderB));
|
||||
QWidget * wdgDst = folderSelectorWidget(leDst, this);
|
||||
|
||||
AutoComboBox * syncDir = new AutoComboBox(this);
|
||||
syncDir->addItem(tr("%1%2 Both directions, to destination folder first").arg(dstArw, srcArw), SyncProcess::SYNC_A2B_B2A);
|
||||
syncDir->addItem(tr("%1%2 Both directions, to source folder first").arg(srcArw, dstArw), SyncProcess::SYNC_B2A_A2B);
|
||||
syncDir->addItem(tr(" %1 Only from source folder to destination folder").arg(dstArw), SyncProcess::SYNC_A2B);
|
||||
syncDir->addItem(tr(" %1 Only from destination folder to source folder").arg(srcArw), SyncProcess::SYNC_B2A);
|
||||
syncDir->setCurrentIndex(-1); // we set the default option later
|
||||
|
||||
AutoComboBox * copyMode = new AutoComboBox(this);
|
||||
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);
|
||||
copyMode->setField(m_syncOptions.compareType);
|
||||
|
||||
QSpinBox * maxSize = new QSpinBox(this);
|
||||
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."));
|
||||
maxSize->setGroupSeparatorShown(true);
|
||||
maxSize->setValue(0); // we set the real default value later
|
||||
|
||||
AutoComboBox * logLevel = new AutoComboBox(this);
|
||||
logLevel->setToolTip(tr("Minimum reporting level. Events of this type and of higher importance are shown.\n" \
|
||||
"WARNING: High log rates may make the user interface temporarily unresponsive."));
|
||||
logLevel->addItem(tr("Skipped"), QtDebugMsg);
|
||||
logLevel->addItem(tr("Created"), QtInfoMsg);
|
||||
logLevel->addItem(tr("Updated"), QtWarningMsg);
|
||||
//logLevel->addItem(tr("Item Deleted"), QtCriticalMsg); // unused
|
||||
logLevel->addItem(tr("Errors Only"), QtFatalMsg);
|
||||
logLevel->setField(m_syncOptions.logLevel);
|
||||
|
||||
AutoBitsetCheckBox * testRun = new AutoBitsetCheckBox(m_syncOptions.flags, SyncProcess::OPT_DRY_RUN, tr("Test-run only"), this);
|
||||
testRun->setToolTip(tr("Run as normal but do not actually copy anything. Useful for verifying results before real sync."));
|
||||
|
||||
// layout to hold size spinbox and checkbox option(s)
|
||||
QHBoxLayout * hlay1 = new QHBoxLayout();
|
||||
hlay1->addWidget(maxSize, 1);
|
||||
hlay1->addWidget(testRun);
|
||||
hlay1->addWidget(new QLabel(tr("Log Level:"), this));
|
||||
hlay1->addWidget(logLevel);
|
||||
|
||||
// "extra" options
|
||||
|
||||
QLabel * lbFilter = new QLabel(tr("Filters:"), this);
|
||||
lbFilter->setToolTip(tr("The \"Include\" filter will only copy files which match the pattern(s).\n" \
|
||||
"The \"Exclude\" filter will skip files matching the filter pattern(s).\n" \
|
||||
"The Include filter is evaluated first."));
|
||||
|
||||
AutoLineEdit * leFiltExc = new AutoLineEdit(this, true);
|
||||
leFiltExc->setField(m_syncOptions.excludeFilter);
|
||||
leFiltExc->setToolTip(tr("One or more file pattern(s) to exclude, separated by commas.\n" "Blank means exclude none. ?, *, and [...] wildcards accepted."));
|
||||
|
||||
AutoLineEdit * leFiltInc = new AutoLineEdit(this, true);
|
||||
leFiltInc->setField(m_syncOptions.includeFilter);
|
||||
leFiltInc->setToolTip(tr("One or more file pattern(s) to include, separated by commas.\n" "Blank means include all. ?, *, and [...] wildcards accepted."));
|
||||
|
||||
// layout to hold filter options
|
||||
QHBoxLayout * filtLo = new QHBoxLayout();
|
||||
filtLo->addWidget(new QLabel(tr("Include:"), this));
|
||||
filtLo->addWidget(leFiltInc, 1);
|
||||
filtLo->addWidget(new QLabel(tr("Exclude:"), this));
|
||||
filtLo->addWidget(leFiltExc, 1);
|
||||
|
||||
// file options
|
||||
QHBoxLayout * filOptsLo = new QHBoxLayout();
|
||||
filOptsLo->addWidget(new AutoBitsetCheckBox(m_syncOptions.dirFilterFlags, QDir::CaseSensitive, tr("Case sensitive"), this));
|
||||
filOptsLo->addWidget(new AutoBitsetCheckBox(m_syncOptions.dirFilterFlags, QDir::NoSymLinks, true, tr("Follow links"), this));
|
||||
filOptsLo->addWidget(new AutoBitsetCheckBox(m_syncOptions.dirFilterFlags, QDir::Hidden, tr("Include hidden"), this));
|
||||
|
||||
// folder options
|
||||
QHBoxLayout * dirOptsLo = new QHBoxLayout();
|
||||
dirOptsLo->addWidget(new AutoBitsetCheckBox(m_syncOptions.flags, SyncProcess::OPT_RECURSIVE, tr("Recursive"), this));
|
||||
dirOptsLo->addWidget(new AutoBitsetCheckBox(m_syncOptions.flags, SyncProcess::OPT_SKIP_DIR_LINKS, true, tr("Follow links"), this));
|
||||
dirOptsLo->addWidget(new AutoBitsetCheckBox(m_syncOptions.dirFilterFlags, QDir::Dirs, true, tr("Skip empty"), this));
|
||||
dirOptsLo->addWidget(new AutoBitsetCheckBox(m_syncOptions.dirFilterFlags, QDir::AllDirs, true, tr("Apply filters"), this));
|
||||
|
||||
// extra options container widget and layout
|
||||
ui_extraOptions = new QWidget(this);
|
||||
ui_extraOptions->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Minimum);
|
||||
|
||||
QGridLayout * xOptsLay = new QGridLayout(ui_extraOptions);
|
||||
xOptsLay->setContentsMargins(0, 0, 0, 0);
|
||||
xOptsLay->addWidget(lbFilter, row, 0);
|
||||
xOptsLay->addLayout(filtLo, row++, 1);
|
||||
xOptsLay->addWidget(new QLabel(tr("Filter Options:"), this), row, 0);
|
||||
xOptsLay->addLayout(filOptsLo, row++, 1);
|
||||
xOptsLay->addWidget(new QLabel(tr("Folder Options:"), this), row, 0);
|
||||
xOptsLay->addLayout(dirOptsLo, row++, 1);
|
||||
|
||||
// status label
|
||||
ui_statusLabel = new QLabel(this);
|
||||
// progress widget
|
||||
ui_progress = new ProgressWidget(this);
|
||||
ui_progress->setMinimumWidth(0);
|
||||
ui_progress->setVisible(false);
|
||||
|
||||
// dialog buttons & menu
|
||||
|
||||
QMenu * optionsMenu = new QMenu(tr("Options"));
|
||||
QAction * xtrasAct = optionsMenu->addAction(CompanionIcon("fuses.png"), tr("Show extra options"), this, &FileSyncDialog::toggleExtraOptions);
|
||||
xtrasAct->setCheckable(true);
|
||||
connect(this, &FileSyncDialog::extraOptionsToggled, xtrasAct, &QAction::setChecked);
|
||||
optionsMenu->addAction(CompanionIcon("paintbrush.png"), tr("Reset to defaults"), this, &FileSyncDialog::resetOptions);
|
||||
|
||||
QToolButton * btnOpts = new QToolButton(this);
|
||||
btnOpts->setDefaultAction(xtrasAct);
|
||||
btnOpts->setMenu(optionsMenu);
|
||||
btnOpts->setToolButtonStyle(Qt::ToolButtonIconOnly);
|
||||
btnOpts->setPopupMode(QToolButton::MenuButtonPopup);
|
||||
btnOpts->setToolTip(xtrasAct->text());
|
||||
|
||||
ui_btnStartStop = new QToolButton(this);
|
||||
ui_btnStartStop->setCheckable(true);
|
||||
ui_btnStartStop->setIcon(playIcon);
|
||||
ui_btnStartStop->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
|
||||
|
||||
ui_btnClose = new QToolButton(this);
|
||||
ui_btnClose->setText(tr("Close"));
|
||||
ui_btnClose->setToolButtonStyle(Qt::ToolButtonTextOnly);
|
||||
ui_btnClose->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Expanding);
|
||||
|
||||
ui_buttonBox = new QDialogButtonBox(this);
|
||||
ui_buttonBox->addButton(btnOpts, QDialogButtonBox::ActionRole);
|
||||
ui_buttonBox->addButton(ui_btnStartStop, QDialogButtonBox::ActionRole);
|
||||
ui_buttonBox->addButton(ui_btnClose, QDialogButtonBox::RejectRole);
|
||||
|
||||
// The options group box.
|
||||
ui_optionsPanel = new QGroupBox(tr("Options"), this);
|
||||
QGridLayout * optsLay = new QGridLayout(ui_optionsPanel);
|
||||
row = 0;
|
||||
optsLay->addWidget(lblSrc, row, 0);
|
||||
optsLay->addWidget(wdgSrc, row++, 1);
|
||||
optsLay->addWidget(lblDst, row, 0);
|
||||
optsLay->addWidget(wdgDst, row++, 1);
|
||||
optsLay->addWidget(new QLabel(tr("Sync. Direction:"), this), row, 0);
|
||||
optsLay->addWidget(syncDir, row++, 1);
|
||||
optsLay->addWidget(new QLabel(tr("Existing Files:"), this), row, 0);
|
||||
optsLay->addWidget(copyMode, row++, 1);
|
||||
optsLay->addWidget(new QLabel(tr("Max. File Size:"), this), row, 0);
|
||||
optsLay->addLayout(hlay1, row++, 1);
|
||||
optsLay->addWidget(ui_extraOptions, row, 0, 1, 2);
|
||||
|
||||
// layout to hold warning label and button box
|
||||
QHBoxLayout * hlay2 = new QHBoxLayout();
|
||||
hlay2->addWidget(ui_statusLabel, 1);
|
||||
hlay2->addWidget(ui_buttonBox);
|
||||
|
||||
// Create main layout and add options, progress, buttons.
|
||||
QVBoxLayout * dlgL = new QVBoxLayout(this);
|
||||
dlgL->addWidget(ui_optionsPanel, 0, Qt::AlignTop);
|
||||
dlgL->addLayout(hlay2);
|
||||
dlgL->addWidget(ui_progress, 1);
|
||||
|
||||
// Connect all signal handlers
|
||||
|
||||
// source path
|
||||
connect(leSrc, &AutoLineEdit::textChanged, this, &FileSyncDialog::validate);
|
||||
// destination path
|
||||
connect(leDst, &AutoLineEdit::textChanged, this, &FileSyncDialog::validate);
|
||||
|
||||
// function to dis/enable the OVERWR_ALWAYS option depending on sync direction
|
||||
connect(syncDir, &AutoComboBox::currentDataChanged, [=](int dir) {
|
||||
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);
|
||||
});
|
||||
|
||||
// function to set magnitude of file size spinbox, KB or MB
|
||||
connect(maxSize, static_cast<void(QSpinBox::*)(int)>(&QSpinBox::valueChanged), [=](int value) {
|
||||
const QSignalBlocker blocker(maxSize);
|
||||
int multi = maxSize->property("multi").isValid() ? maxSize->property("multi").toInt() : 0;
|
||||
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);
|
||||
m_syncOptions.maxFileSize = value * multi;
|
||||
});
|
||||
|
||||
// we set these after connections are made because we want the signal processors to run
|
||||
syncDir->setField(m_syncOptions.direction);
|
||||
maxSize->setValue(m_syncOptions.maxFileSize / 1024);
|
||||
|
||||
// connect Reset action to all fields
|
||||
for (AutoLineEdit * a : ui_optionsPanel->findChildren<AutoLineEdit *>())
|
||||
connect(this, &FileSyncDialog::optionsWereReset, a, &AutoLineEdit::updateValue);
|
||||
for (AutoComboBox * a : ui_optionsPanel->findChildren<AutoComboBox *>())
|
||||
connect(this, &FileSyncDialog::optionsWereReset, a, &AutoComboBox::updateValue);
|
||||
for (AutoBitsetCheckBox * a : ui_optionsPanel->findChildren<AutoBitsetCheckBox *>())
|
||||
connect(this, &FileSyncDialog::optionsWereReset, a, &AutoBitsetCheckBox::updateValue);
|
||||
connect(this, &FileSyncDialog::optionsWereReset, [=]() { maxSize->setValue(m_syncOptions.maxFileSize / 1024); });
|
||||
|
||||
// React to name changes for source and destination path labels
|
||||
connect(this, &FileSyncDialog::folderNameAChanged, [=](const QString &text) {
|
||||
lblSrc->setText(text % ":");
|
||||
wdgSrc->setProperty("fileDialogTitle", text);
|
||||
});
|
||||
connect(this, &FileSyncDialog::folderNameBChanged, [=](const QString &text) {
|
||||
lblDst->setText(text % ":");
|
||||
wdgDst->setProperty("fileDialogTitle", text);
|
||||
});
|
||||
|
||||
// button actions
|
||||
connect(ui_btnStartStop, &QPushButton::clicked, this, &FileSyncDialog::toggleSync);
|
||||
connect(ui_buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
|
||||
connect(ui_buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
|
||||
connect(ui_progress, &ProgressWidget::detailsToggled, this, &FileSyncDialog::adjustSizeDelayed);
|
||||
}
|
||||
|
||||
void FileSyncDialog::toggleSync(bool)
|
||||
{
|
||||
if (m_running) {
|
||||
setCursor(Qt::WaitCursor);
|
||||
emit stopSync();
|
||||
return;
|
||||
}
|
||||
|
||||
ui_progress->clearDetails();
|
||||
ui_progress->setInfo("");
|
||||
SyncProcess * syncProcess = new SyncProcess(m_syncOptions);
|
||||
|
||||
// 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, &FileSyncDialog::startSync, syncProcess, &SyncProcess::run);
|
||||
connect(this, &FileSyncDialog::stopSync, syncProcess, &SyncProcess::stop);
|
||||
connect(syncProcess, &SyncProcess::started, this, &FileSyncDialog::onSyncStarted);
|
||||
connect(syncProcess, &SyncProcess::finished, this, &FileSyncDialog::onSyncFinished);
|
||||
connect(syncProcess, &SyncProcess::statusMessage, this, &FileSyncDialog::setStatusText);
|
||||
connect(syncProcess, &SyncProcess::statusUpdate, this, &FileSyncDialog::onStatusUpdate);
|
||||
connect(syncProcess, &SyncProcess::fileCountChanged, ui_progress, &ProgressWidget::setMaximum);
|
||||
connect(syncProcess, &SyncProcess::progressMessage, ui_progress, &ProgressWidget::addMessage);
|
||||
connect(syncProcess, &SyncProcess::finished, syncThread, &QThread::quit);
|
||||
connect(syncThread, &QThread::finished, syncThread, &QThread::deleteLater);
|
||||
connect(syncThread, &QThread::destroyed, syncProcess, &SyncProcess::deleteLater);
|
||||
|
||||
// go
|
||||
syncThread->start(QThread::LowPriority);
|
||||
emit startSync();
|
||||
}
|
||||
|
||||
void FileSyncDialog::onSyncStarted()
|
||||
{
|
||||
m_running = true;
|
||||
updateRunningState();
|
||||
}
|
||||
|
||||
void FileSyncDialog::onSyncFinished()
|
||||
{
|
||||
m_running = false;
|
||||
updateRunningState();
|
||||
if (parentWidget())
|
||||
QApplication::alert(parentWidget());
|
||||
}
|
||||
|
||||
void FileSyncDialog::updateRunningState()
|
||||
{
|
||||
ui_btnStartStop->setChecked(m_running);
|
||||
ui_optionsPanel->setEnabled(!m_running);
|
||||
ui_btnClose->setEnabled(!m_running);
|
||||
ui_btnStartStop->setText(m_running ? tr("Abort") : tr("Start"));
|
||||
if (m_running) {
|
||||
ui_progress->setVisible(true); // don't hide after run
|
||||
}
|
||||
else {
|
||||
setCursor(Qt::ArrowCursor); // in case we set a wait cursor upon abort request
|
||||
}
|
||||
}
|
||||
|
||||
void FileSyncDialog::onStatusUpdate(const SyncProcess::SyncStatus & stat)
|
||||
{
|
||||
static const QString reportTemplate = tr("Total: <b>%1</b>; Created: <b>%2</b>; Updated: <b>%3</b>; Skipped: <b>%4</b>; Errors: <font color=%6><b>%5</b></font>;");
|
||||
ui_progress->setInfo((stat.index < stat.count ? tr("Current: <b>%1</b> of ").arg(stat.index) : "") % reportTemplate.arg(stat.count).arg(stat.created).arg(stat.updated).arg(stat.skipped).arg(stat.errored).arg(stat.errored ? "red" : "black"));
|
||||
ui_progress->setValue(stat.index);
|
||||
}
|
||||
|
||||
void FileSyncDialog::setSyncOptions(const SyncProcess::SyncOptions & syncOptions)
|
||||
{
|
||||
m_syncOptions = syncOptions;
|
||||
emit optionsWereReset();
|
||||
}
|
||||
|
||||
void FileSyncDialog::resetOptions()
|
||||
{
|
||||
m_syncOptions.reset();
|
||||
emit optionsWereReset();
|
||||
}
|
||||
|
||||
void FileSyncDialog::toggleExtraOptions(bool show)
|
||||
{
|
||||
ui_extraOptions->setVisible(show);
|
||||
if (!ui_progress->detailsVisible())
|
||||
adjustSizeDelayed();
|
||||
m_extraOptionsVisible = show;
|
||||
emit extraOptionsToggled(show);
|
||||
}
|
||||
|
||||
void FileSyncDialog::setFolderNameA(const QString & name)
|
||||
{
|
||||
if (m_folderNameA != name) {
|
||||
m_folderNameA = name;
|
||||
emit folderNameAChanged(name);
|
||||
}
|
||||
}
|
||||
|
||||
void FileSyncDialog::setFolderNameB(const QString & name)
|
||||
{
|
||||
if (m_folderNameB != name) {
|
||||
m_folderNameB = name;
|
||||
emit folderNameBChanged(name);
|
||||
}
|
||||
}
|
||||
|
||||
void FileSyncDialog::setStatusText(const QString & text, int type)
|
||||
{
|
||||
ui_statusLabel->setText(text);
|
||||
ui_statusLabel->setStyleSheet(type != QtInfoMsg && type >= QtWarningMsg ? "QLabel { color: red; }:" : "");
|
||||
}
|
||||
|
||||
bool FileSyncDialog::validate()
|
||||
{
|
||||
QString msg;
|
||||
if (m_syncOptions.folderA.isEmpty() || !QFileInfo(m_syncOptions.folderA).exists())
|
||||
msg = tr("Source folder not found.");
|
||||
else if (m_syncOptions.folderB.isEmpty() || !QFileInfo(m_syncOptions.folderB).exists())
|
||||
msg = tr("Destination folder not found.");
|
||||
else if (QFileInfo(m_syncOptions.folderA).canonicalFilePath() == QFileInfo(m_syncOptions.folderB).canonicalFilePath())
|
||||
msg = tr("Source and destination folders are the same.");
|
||||
|
||||
setStatusText(msg, QtCriticalMsg);
|
||||
ui_btnStartStop->setEnabled(msg.isEmpty());
|
||||
return msg.isEmpty();
|
||||
}
|
||||
|
||||
void FileSyncDialog::adjustSizeDelayed()
|
||||
{
|
||||
QTimer::singleShot(0, [=]() { adjustSize(); });
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
* Copyright (C) OpenTX
|
||||
*
|
||||
* Based on code named
|
||||
* th9x - http://code.google.com/p/th9x
|
||||
* er9x - http://code.google.com/p/er9x
|
||||
* gruvin9x - http://code.google.com/p/gruvin9x
|
||||
*
|
||||
* License GPLv2: http://www.gnu.org/licenses/gpl-2.0.html
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License version 2 as
|
||||
* published by the Free Software Foundation.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*/
|
||||
|
||||
#ifndef FILESYNCDIALOG_H
|
||||
#define FILESYNCDIALOG_H
|
||||
|
||||
#include "process_sync.h"
|
||||
|
||||
#include <QDialog>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QGroupBox>
|
||||
#include <QLabel>
|
||||
#include <QToolButton>
|
||||
|
||||
class ProgressWidget;
|
||||
|
||||
class FileSyncDialog : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(SyncProcess::SyncOptions syncOptions READ syncOptions WRITE setSyncOptions RESET resetOptions NOTIFY optionsWereReset)
|
||||
Q_PROPERTY(bool extraOptionsVisible READ extraOptionsVisible WRITE toggleExtraOptions NOTIFY extraOptionsToggled)
|
||||
Q_PROPERTY(QString folderNameA READ folderNameA WRITE setFolderNameA NOTIFY folderNameAChanged)
|
||||
Q_PROPERTY(QString folderNameB READ folderNameB WRITE setFolderNameB NOTIFY folderNameBChanged)
|
||||
|
||||
public:
|
||||
FileSyncDialog(QWidget * parent = nullptr, const SyncProcess::SyncOptions & syncOptions = SyncProcess::SyncOptions());
|
||||
|
||||
inline SyncProcess::SyncOptions syncOptions() const { return m_syncOptions; }
|
||||
void setSyncOptions(const SyncProcess::SyncOptions & syncOptions);
|
||||
void resetOptions();
|
||||
|
||||
inline bool extraOptionsVisible() const { return m_extraOptionsVisible; }
|
||||
void toggleExtraOptions(bool show);
|
||||
|
||||
inline QString folderNameA() const { return m_folderNameA; }
|
||||
void setFolderNameA(const QString & name);
|
||||
|
||||
inline QString folderNameB() const { return m_folderNameB; }
|
||||
void setFolderNameB(const QString & name);
|
||||
|
||||
public slots:
|
||||
virtual void reject() override;
|
||||
void setStatusText(const QString & text, int type = QtWarningMsg);
|
||||
bool validate();
|
||||
|
||||
signals:
|
||||
void startSync();
|
||||
void stopSync();
|
||||
void folderNameAChanged(const QString & name);
|
||||
void folderNameBChanged(const QString & name);
|
||||
void extraOptionsToggled(bool show);
|
||||
void optionsWereReset();
|
||||
|
||||
private slots:
|
||||
void setupUi();
|
||||
void toggleSync(bool);
|
||||
void onSyncStarted();
|
||||
void onSyncFinished();
|
||||
void updateRunningState();
|
||||
void onStatusUpdate(const SyncProcess::SyncStatus & stat);
|
||||
void adjustSizeDelayed();
|
||||
|
||||
private:
|
||||
SyncProcess::SyncOptions m_syncOptions;
|
||||
QString m_folderNameA;
|
||||
QString m_folderNameB;
|
||||
bool m_running;
|
||||
bool m_extraOptionsVisible;
|
||||
|
||||
QGroupBox * ui_optionsPanel;
|
||||
QWidget * ui_extraOptions;
|
||||
ProgressWidget * ui_progress;
|
||||
QDialogButtonBox * ui_buttonBox;
|
||||
QToolButton * ui_btnStartStop;
|
||||
QToolButton * ui_btnClose;
|
||||
QLabel * ui_statusLabel;
|
||||
};
|
||||
|
||||
#endif // FILESYNCDIALOG_H
|
||||
+28
-239
@@ -48,6 +48,8 @@
|
||||
#include "storage.h"
|
||||
#include "translations.h"
|
||||
|
||||
#include "dialogs/filesyncdialog.h"
|
||||
|
||||
#include <QtGui>
|
||||
#include <QNetworkProxyFactory>
|
||||
#include <QFileInfo>
|
||||
@@ -56,12 +58,12 @@
|
||||
#define OPENTX_DOWNLOADS_PAGE_URL "http://www.open-tx.org/downloads"
|
||||
#define DONATE_STR "https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=QUZ48K4SEXDP2"
|
||||
|
||||
#ifdef __APPLE__
|
||||
#ifdef Q_OS_MACOS
|
||||
#define COMPANION_STAMP "companion-macosx.stamp"
|
||||
#define COMPANION_INSTALLER "macosx/opentx-companion-%1.dmg"
|
||||
#define COMPANION_FILEMASK QT_TRANSLATE_NOOP("MainWindow", "Diskimage (*.dmg)")
|
||||
#define COMPANION_INSTALL_QUESTION QT_TRANSLATE_NOOP("MainWindow", "Would you like to open the disk image to install the new version?")
|
||||
#elif WIN32
|
||||
#elif defined(Q_OS_WIN)
|
||||
#define COMPANION_STAMP "companion-windows.stamp"
|
||||
#define COMPANION_INSTALLER "windows/companion-windows-%1.exe"
|
||||
#define COMPANION_FILEMASK QT_TRANSLATE_NOOP("MainWindow", "Executable (*.exe)")
|
||||
@@ -797,254 +799,41 @@ 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()
|
||||
{
|
||||
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 % " ";
|
||||
// remember user-selectable options for duration of session TODO: save to settings
|
||||
static SyncProcess::SyncOptions syncOpts;
|
||||
static bool showExtraOptions = false;
|
||||
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 (syncOpts.folderA.isEmpty())
|
||||
syncOpts.folderA = g.profile[g.id()].sdPath();
|
||||
if (syncOpts.folderB.isEmpty())
|
||||
syncOpts.folderB = findMassstoragePath("SOUNDS", true);
|
||||
|
||||
if (sourcePath.isEmpty())
|
||||
sourcePath = g.profile[g.id()].sdPath();
|
||||
if (destPath.isEmpty())
|
||||
destPath = findMassstoragePath("SOUNDS").replace(QRegExp("[/\\\\]?SOUNDS"), "");
|
||||
|
||||
if (sourcePath.isEmpty())
|
||||
if (syncOpts.folderA.isEmpty())
|
||||
errorMsgs << tr("No local SD structure path configured!");
|
||||
if (destPath.isEmpty())
|
||||
if (syncOpts.folderB.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);
|
||||
QPointer<FileSyncDialog> dlg = new FileSyncDialog(this, syncOpts);
|
||||
dlg->setAttribute(Qt::WA_DeleteOnClose, true);
|
||||
dlg->setWindowFlags(dlg->windowFlags() | Qt::WindowStaysOnTopHint);
|
||||
dlg->setWindowTitle(tr("Synchronize SD"));
|
||||
dlg->setWindowIcon(CompanionIcon("sdsync.png"));
|
||||
dlg->setFolderNameA(tr("Local Folder"));
|
||||
dlg->setFolderNameB(tr("Radio Folder"));
|
||||
dlg->toggleExtraOptions(showExtraOptions);
|
||||
if (errorMsgs.size())
|
||||
dlg->setStatusText(errorMsgs.join('\n'), QtWarningMsg);
|
||||
dlg->show();
|
||||
|
||||
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"));
|
||||
connect(dlg.data(), &FileSyncDialog::finished, [=](int) {
|
||||
if (!dlg.isNull()) {
|
||||
syncOpts = dlg->syncOptions();
|
||||
showExtraOptions = dlg->extraOptionsVisible();
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
// 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()
|
||||
|
||||
+277
-124
@@ -22,128 +22,219 @@
|
||||
|
||||
#include <QApplication>
|
||||
#include <QCryptographicHash>
|
||||
#include <QDateTime>
|
||||
#include <QElapsedTimer>
|
||||
#include <QMutexLocker>
|
||||
#include <QDirIterator>
|
||||
#include <QDebug>
|
||||
|
||||
#define SYNC_MAX_ERRORS 50 // give up after this many errors per destination
|
||||
|
||||
// a flood of log messages can make the UI unresponsive so we'll introduce a dynamic sleep period based on log frequency (values in [us])
|
||||
#define PAUSE_FACTOR 60UL
|
||||
#define PAUSE_RECOVERY (PAUSE_FACTOR / 3 * 2)
|
||||
#define PAUSE_MINTM 100UL
|
||||
#define PAUSE_MAXTM 75000UL
|
||||
|
||||
#if (QT_VERSION < QT_VERSION_CHECK(5, 5, 0))
|
||||
#define QtInfoMsg QtMsgType(4)
|
||||
#define QtInfoMsg QtMsgType(4)
|
||||
#endif
|
||||
|
||||
#define PRINT_INFO(str) emit progressMessage((str), QtInfoMsg)
|
||||
#define PRINT_CREATE(str) emit progressMessage((str), QtInfoMsg)
|
||||
#define PRINT_REPLACE(str) emit progressMessage((str), QtWarningMsg)
|
||||
#define PRINT_DELETE(str) emit progressMessage((str), QtCriticalMsg)
|
||||
#define PRINT_ERROR(str) emit progressMessage((str), QtFatalMsg)
|
||||
//#define PRINT_SKIP(str) emit progressMessage((str), QtDebugMsg) // mostly useless noise (maybe make an option later)
|
||||
#define PRINT_SKIP(str)
|
||||
#define PRINT_SEP() PRINT_INFO(QString(80, '='))
|
||||
#define PRINT_CREATE(str) emitProgressMessage((str), QtInfoMsg)
|
||||
#define PRINT_REPLACE(str) emitProgressMessage((str), QtWarningMsg)
|
||||
//#define PRINT_DELETE(str) emitProgressMessage((str), QtCriticalMsg) // unused
|
||||
#define PRINT_ERROR(str) emitProgressMessage((str), QtFatalMsg)
|
||||
#define PRINT_SKIP(str) emitProgressMessage((str), QtDebugMsg)
|
||||
#define PRINT_INFO(str) emit progressMessage((str)) // this is always emitted regardless of logLevel option
|
||||
#define PRINT_SEP() PRINT_INFO(QString(70, '='))
|
||||
|
||||
#define SYNC_MAX_ERRORS 50 // give up after this many errors per destination
|
||||
#ifdef Q_OS_WIN
|
||||
extern Q_CORE_EXPORT int qt_ntfs_permission_lookup;
|
||||
#define FILTER_RE_SYNTX QRegExp::Wildcard
|
||||
#else
|
||||
#define FILTER_RE_SYNTX QRegExp::WildcardUnix
|
||||
#endif
|
||||
|
||||
SyncProcess::SyncProcess(const QString & folderA, const QString & folderB, const int & syncDirection, const int & compareType, const qint64 & maxFileSize, const bool dryRun):
|
||||
folder1(folderA),
|
||||
folder2(folderB),
|
||||
direction((SyncDirection)syncDirection),
|
||||
ctype((SyncCompareType)compareType),
|
||||
maxFileSize(qMax<qint64>(0, maxFileSize)),
|
||||
dryRun(dryRun),
|
||||
SyncProcess::SyncProcess(const SyncProcess::SyncOptions & options) :
|
||||
m_options(options),
|
||||
m_pauseTime(PAUSE_MINTM),
|
||||
stopping(false)
|
||||
{
|
||||
if (direction == SYNC_B2A_A2B) {
|
||||
folder1 = folderB;
|
||||
folder2 = folderA;
|
||||
direction = SYNC_A2B_B2A;
|
||||
qRegisterMetaType<SyncProcess::SyncStatus>();
|
||||
|
||||
if (m_options.compareType == OVERWR_ALWAYS && (m_options.direction == SYNC_A2B_B2A || m_options.direction == SYNC_B2A_A2B))
|
||||
m_options.compareType = OVERWR_IF_DIFF;
|
||||
|
||||
m_dirFilters = QDir::Filters(m_options.dirFilterFlags);
|
||||
if (!(m_dirFilters & QDir::Dirs))
|
||||
m_dirFilters &= ~(QDir::AllDirs);
|
||||
|
||||
if (!m_options.includeFilter.isEmpty() && m_options.includeFilter != "*")
|
||||
m_dirIteratorFilters = m_options.includeFilter.split(',', QString::SkipEmptyParts);
|
||||
|
||||
if (!m_options.excludeFilter.isEmpty()) {
|
||||
for (const QString & f : m_options.excludeFilter.split(',', QString::SkipEmptyParts))
|
||||
m_excludeFilters.append(QRegExp(f, ((m_dirFilters & QDir::CaseSensitive) ? Qt::CaseSensitive : Qt::CaseInsensitive), FILTER_RE_SYNTX));
|
||||
}
|
||||
|
||||
if (ctype == OVERWR_ALWAYS && direction == SYNC_A2B_B2A)
|
||||
ctype = OVERWR_IF_DIFF;
|
||||
|
||||
dirFilters = QDir::Filters(QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot);
|
||||
|
||||
reportTemplate = tr("New: <b>%1</b>; Updated: <b>%2</b>; Skipped: <b>%3</b>; Errors: <font color=%5><b>%4</b></font>;");
|
||||
if (dryRun)
|
||||
if (m_options.flags & OPT_DRY_RUN)
|
||||
testRunStr = tr("[TEST RUN] ");
|
||||
|
||||
//qDebug() << m_options;
|
||||
#ifdef Q_OS_WIN
|
||||
qt_ntfs_permission_lookup++; // global enable NTFS permissions checking
|
||||
#endif
|
||||
}
|
||||
|
||||
SyncProcess::~SyncProcess()
|
||||
{
|
||||
#ifdef Q_OS_WIN
|
||||
qt_ntfs_permission_lookup--; // global revert NTFS permissions checking
|
||||
#endif
|
||||
}
|
||||
|
||||
void SyncProcess::stop()
|
||||
{
|
||||
QMutexLocker locker(&stopReqMutex);
|
||||
QWriteLocker locker(&stopReqMutex);
|
||||
stopping = true;
|
||||
}
|
||||
|
||||
bool SyncProcess::isStopRequsted()
|
||||
{
|
||||
QMutexLocker locker(&stopReqMutex);
|
||||
QReadLocker locker(&stopReqMutex);
|
||||
return stopping;
|
||||
}
|
||||
|
||||
void SyncProcess::run()
|
||||
{
|
||||
count = index = created = updated = skipped = errored = 0;
|
||||
const QString folderA = (m_options.direction == SYNC_B2A_A2B ? m_options.folderB : m_options.folderA);
|
||||
const QString folderB = (m_options.direction == SYNC_B2A_A2B ? m_options.folderA : m_options.folderB);
|
||||
const SyncDirection direction = (m_options.direction == SYNC_B2A_A2B ? SYNC_A2B_B2A : SyncDirection(m_options.direction));
|
||||
const QString gathering = tr("Gathering file information for %1...");
|
||||
const QString noFiles = tr("No files found in %1");
|
||||
int count = 0;
|
||||
|
||||
m_stat.clear();
|
||||
m_startTime = QDateTime::currentDateTime();
|
||||
|
||||
emit started();
|
||||
emit progressStep(index);
|
||||
emit statusMessage(tr("Gathering file information..."));
|
||||
emit fileCountChanged(0);
|
||||
emit statusUpdate(m_stat);
|
||||
|
||||
if (direction == SYNC_A2B_B2A || direction == SYNC_A2B)
|
||||
count += getFilesCount(folder1);
|
||||
if (direction == SYNC_A2B_B2A || direction == SYNC_A2B) {
|
||||
emit statusMessage(gathering.arg(folderA));
|
||||
count = getFilesCount(folderA);
|
||||
|
||||
if (isStopRequsted()) {
|
||||
finish();
|
||||
return;
|
||||
if (isStopRequsted())
|
||||
goto endrun;
|
||||
|
||||
if (count) {
|
||||
m_stat.count = count;
|
||||
if (m_options.direction == SYNC_A2B_B2A)
|
||||
count *= 2; // assume this direction is only 50% of total, exact will be calculated later
|
||||
emit fileCountChanged(count);
|
||||
updateDir(folderA, folderB);
|
||||
if (isStopRequsted())
|
||||
goto endrun;
|
||||
}
|
||||
else {
|
||||
PRINT_INFO(noFiles.arg(folderA));
|
||||
PRINT_SEP();
|
||||
}
|
||||
}
|
||||
|
||||
if (direction == SYNC_A2B_B2A || direction == SYNC_B2A)
|
||||
count += getFilesCount(folder2);
|
||||
if (direction == SYNC_A2B_B2A || direction == SYNC_B2A) {
|
||||
emit statusMessage(gathering.arg(folderB));
|
||||
count = getFilesCount(folderB);
|
||||
|
||||
if (isStopRequsted()) {
|
||||
finish();
|
||||
return;
|
||||
if (isStopRequsted())
|
||||
goto endrun;
|
||||
|
||||
m_stat.count += count;
|
||||
emit fileCountChanged(m_stat.count);
|
||||
|
||||
if (count) {
|
||||
updateDir(folderB, folderA);
|
||||
}
|
||||
else {
|
||||
PRINT_INFO(noFiles.arg(folderB));
|
||||
PRINT_SEP();
|
||||
}
|
||||
}
|
||||
|
||||
emit fileCountChanged(count);
|
||||
|
||||
if (!count) {
|
||||
QString nf = tr("Synchronization failed, nothing found to copy.");
|
||||
emit statusMessage(nf);
|
||||
PRINT_ERROR(nf);
|
||||
if (!m_stat.count) {
|
||||
emit statusMessage(tr("Synchronization failed, nothing found to copy."), QtWarningMsg);
|
||||
emit finished();
|
||||
return;
|
||||
}
|
||||
|
||||
if (direction == SYNC_A2B_B2A || direction == SYNC_A2B)
|
||||
updateDir(folder1, folder2);
|
||||
|
||||
if (isStopRequsted()) {
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
if (direction == SYNC_A2B_B2A || direction == SYNC_B2A)
|
||||
updateDir(folder2, folder1);
|
||||
|
||||
endrun:
|
||||
finish();
|
||||
}
|
||||
|
||||
void SyncProcess::finish()
|
||||
{
|
||||
QString endStr = testRunStr % tr("Synchronization finished. ") % reportTemplate;
|
||||
emit statusMessage(endStr.arg(created).arg(updated).arg(skipped).arg(errored).arg(errored ? "red" : "black"));
|
||||
const std::lldiv_t elapsed = std::lldiv(m_startTime.secsTo(QDateTime::currentDateTime()), 60);
|
||||
QString endStr = testRunStr;
|
||||
if (m_stat.index < m_stat.count)
|
||||
endStr.append(tr("Synchronization aborted at %1 of %2 files.").arg(m_stat.index).arg(m_stat.count));
|
||||
else
|
||||
endStr.append(tr("Synchronization finished with %1 files in %2m %3s.").arg(m_stat.count).arg(elapsed.quot).arg(elapsed.rem));
|
||||
emit statusMessage(endStr);
|
||||
emit finished();
|
||||
}
|
||||
|
||||
SyncProcess::FileFilterResult SyncProcess::fileFilter(const QFileInfo & fileInfo)
|
||||
{
|
||||
// Windows Junctions (mount points) are not detected as links (QTBUG-45344), but that's OK since they're really "hard links."
|
||||
const bool chkDirLnk = ((m_dirFilters & QDir::NoSymLinks) && !(m_dirFilters & QDir::AllDirs)) || ((m_options.flags & OPT_SKIP_DIR_LINKS) && fileInfo.isDir());
|
||||
if ((chkDirLnk || ((m_dirFilters & QDir::NoSymLinks) && fileInfo.isFile())) && QFileInfo(fileInfo.absoluteFilePath()).isSymLink()) // MUST create a new QFileInfo here (QTBUG-69001)
|
||||
return FILE_LINK_IGNORE;
|
||||
|
||||
if (m_options.maxFileSize > 0 && fileInfo.isFile() && fileInfo.size() > m_options.maxFileSize)
|
||||
return FILE_OVERSIZE;
|
||||
|
||||
if (!m_excludeFilters.isEmpty() && (!(m_dirFilters & QDir::AllDirs) || fileInfo.isFile())) {
|
||||
for (QVector<QRegExp>::const_iterator it = m_excludeFilters.constBegin(), end = m_excludeFilters.constEnd(); it != end; ++it) {
|
||||
if (QRegExp(*it).exactMatch(fileInfo.fileName()))
|
||||
return FILE_EXCLUDE;
|
||||
}
|
||||
}
|
||||
|
||||
return FILE_ALLOW;
|
||||
}
|
||||
|
||||
QFileInfoList SyncProcess::dirInfoList(const QString & directory)
|
||||
{
|
||||
QDir::Filters flt = m_dirFilters;
|
||||
if (!(flt & QDir::Dirs) && (m_options.flags & OPT_RECURSIVE))
|
||||
flt |= ((m_options.dirFilterFlags & QDir::AllDirs) ? QDir::AllDirs : QDir::Dirs);
|
||||
return QDir(directory).entryInfoList(m_dirIteratorFilters, flt, QDir::Name | QDir::DirsLast);
|
||||
}
|
||||
|
||||
void SyncProcess::pushDirEntries(const QFileInfo & fileInfo, QMutableListIterator<QFileInfo> & it)
|
||||
{
|
||||
if ((m_options.flags & OPT_RECURSIVE) && fileInfo.isDir()) {
|
||||
for (const QFileInfo &fi : dirInfoList(fileInfo.absoluteFilePath()))
|
||||
it.insert(fi);
|
||||
}
|
||||
}
|
||||
|
||||
int SyncProcess::getFilesCount(const QString & directory)
|
||||
{
|
||||
if (!QFile::exists(directory))
|
||||
return 0;
|
||||
|
||||
int result = 0;
|
||||
QDirIterator it(directory, dirFilters, QDirIterator::Subdirectories | QDirIterator::FollowSymlinks);
|
||||
while (it.hasNext() && !isStopRequsted()) {
|
||||
it.next();
|
||||
result++;
|
||||
QFileInfoList infoList = dirInfoList(directory);
|
||||
QMutableListIterator<QFileInfo> it(infoList);
|
||||
it.toBack();
|
||||
while (it.hasPrevious() && !isStopRequsted()) {
|
||||
const QFileInfo fi(it.previous());
|
||||
it.remove();
|
||||
if (fileFilter(fi) == FILE_ALLOW) {
|
||||
pushDirEntries(fi, it);
|
||||
if (fi.isFile()) {
|
||||
result++;
|
||||
}
|
||||
}
|
||||
QApplication::processEvents();
|
||||
}
|
||||
return result;
|
||||
@@ -151,72 +242,117 @@ int SyncProcess::getFilesCount(const QString & directory)
|
||||
|
||||
void SyncProcess::updateDir(const QString & source, const QString & destination)
|
||||
{
|
||||
int counts[4] = { created, updated, skipped, errored };
|
||||
QString statusStr = testRunStr % tr("Synchronizing %1 -> %2: %3").arg(source, destination, "<b>%1</b>|<b>%2</b> (%3)");
|
||||
SyncStatus pStat = m_stat;
|
||||
const QDir srcDir(source), dstDir(destination);
|
||||
FileFilterResult ffr;
|
||||
emit statusMessage(testRunStr % tr("Synchronizing: %1\n To: %2").arg(source, destination));
|
||||
PRINT_INFO(testRunStr % tr("Starting synchronization:\n %1 -> %2\n").arg(source, destination));
|
||||
|
||||
PRINT_INFO(testRunStr % tr("Starting synchronization: %1 -> %2<br>").arg(source, destination));
|
||||
|
||||
QDirIterator it(source, dirFilters, QDirIterator::Subdirectories | QDirIterator::FollowSymlinks);
|
||||
while (it.hasNext() && !isStopRequsted()) {
|
||||
it.next();
|
||||
++index;
|
||||
emit statusMessage(statusStr.arg(index).arg(count).arg(reportTemplate.arg(created).arg(updated).arg(skipped).arg(errored).arg(errored ? "red" : "black")));
|
||||
emit progressStep(index);
|
||||
if (maxFileSize && it.fileInfo().isFile() && it.fileInfo().size() > maxFileSize) {
|
||||
PRINT_SKIP(tr("Skipping large file: %1 (%2KB)").arg(it.fileName()).arg(int(it.fileInfo().size() / 1024)));
|
||||
++skipped;
|
||||
}
|
||||
else {
|
||||
updateEntry(it.filePath(), source, destination);
|
||||
if (errored - counts[3] > SYNC_MAX_ERRORS) {
|
||||
PRINT_ERROR(tr("<br><b>Too many errors, giving up.<b>"));
|
||||
break;
|
||||
QFileInfoList infoList = dirInfoList(source);
|
||||
QMutableListIterator<QFileInfo> it(infoList);
|
||||
it.toBack();
|
||||
while (it.hasPrevious() && !isStopRequsted()) {
|
||||
const QFileInfo fi(it.previous());
|
||||
it.remove();
|
||||
if ((ffr = fileFilter(fi)) == FILE_ALLOW) {
|
||||
pushDirEntries(fi, it);
|
||||
if ((m_dirFilters & QDir::Dirs) || fi.isFile()) {
|
||||
updateEntry(fi.filePath(), srcDir, dstDir);
|
||||
if (fi.isFile())
|
||||
++m_stat.index;
|
||||
emit statusUpdate(m_stat);
|
||||
if (m_stat.errored - pStat.errored > SYNC_MAX_ERRORS) {
|
||||
PRINT_ERROR(tr("\nToo many errors, giving up."));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
QApplication::processEvents();
|
||||
else if (m_options.logLevel == QtDebugMsg) {
|
||||
switch (ffr) {
|
||||
case FILE_OVERSIZE:
|
||||
PRINT_SKIP(tr("Skipping large file: %1 (%2KB)").arg(fi.fileName()).arg(int(fi.size() / 1024)));
|
||||
break;
|
||||
case FILE_EXCLUDE:
|
||||
PRINT_SKIP(tr("Skipping filtered file: %1").arg(fi.fileName()));
|
||||
break;
|
||||
case FILE_LINK_IGNORE:
|
||||
PRINT_SKIP(tr("Skipping linked file: %1").arg(fi.fileName()));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
// don't count as skipped because these weren't included in the total file count to begin with
|
||||
}
|
||||
// throttle if needed
|
||||
m_pauseTime = qMax(m_pauseTime - PAUSE_RECOVERY, PAUSE_MINTM);
|
||||
pause();
|
||||
}
|
||||
|
||||
QString endStr = "<br>" % testRunStr % tr("Finished synchronizing %1 -> %2 :<br> %3").arg(source, destination, reportTemplate);
|
||||
PRINT_INFO(endStr.arg(created-counts[0]).arg(updated-counts[1]).arg(skipped-counts[2]).arg(errored-counts[3]).arg(errored-counts[3] ? "red" : "black"));
|
||||
QString endStr = "\n" % testRunStr;
|
||||
if (isStopRequsted())
|
||||
endStr.append(tr("Aborted synchronization of:"));
|
||||
else
|
||||
endStr.append(tr("Finished synchronizing:"));
|
||||
endStr.append(QString("\n %1 -> %2\n ").arg(source, destination));
|
||||
endStr.append(tr("Created: %1; Updated: %2; Skipped: %3; Errors: %4;").arg(m_stat.created-pStat.created).arg(m_stat.updated-pStat.updated).arg(m_stat.skipped-pStat.skipped).arg(m_stat.errored-pStat.errored));
|
||||
PRINT_INFO(endStr);
|
||||
PRINT_SEP();
|
||||
}
|
||||
|
||||
bool SyncProcess::updateEntry(const QString & entry, const QDir & source, const QDir & destination)
|
||||
{
|
||||
QString srcPath = source.toNativeSeparators(source.absoluteFilePath(entry));
|
||||
QString relPath = source.relativeFilePath(entry);
|
||||
QString destPath = destination.toNativeSeparators(destination.absoluteFilePath(relPath));
|
||||
QFileInfo sourceInfo(srcPath);
|
||||
QFileInfo destInfo(destPath);
|
||||
const QString srcPath = QDir::toNativeSeparators(source.absoluteFilePath(entry));
|
||||
const QString destPath = QDir::toNativeSeparators(destination.absoluteFilePath(source.relativeFilePath(entry)));
|
||||
const QFileInfo sourceInfo(srcPath);
|
||||
const QFileInfo destInfo(destPath);
|
||||
static QString lastMkPath;
|
||||
|
||||
if (sourceInfo.isDir()) {
|
||||
if (!destInfo.exists()) {
|
||||
PRINT_CREATE(tr("Creating directory: %1").arg(destPath));
|
||||
if (!dryRun && !destination.mkpath(destPath)) {
|
||||
PRINT_ERROR(tr("Could not create directory: %1").arg(destPath));
|
||||
++errored;
|
||||
return false;
|
||||
// check if this is a directory OR if we're copying a file with a path which doesn't exist yet.
|
||||
if (sourceInfo.isDir() || !destInfo.absoluteDir().exists()) {
|
||||
const QString mkPath = sourceInfo.isDir() ? destPath : QDir::toNativeSeparators(destInfo.absolutePath());
|
||||
if (!destination.exists(mkPath)) {
|
||||
if (mkPath == lastMkPath) {
|
||||
// we've already tried, and apparently failed, to create this folder... bail out but log as error.
|
||||
if (!(m_options.flags & OPT_DRY_RUN)) {
|
||||
++m_stat.errored;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else {
|
||||
lastMkPath = mkPath;
|
||||
PRINT_CREATE(tr("Creating directory: %1").arg(mkPath));
|
||||
if (!(m_options.flags & OPT_DRY_RUN) && !destination.mkpath(mkPath)) {
|
||||
PRINT_ERROR(tr("Could not create directory: %1").arg(mkPath));
|
||||
++m_stat.errored;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
++created;
|
||||
}
|
||||
else {
|
||||
PRINT_SKIP(tr("Destination directory exists: %1").arg(destPath));
|
||||
++skipped;
|
||||
else if (m_dirFilters & QDir::Dirs) {
|
||||
PRINT_SKIP(tr("Directory exists: %1").arg(mkPath));
|
||||
}
|
||||
return true;
|
||||
if (sourceInfo.isDir())
|
||||
return true;
|
||||
}
|
||||
|
||||
//qDebug() << destPath;
|
||||
QFile sourceFile(srcPath);
|
||||
QFile destinationFile(destPath);
|
||||
bool destExists = destInfo.exists();
|
||||
bool checkDate = (ctype == OVERWR_NEWER_IF_DIFF || ctype == OVERWR_NEWER_ALWAYS);
|
||||
bool checkContent = (ctype == OVERWR_NEWER_IF_DIFF || ctype == OVERWR_IF_DIFF);
|
||||
const bool destExists = destInfo.exists();
|
||||
bool checkDate = (m_options.compareType == OVERWR_NEWER_IF_DIFF || m_options.compareType == OVERWR_NEWER_ALWAYS);
|
||||
bool checkContent = (m_options.compareType == OVERWR_NEWER_IF_DIFF || m_options.compareType == OVERWR_IF_DIFF);
|
||||
bool existed = false;
|
||||
|
||||
if (destExists && checkDate) {
|
||||
const QDate cmprDate = QDate::currentDate();
|
||||
if (sourceInfo.lastModified().date() > cmprDate || destInfo.lastModified().date() > cmprDate) {
|
||||
PRINT_ERROR(tr("At least one of the file modification dates is in the future, error on: %1").arg(srcPath));
|
||||
++m_stat.errored;
|
||||
return false;
|
||||
}
|
||||
if (sourceInfo.lastModified() <= destInfo.lastModified()) {
|
||||
PRINT_SKIP(tr("Skipping older file: %1").arg(srcPath));
|
||||
++skipped;
|
||||
++m_stat.skipped;
|
||||
return true;
|
||||
}
|
||||
checkDate = false;
|
||||
@@ -225,21 +361,21 @@ bool SyncProcess::updateEntry(const QString & entry, const QDir & source, const
|
||||
if (destExists && checkContent) {
|
||||
if (!sourceFile.open(QFile::ReadOnly)) {
|
||||
PRINT_ERROR(tr("Could not open source file '%1': %2").arg(srcPath, sourceFile.errorString()));
|
||||
++errored;
|
||||
++m_stat.errored;
|
||||
return false;
|
||||
}
|
||||
if (!destinationFile.open(QFile::ReadOnly)) {
|
||||
PRINT_ERROR(tr("Could not open destination file '%1': %2").arg(destPath, destinationFile.errorString()));
|
||||
++errored;
|
||||
++m_stat.errored;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool skip = QCryptographicHash::hash(sourceFile.readAll(), QCryptographicHash::Md5) == QCryptographicHash::hash(destinationFile.readAll(), QCryptographicHash::Md5);
|
||||
const bool skip = QCryptographicHash::hash(sourceFile.readAll(), QCryptographicHash::Md5) == QCryptographicHash::hash(destinationFile.readAll(), QCryptographicHash::Md5);
|
||||
sourceFile.close();
|
||||
destinationFile.close();
|
||||
if (skip) {
|
||||
PRINT_SKIP(tr("Skipping identical file: %1").arg(srcPath));
|
||||
++skipped;
|
||||
++m_stat.skipped;
|
||||
return true;
|
||||
}
|
||||
checkContent = false;
|
||||
@@ -248,27 +384,44 @@ bool SyncProcess::updateEntry(const QString & entry, const QDir & source, const
|
||||
if (!destExists || (!checkDate && !checkContent)) {
|
||||
if (destInfo.exists()) {
|
||||
existed = true;
|
||||
PRINT_REPLACE(tr("Replacing destination file: %1").arg(destPath));
|
||||
if (!dryRun && !destinationFile.remove()) {
|
||||
PRINT_REPLACE(tr("Replacing file: %1").arg(destPath));
|
||||
if (!(m_options.flags & OPT_DRY_RUN) && !destinationFile.remove()) {
|
||||
PRINT_ERROR(tr("Could not delete destination file '%1': %2").arg(destPath, destinationFile.errorString()));
|
||||
++errored;
|
||||
++m_stat.errored;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else {
|
||||
PRINT_CREATE(tr("Creating destination file: %1").arg(destPath));
|
||||
PRINT_CREATE(tr("Creating file: %1").arg(destPath));
|
||||
}
|
||||
if (!dryRun && !sourceFile.copy(destPath)) {
|
||||
if (!(m_options.flags & OPT_DRY_RUN) && !sourceFile.copy(destPath)) {
|
||||
PRINT_ERROR(tr("Copy failed: '%1' to '%2': %3").arg(srcPath, destPath, sourceFile.errorString()));
|
||||
++errored;
|
||||
++m_stat.errored;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (existed)
|
||||
++updated;
|
||||
++m_stat.updated;
|
||||
else
|
||||
++created;
|
||||
++m_stat.created;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void SyncProcess::pause()
|
||||
{
|
||||
QElapsedTimer tim;
|
||||
const qint64 exp = m_pauseTime * 1000;
|
||||
tim.start();
|
||||
while (tim.nsecsElapsed() < exp && !isStopRequsted())
|
||||
QApplication::processEvents();
|
||||
}
|
||||
|
||||
void SyncProcess::emitProgressMessage(const QString & text, int type)
|
||||
{
|
||||
if (m_options.logLevel == QtDebugMsg || (m_options.logLevel == QtInfoMsg && type > QtDebugMsg) || (type < QtInfoMsg && type >= m_options.logLevel)) {
|
||||
emit progressMessage(text, type);
|
||||
m_pauseTime = qMin(m_pauseTime + PAUSE_FACTOR, PAUSE_MAXTM);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,13 +18,17 @@
|
||||
* GNU General Public License for more details.
|
||||
*/
|
||||
|
||||
#ifndef _PROCESS_SYNC_H_
|
||||
#define _PROCESS_SYNC_H_
|
||||
#ifndef PROCESS_SYNC_H
|
||||
#define PROCESS_SYNC_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QDateTime>
|
||||
#include <QDebug>
|
||||
#include <QDir>
|
||||
#include <QMutex>
|
||||
#include <QString>
|
||||
#include <QDirIterator>
|
||||
#include <QReadWriteLock>
|
||||
#include <QRegExp>
|
||||
#include <QVector>
|
||||
|
||||
class SyncProcess : public QObject
|
||||
{
|
||||
@@ -32,25 +36,75 @@ class SyncProcess : public QObject
|
||||
|
||||
public:
|
||||
enum SyncDirection {
|
||||
SYNC_A2B_B2A,
|
||||
SYNC_A2B_B2A = 0,
|
||||
SYNC_B2A_A2B,
|
||||
SYNC_A2B,
|
||||
SYNC_B2A
|
||||
};
|
||||
Q_ENUM(SyncDirection)
|
||||
|
||||
enum SyncCompareType {
|
||||
OVERWR_NEWER_IF_DIFF,
|
||||
OVERWR_NEWER_IF_DIFF = 0,
|
||||
OVERWR_NEWER_ALWAYS,
|
||||
OVERWR_IF_DIFF,
|
||||
OVERWR_ALWAYS
|
||||
};
|
||||
Q_ENUM(SyncCompareType)
|
||||
|
||||
SyncProcess(const QString & folderA,
|
||||
const QString & folderB,
|
||||
const int & syncDirection = SYNC_A2B_B2A,
|
||||
const int & compareType = OVERWR_NEWER_IF_DIFF,
|
||||
const qint64 & maxFileSize = 5*1024*1024,
|
||||
const bool dryRun = false);
|
||||
enum SyncOptionFlag {
|
||||
OPT_NONE = 0,
|
||||
OPT_DRY_RUN = 0x01,
|
||||
OPT_SKIP_EMPTY_DIR = 0x02,
|
||||
OPT_RECURSIVE = 0x04,
|
||||
OPT_SKIP_DIR_LINKS = 0x08
|
||||
};
|
||||
Q_DECLARE_FLAGS(SyncOptionFlags, SyncOptionFlag)
|
||||
Q_FLAG(SyncOptionFlags)
|
||||
|
||||
// TODO: perhaps move this to AppData/saved settings once that refactoring is merged.
|
||||
struct SyncOptions {
|
||||
explicit SyncOptions() { reset(); }
|
||||
|
||||
QString folderA;
|
||||
QString folderB;
|
||||
QString excludeFilter; // CSV list of glob pattern(s)
|
||||
QString includeFilter; // CSV list of glob pattern(s)
|
||||
qint64 maxFileSize; // Bytes
|
||||
int direction; // SyncDirection
|
||||
int compareType; // SyncCompareType
|
||||
int flags; // SyncOptionFlags
|
||||
int dirFilterFlags; // QDir::Filters
|
||||
int logLevel; // QtMsgType (see ProgressWidget::addMessage())
|
||||
|
||||
void reset()
|
||||
{
|
||||
excludeFilter = QStringLiteral(".*,System Volume*");
|
||||
includeFilter.clear();
|
||||
direction = SYNC_A2B_B2A;
|
||||
compareType = OVERWR_NEWER_IF_DIFF;
|
||||
maxFileSize = 2 * 1024 * 1024;
|
||||
logLevel = QtWarningMsg;
|
||||
flags = OPT_RECURSIVE;
|
||||
dirFilterFlags = QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot | QDir::Readable;
|
||||
}
|
||||
|
||||
friend inline QDebug operator<<(QDebug debug, const SyncOptions &o) {
|
||||
return debug << "Incl:" << o.includeFilter << "Excl:" << o.excludeFilter << SyncDirection(o.direction) << SyncCompareType(o.compareType) << SyncOptionFlags(o.flags) << QDir::Filters(o.dirFilterFlags);
|
||||
}
|
||||
};
|
||||
|
||||
struct SyncStatus {
|
||||
int index;
|
||||
int count;
|
||||
int created;
|
||||
int updated;
|
||||
int skipped;
|
||||
int errored;
|
||||
void clear() { memset(this, 0, sizeof(SyncStatus)); }
|
||||
};
|
||||
|
||||
explicit SyncProcess(const SyncOptions & options);
|
||||
~SyncProcess();
|
||||
|
||||
public slots:
|
||||
void run();
|
||||
@@ -60,34 +114,39 @@ class SyncProcess : public QObject
|
||||
void started();
|
||||
void finished();
|
||||
void fileCountChanged(int count);
|
||||
void progressStep(int step);
|
||||
void progressMessage(const QString & text, const int & type);
|
||||
void statusMessage(const QString & text);
|
||||
void statusUpdate(const SyncProcess::SyncStatus & status);
|
||||
void statusMessage(const QString & text, const int & type = QtInfoMsg);
|
||||
void progressMessage(const QString & text, const int & type = QtInfoMsg, bool richText = false);
|
||||
|
||||
protected:
|
||||
enum FileFilterResult { FILE_ALLOW, FILE_OVERSIZE, FILE_EXCLUDE, FILE_LINK_IGNORE };
|
||||
|
||||
bool isStopRequsted();
|
||||
void finish();
|
||||
FileFilterResult fileFilter(const QFileInfo & fileInfo);
|
||||
QFileInfoList dirInfoList(const QString & directory);
|
||||
int getFilesCount(const QString & directory);
|
||||
void updateDir(const QString & source, const QString & destination);
|
||||
void pushDirEntries(const QFileInfo & fi, QMutableListIterator<QFileInfo> &it);
|
||||
bool updateEntry(const QString & entry, const QDir & source, const QDir & destination);
|
||||
void pause();
|
||||
void emitProgressMessage(const QString &text, int type);
|
||||
|
||||
QString folder1;
|
||||
QString folder2;
|
||||
SyncDirection direction;
|
||||
SyncCompareType ctype;
|
||||
qint64 maxFileSize; // Bytes
|
||||
QDir::Filters dirFilters;
|
||||
QMutex stopReqMutex;
|
||||
QString reportTemplate;
|
||||
SyncOptions m_options;
|
||||
SyncStatus m_stat;
|
||||
QReadWriteLock stopReqMutex;
|
||||
QString testRunStr;
|
||||
int index;
|
||||
int count;
|
||||
int created;
|
||||
int updated;
|
||||
int skipped;
|
||||
int errored;
|
||||
bool dryRun;
|
||||
QVector<QRegExp> m_excludeFilters;
|
||||
QStringList m_dirIteratorFilters;
|
||||
QDir::Filters m_dirFilters;
|
||||
QDateTime m_startTime;
|
||||
unsigned long m_pauseTime;
|
||||
bool stopping;
|
||||
};
|
||||
|
||||
#endif // _PROCESS_SYNC_H_
|
||||
Q_DECLARE_METATYPE(SyncProcess::SyncOptions)
|
||||
Q_DECLARE_TYPEINFO(SyncProcess::SyncOptions, Q_MOVABLE_TYPE);
|
||||
Q_DECLARE_METATYPE(SyncProcess::SyncStatus)
|
||||
Q_DECLARE_TYPEINFO(SyncProcess::SyncStatus, Q_PRIMITIVE_TYPE);
|
||||
|
||||
#endif // PROCESS_SYNC_H
|
||||
|
||||
@@ -21,19 +21,27 @@
|
||||
#include "progresswidget.h"
|
||||
#include "ui_progresswidget.h"
|
||||
#include "appdata.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QDebug>
|
||||
#include <QTimer>
|
||||
#include <QThread>
|
||||
#include <QScrollBar>
|
||||
|
||||
ProgressWidget::ProgressWidget(QWidget *parent):
|
||||
QWidget(parent),
|
||||
ui(new Ui::ProgressWidget)
|
||||
ProgressWidget::ProgressWidget(QWidget *parent) :
|
||||
QWidget(parent),
|
||||
ui(new Ui::ProgressWidget),
|
||||
m_forceOpen(false),
|
||||
m_hasDetails(false)
|
||||
{
|
||||
ui->setupUi(this);
|
||||
ui->info->hide();
|
||||
ui->checkBox->hide();
|
||||
ui->textEdit->hide();
|
||||
|
||||
ui->checkBox->setChecked(g.outputDisplayDetails());
|
||||
connect(ui->checkBox, &QCheckBox::toggled, this, &ProgressWidget::onShowDetailsToggled);
|
||||
|
||||
QFont newFont(ui->textEdit->font());
|
||||
newFont.setFamily("Courier");
|
||||
#ifdef __APPLE__
|
||||
@@ -43,6 +51,7 @@ ui(new Ui::ProgressWidget)
|
||||
newFont.setPointSize(9);
|
||||
#endif
|
||||
ui->textEdit->setFont(newFont);
|
||||
|
||||
}
|
||||
|
||||
ProgressWidget::~ProgressWidget()
|
||||
@@ -55,10 +64,16 @@ void ProgressWidget::stop()
|
||||
emit stopped();
|
||||
}
|
||||
|
||||
void ProgressWidget::clearDetails() const
|
||||
{
|
||||
ui->textEdit->clear();
|
||||
}
|
||||
|
||||
void ProgressWidget::forceOpen()
|
||||
{
|
||||
m_forceOpen = true;
|
||||
ui->checkBox->hide();
|
||||
ui->textEdit->show();
|
||||
toggleDetails();
|
||||
}
|
||||
|
||||
void ProgressWidget::setInfo(const QString &text)
|
||||
@@ -84,8 +99,11 @@ void ProgressWidget::setValue(int value)
|
||||
|
||||
void ProgressWidget::addText(const QString &text, const bool richText)
|
||||
{
|
||||
ui->checkBox->setVisible(true);
|
||||
ui->checkBox->setChecked(g.outputDisplayDetails());
|
||||
if (!m_hasDetails) {
|
||||
m_hasDetails = true;
|
||||
if (!m_forceOpen)
|
||||
toggleDetails();
|
||||
}
|
||||
|
||||
QTextCursor cursor(ui->textEdit->textCursor());
|
||||
|
||||
@@ -108,7 +126,7 @@ void ProgressWidget::addHtml(const QString & text)
|
||||
addText(text, true);
|
||||
}
|
||||
|
||||
void ProgressWidget::addMessage(const QString & text, const int & type)
|
||||
void ProgressWidget::addMessage(const QString & text, const int & type, bool richText)
|
||||
{
|
||||
QString color;
|
||||
switch (type) {
|
||||
@@ -129,23 +147,47 @@ void ProgressWidget::addMessage(const QString & text, const int & type)
|
||||
break;
|
||||
}
|
||||
if (color.isEmpty()) {
|
||||
if (text.contains(QRegExp("<[^>]+>"))) // detect html (rouhgly)
|
||||
if (richText)
|
||||
addHtml(text % "<br>");
|
||||
else
|
||||
addText(text % "\n", false);
|
||||
addText(text % "\n");
|
||||
}
|
||||
else
|
||||
else {
|
||||
addHtml(QString("<font color=%1>").arg(color) % text % "</font><br>");
|
||||
}
|
||||
}
|
||||
|
||||
QString ProgressWidget::getText()
|
||||
QString ProgressWidget::getText() const
|
||||
{
|
||||
return ui->textEdit->toPlainText();
|
||||
}
|
||||
|
||||
bool ProgressWidget::isEmpty() const
|
||||
{
|
||||
return ui->textEdit->toPlainText().isEmpty();
|
||||
return getText().isEmpty();
|
||||
}
|
||||
|
||||
bool ProgressWidget::detailsVisible() const
|
||||
{
|
||||
return ui->textEdit->isVisible();
|
||||
}
|
||||
|
||||
void ProgressWidget::onShowDetailsToggled(bool checked)
|
||||
{
|
||||
g.outputDisplayDetails(checked);
|
||||
toggleDetails();
|
||||
}
|
||||
|
||||
void ProgressWidget::toggleDetails()
|
||||
{
|
||||
bool showDetails = m_forceOpen || (m_hasDetails && g.outputDisplayDetails());
|
||||
if (!m_forceOpen)
|
||||
ui->checkBox->show();
|
||||
if ((!showDetails && ui->textEdit->isVisible()) || (showDetails && !ui->textEdit->isVisible())) {
|
||||
ui->textEdit->setMinimumHeight(showDetails ? 300 : 0);
|
||||
ui->textEdit->setVisible(showDetails);
|
||||
emit detailsToggled();
|
||||
}
|
||||
}
|
||||
|
||||
void ProgressWidget::setProgressColor(const QColor &color)
|
||||
@@ -160,21 +202,7 @@ void ProgressWidget::addSeparator()
|
||||
addText("\n" HLINE_SEPARATOR "\n");
|
||||
}
|
||||
|
||||
void ProgressWidget::on_checkBox_toggled(bool checked)
|
||||
{
|
||||
g.outputDisplayDetails(checked);
|
||||
ui->textEdit->setVisible(checked);
|
||||
QTimer::singleShot(0, this, SLOT(shrink()));
|
||||
}
|
||||
|
||||
void ProgressWidget::lock(bool lock)
|
||||
{
|
||||
emit locked(lock);
|
||||
}
|
||||
|
||||
void ProgressWidget::shrink()
|
||||
{
|
||||
emit detailsToggled();
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -38,20 +38,24 @@ class ProgressWidget : public QWidget
|
||||
public:
|
||||
explicit ProgressWidget(QWidget *parent);
|
||||
~ProgressWidget();
|
||||
QString getText() const;
|
||||
int maximum();
|
||||
bool isEmpty() const;
|
||||
bool detailsVisible() const;
|
||||
|
||||
public slots:
|
||||
void lock(bool lock);
|
||||
void addText(const QString &text, const bool richText = false);
|
||||
void addHtml(const QString &text);
|
||||
void addMessage(const QString & text, const int & type = QtInfoMsg);
|
||||
QString getText();
|
||||
void addMessage(const QString & text, const int & type = QtInfoMsg, bool richText = false);
|
||||
void setInfo(const QString &text);
|
||||
void setMaximum(int value);
|
||||
int maximum();
|
||||
void setValue(int value);
|
||||
void setProgressColor(const QColor &color);
|
||||
void addSeparator();
|
||||
void forceOpen();
|
||||
void stop();
|
||||
bool isEmpty() const;
|
||||
void clearDetails() const;
|
||||
|
||||
signals:
|
||||
void detailsToggled();
|
||||
@@ -59,11 +63,13 @@ class ProgressWidget : public QWidget
|
||||
void stopped();
|
||||
|
||||
protected slots:
|
||||
void on_checkBox_toggled(bool checked);
|
||||
void shrink();
|
||||
void toggleDetails();
|
||||
void onShowDetailsToggled(bool checked);
|
||||
|
||||
private:
|
||||
Ui::ProgressWidget *ui;
|
||||
bool m_forceOpen;
|
||||
bool m_hasDetails;
|
||||
};
|
||||
|
||||
#endif // _PROGRESSWIDGET_H_
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout" stretch="0,0,1,0">
|
||||
<layout class="QVBoxLayout" name="verticalLayout" stretch="0,0,1">
|
||||
<property name="spacing">
|
||||
<number>6</number>
|
||||
</property>
|
||||
@@ -41,10 +41,10 @@
|
||||
<property name="bottomMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<item alignment="Qt::AlignTop">
|
||||
<widget class="QLabel" name="info">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Maximum">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
@@ -54,7 +54,7 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<item alignment="Qt::AlignTop">
|
||||
<widget class="QWidget" name="widget" native="true">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout" stretch="1,0">
|
||||
<property name="spacing">
|
||||
@@ -75,7 +75,7 @@
|
||||
<item>
|
||||
<widget class="QProgressBar" name="progressBar">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Maximum">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
@@ -107,17 +107,11 @@
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>350</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="font">
|
||||
<font>
|
||||
<family>Courier</family>
|
||||
@@ -145,19 +139,6 @@ p, li { white-space: pre-wrap; }
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
|
||||
@@ -7,6 +7,7 @@ set(shared_SRCS
|
||||
)
|
||||
|
||||
set(shared_HDRS
|
||||
autobitsetcheckbox.h
|
||||
autocheckbox.h
|
||||
autocombobox.h
|
||||
autodoublespinbox.h
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
/*
|
||||
* Copyright (C) OpenTX
|
||||
*
|
||||
* Based on code named
|
||||
* th9x - http://code.google.com/p/th9x
|
||||
* er9x - http://code.google.com/p/er9x
|
||||
* gruvin9x - http://code.google.com/p/gruvin9x
|
||||
*
|
||||
* License GPLv2: http://www.gnu.org/licenses/gpl-2.0.html
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License version 2 as
|
||||
* published by the Free Software Foundation.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*/
|
||||
|
||||
#ifndef AUTOBITSETCHECKBOX_H
|
||||
#define AUTOBITSETCHECKBOX_H
|
||||
|
||||
#include <QCheckBox>
|
||||
#include "genericpanel.h"
|
||||
|
||||
class AutoBitsetCheckBox: public QCheckBox
|
||||
{
|
||||
Q_OBJECT
|
||||
//! \property bitmask The value (shifted bit(s)) to set/clear based on checked state.
|
||||
Q_PROPERTY(int bitmask READ bitmask WRITE setBitmask)
|
||||
//! \property inverted If \e true, then the \p bitmask is cleared when the checkbox is checked, and vice versa. Default behavior is to set the bitmask when checked.
|
||||
Q_PROPERTY(bool inverted READ inverted WRITE setInverted)
|
||||
//! \property toggleMask If > 0, then set this value when clearing the \p bitmask value (toggles between the two masks being set). \p inverted is ignored if \p toggleMask is set.
|
||||
Q_PROPERTY(int toggleMask READ toggleMask WRITE setToggleMask)
|
||||
|
||||
public:
|
||||
explicit AutoBitsetCheckBox(QWidget *parent = nullptr) : AutoBitsetCheckBox(QString(), parent) {}
|
||||
explicit AutoBitsetCheckBox(const QString &text, QWidget *parent = nullptr) :
|
||||
QCheckBox(text, parent)
|
||||
{
|
||||
init();
|
||||
}
|
||||
|
||||
// int field constructors
|
||||
explicit AutoBitsetCheckBox(int & field, int bitmask, const QString &text = QString(), QWidget *parent = nullptr) : AutoBitsetCheckBox(field, bitmask, false, text, parent) {}
|
||||
explicit AutoBitsetCheckBox(int & field, int bitmask, bool invert, const QString &text = QString(), QWidget *parent = nullptr) : QCheckBox(text, parent)
|
||||
{
|
||||
setField(field, bitmask, invert);
|
||||
init();
|
||||
}
|
||||
explicit AutoBitsetCheckBox(int & field, int bitmask, int toggleMask, const QString &text = QString(), QWidget *parent = nullptr) : QCheckBox(text, parent)
|
||||
{
|
||||
setField(field, bitmask, false, toggleMask);
|
||||
init();
|
||||
}
|
||||
|
||||
// unsigned field constructors
|
||||
explicit AutoBitsetCheckBox(unsigned & field, int bitmask, const QString &text = QString(), QWidget *parent = nullptr) : AutoBitsetCheckBox(field, bitmask, false, text, parent) {}
|
||||
explicit AutoBitsetCheckBox(unsigned & field, int bitmask, bool invert, const QString &text = QString(), QWidget *parent = nullptr) : QCheckBox(text, parent)
|
||||
{
|
||||
setField(field, bitmask, invert);
|
||||
init();
|
||||
}
|
||||
explicit AutoBitsetCheckBox(unsigned & field, int bitmask, int toggleMask, const QString &text = QString(), QWidget *parent = nullptr) : QCheckBox(text, parent)
|
||||
{
|
||||
setField(field, bitmask, false, toggleMask);
|
||||
init();
|
||||
}
|
||||
|
||||
inline int bitmask() const { return m_bitmask; }
|
||||
inline int toggleMask() const { return m_toggleMask; }
|
||||
inline bool inverted() const { return m_toggleMask ? false : m_invert; }
|
||||
|
||||
public slots:
|
||||
void setField(int & field, int bitmask = 0, bool invert = false, int toggleMask = 0)
|
||||
{
|
||||
m_field = &field;
|
||||
initField(bitmask, invert, toggleMask);
|
||||
}
|
||||
|
||||
void setField(unsigned & field, int bitmask = 0, bool invert = false, int toggleMask = 0)
|
||||
{
|
||||
m_field = reinterpret_cast<int *>(&field);
|
||||
initField(bitmask, invert, toggleMask);
|
||||
}
|
||||
|
||||
void setBitmask(int bitmask)
|
||||
{
|
||||
m_bitmask = bitmask;
|
||||
updateValue();
|
||||
}
|
||||
|
||||
void setToggleMask(int toggleMask)
|
||||
{
|
||||
m_toggleMask = toggleMask;
|
||||
updateValue();
|
||||
}
|
||||
|
||||
void setInverted(bool invert)
|
||||
{
|
||||
m_invert = invert;
|
||||
updateValue();
|
||||
}
|
||||
|
||||
void updateValue()
|
||||
{
|
||||
if (!m_field)
|
||||
return;
|
||||
const bool oldLock = setLocked(true);
|
||||
setChecked((m_invert && !m_toggleMask) != bool(*m_field & m_bitmask));
|
||||
setLocked(oldLock);
|
||||
}
|
||||
|
||||
void setPanel(GenericPanel * panel)
|
||||
{
|
||||
m_panel = panel;
|
||||
}
|
||||
|
||||
protected slots:
|
||||
void init()
|
||||
{
|
||||
connect(this, &AutoBitsetCheckBox::toggled, this, &AutoBitsetCheckBox::onToggled);
|
||||
}
|
||||
|
||||
void initField(int bitmask, bool invert, int toggleMask)
|
||||
{
|
||||
m_bitmask = bitmask;
|
||||
m_toggleMask = toggleMask;
|
||||
m_invert = toggleMask ? false : invert;
|
||||
updateValue();
|
||||
}
|
||||
|
||||
void onToggled(bool checked)
|
||||
{
|
||||
if (!m_field)
|
||||
return;
|
||||
|
||||
if (m_toggleMask) {
|
||||
if (checked) {
|
||||
*m_field &= ~m_toggleMask;
|
||||
*m_field |= m_bitmask;
|
||||
}
|
||||
else {
|
||||
*m_field &= ~m_bitmask;
|
||||
*m_field |= m_toggleMask;
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (m_invert)
|
||||
checked = !checked;
|
||||
if (checked)
|
||||
*m_field |= m_bitmask;
|
||||
else
|
||||
*m_field &= ~m_bitmask;
|
||||
}
|
||||
|
||||
if (m_panel && !m_lock)
|
||||
emit m_panel->modified();
|
||||
}
|
||||
|
||||
bool setLocked(bool lock)
|
||||
{
|
||||
const bool ret = m_lock;
|
||||
m_lock = lock;
|
||||
return ret;
|
||||
}
|
||||
|
||||
protected:
|
||||
int * m_field = nullptr;
|
||||
int m_bitmask = 0;
|
||||
int m_toggleMask = 0;
|
||||
bool m_invert = false;
|
||||
bool m_lock = false;
|
||||
GenericPanel * m_panel = nullptr;
|
||||
};
|
||||
|
||||
#endif // AUTOBITSETCHECKBOX_H
|
||||
@@ -63,50 +63,53 @@ class AutoComboBox: public QComboBox
|
||||
{
|
||||
lock = true;
|
||||
QComboBox::addItem(item, value);
|
||||
if (field && *field==value) {
|
||||
setCurrentIndex(count()-1);
|
||||
}
|
||||
lock = false;
|
||||
updateValue();
|
||||
}
|
||||
|
||||
void setField(unsigned int & field, GenericPanel * panel=NULL)
|
||||
{
|
||||
this->field = (int *)&field;
|
||||
this->panel = panel;
|
||||
for (int i=0; i<count(); ++i) {
|
||||
if ((int)field == itemData(i))
|
||||
setCurrentIndex(i);
|
||||
}
|
||||
updateValue();
|
||||
}
|
||||
|
||||
void setField(int & field, GenericPanel * panel=NULL)
|
||||
{
|
||||
this->field = &field;
|
||||
this->panel = panel;
|
||||
for (int i=0; i<count(); ++i) {
|
||||
if ((int)field == itemData(i))
|
||||
setCurrentIndex(i);
|
||||
}
|
||||
updateValue();
|
||||
}
|
||||
|
||||
void setAutoIndexes()
|
||||
{
|
||||
for (int i=0; i<count(); ++i) {
|
||||
for (int i=0; i<count(); ++i)
|
||||
setItemData(i, i);
|
||||
if (this->field && *this->field == i)
|
||||
setCurrentIndex(i);
|
||||
}
|
||||
updateValue();
|
||||
}
|
||||
|
||||
void updateValue()
|
||||
{
|
||||
if (!field)
|
||||
return;
|
||||
lock = true;
|
||||
setCurrentIndex(findData(*field));
|
||||
lock = false;
|
||||
}
|
||||
|
||||
signals:
|
||||
void currentDataChanged(int value);
|
||||
|
||||
protected slots:
|
||||
void onCurrentIndexChanged(int index)
|
||||
{
|
||||
const int val = itemData(index).toInt();
|
||||
if (field && !lock) {
|
||||
*field = itemData(index).toInt();
|
||||
if (panel) {
|
||||
*field = val;
|
||||
if (panel)
|
||||
emit panel->modified();
|
||||
}
|
||||
}
|
||||
emit currentDataChanged(val);
|
||||
}
|
||||
|
||||
protected:
|
||||
|
||||
@@ -25,54 +25,72 @@
|
||||
#include <QRegExpValidator>
|
||||
#include "genericpanel.h"
|
||||
|
||||
#define CHAR_FOR_NAMES_REGEX "[ A-Za-z0-9_.-,]*"
|
||||
|
||||
class AutoLineEdit: public QLineEdit
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit AutoLineEdit(QWidget *parent = 0):
|
||||
explicit AutoLineEdit(QWidget *parent = nullptr, bool updateOnChange = false):
|
||||
QLineEdit(parent),
|
||||
field(NULL),
|
||||
strField(NULL),
|
||||
panel(NULL),
|
||||
lock(false)
|
||||
{
|
||||
QRegExp rx(CHAR_FOR_NAMES_REGEX);
|
||||
setValidator(new QRegExpValidator(rx, this));
|
||||
connect(this, SIGNAL(editingFinished()), this, SLOT(onEdited()));
|
||||
if (updateOnChange)
|
||||
connect(this, &AutoLineEdit::textChanged, this, &AutoLineEdit::onEdited);
|
||||
else
|
||||
connect(this, &AutoLineEdit::editingFinished, this, &AutoLineEdit::onEdited);
|
||||
}
|
||||
|
||||
void setField(char * field, int len, GenericPanel * panel=NULL)
|
||||
void setField(char * field, int len, GenericPanel * panel = nullptr)
|
||||
{
|
||||
this->field = field;
|
||||
this->panel = panel;
|
||||
setValidator(new QRegExpValidator(QRegExp("[ A-Za-z0-9_.-,]*"), this));
|
||||
setMaxLength(len);
|
||||
updateValue();
|
||||
}
|
||||
|
||||
void setField(QString & field, int len = 0, GenericPanel * panel = nullptr)
|
||||
{
|
||||
strField = &field;
|
||||
this->panel = panel;
|
||||
if (len)
|
||||
setMaxLength(len);
|
||||
updateValue();
|
||||
}
|
||||
|
||||
void updateValue()
|
||||
{
|
||||
lock = true;
|
||||
if (field) {
|
||||
if (field)
|
||||
setText(field);
|
||||
}
|
||||
else if (strField)
|
||||
setText(*strField);
|
||||
lock = false;
|
||||
}
|
||||
|
||||
protected slots:
|
||||
void onEdited()
|
||||
{
|
||||
if (field && !lock) {
|
||||
if (lock)
|
||||
return;
|
||||
|
||||
if (field)
|
||||
strcpy(field, text().toLatin1());
|
||||
if (panel) {
|
||||
emit panel->modified();
|
||||
}
|
||||
}
|
||||
else if (strField)
|
||||
*strField = text();
|
||||
else
|
||||
return;
|
||||
|
||||
if (panel)
|
||||
emit panel->modified();
|
||||
}
|
||||
|
||||
protected:
|
||||
char * field;
|
||||
QString * strField;
|
||||
GenericPanel * panel;
|
||||
bool lock;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user