Merge pull request #6009 from opentx/mpaperno/cpn_fsync_v2

SD Sync "version 3"
This commit is contained in:
Bertrand Songis
2018-07-05 19:07:10 +02:00
committed by GitHub
13 changed files with 1299 additions and 483 deletions
+2
View File
@@ -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
+502
View File
@@ -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(); });
}
+96
View File
@@ -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
View File
@@ -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
View File
@@ -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 -&gt; %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 -&gt; %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 -&gt; %2 :<br>&nbsp;&nbsp;&nbsp;&nbsp; %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);
}
}
+90 -31
View File
@@ -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
+54 -26
View File
@@ -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();
}
+12 -6
View File
@@ -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_
+6 -25
View File
@@ -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/>
+1
View File
@@ -7,6 +7,7 @@ set(shared_SRCS
)
set(shared_HDRS
autobitsetcheckbox.h
autocheckbox.h
autocombobox.h
autodoublespinbox.h
+178
View File
@@ -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
+21 -18
View File
@@ -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:
+32 -14
View File
@@ -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;
};