diff --git a/source/app/powertabeditor.cpp b/source/app/powertabeditor.cpp index 6486e73fc..a6a8b3939 100644 --- a/source/app/powertabeditor.cpp +++ b/source/app/powertabeditor.cpp @@ -98,6 +98,7 @@ #include #include #include +#include #include #include #include @@ -504,6 +505,12 @@ void PowerTabEditor::printDocument() getScoreArea()->print(printer); } +void PowerTabEditor::bulkConverter() +{ + BulkConverterDialog dialog(this, myFileFormatManager); + dialog.exec(); +} + void PowerTabEditor::printPreview() { // Set the window flags to Qt::Window so that the dialog can be maximized. @@ -1546,7 +1553,7 @@ void PowerTabEditor::editLeftHandFingering() LeftHandFingeringDialog dialog(this); if (dialog.exec() == QDialog::Accepted) { - myUndoManager->push(new AddLeftHandFingering(location, + myUndoManager->push(new AddLeftHandFingering(location, dialog.getLeftHandFingering()), location.getSystemIndex()); } @@ -1883,6 +1890,12 @@ void PowerTabEditor::createCommands() connect(myPrintPreviewCommand, &QAction::triggered, this, &PowerTabEditor::printPreview); + myBulkConverterCommand = new Command(tr("Bulk Converter"), + "File.BulkConverter", + QKeySequence(), this); + connect(myBulkConverterCommand, &QAction::triggered, this, + &PowerTabEditor::bulkConverter); + myEditShortcutsCommand = new Command(tr("Customize Shortcuts..."), "File.CustomizeShortcuts", QKeySequence(), this); @@ -2220,7 +2233,7 @@ void PowerTabEditor::createCommands() Qt::SHIFT + Qt::Key_Left, this); connect(myRemoveDotCommand, &QAction::triggered, this, &PowerTabEditor::removeDot); - myLeftHandFingeringCommand = new Command(tr("Left Hand Fingering..."), + myLeftHandFingeringCommand = new Command(tr("Left Hand Fingering..."), "Notes.LeftHandFingering", QKeySequence(), this); myLeftHandFingeringCommand->setCheckable(true); @@ -2530,7 +2543,7 @@ void PowerTabEditor::createCommands() &PowerTabEditor::editViewFilters); // Window Menu commands. - + #ifdef Q_OS_MAC // NextChild is Command-{ on OS X, so use the more conventional Control-Tab // to match Safari, Finder, etc. @@ -2540,7 +2553,7 @@ void PowerTabEditor::createCommands() QKeySequence next_tab_seq = QKeySequence::NextChild; QKeySequence prev_tab_seq = QKeySequence::PreviousChild; #endif - + myNextTabCommand = new Command(tr("Next Tab"), "Window.NextTab", next_tab_seq, this); connect(myNextTabCommand, &QAction::triggered, [=]() { @@ -2723,6 +2736,8 @@ void PowerTabEditor::createMenus() myFileMenu->addSeparator(); myRecentFilesMenu = myFileMenu->addMenu(tr("Recent Files")); myFileMenu->addSeparator(); + myFileMenu->addAction(myBulkConverterCommand); + myFileMenu->addSeparator(); myFileMenu->addAction(myEditShortcutsCommand); myFileMenu->addAction(myEditPreferencesCommand); myFileMenu->addSeparator(); diff --git a/source/app/powertabeditor.h b/source/app/powertabeditor.h index 7d0dc755e..6b5864f6a 100644 --- a/source/app/powertabeditor.h +++ b/source/app/powertabeditor.h @@ -14,7 +14,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - + #ifndef APP_POWERTABEDITOR_H #define APP_POWERTABEDITOR_H @@ -84,6 +84,9 @@ private slots: /// Prints the current document. void printDocument(); + /// Bulk conversion tool + void bulkConverter(); + /// Displays a preview of the current document. void printPreview(); @@ -429,6 +432,7 @@ private slots: Command *mySaveAsCommand; Command *myPrintCommand; Command *myPrintPreviewCommand; + Command *myBulkConverterCommand; QMenu *myRecentFilesMenu; Command *myEditShortcutsCommand; Command *myEditPreferencesCommand; @@ -463,7 +467,7 @@ private slots: Command *myPrevPositionCommand; Command *myNextStringCommand; Command *myPrevStringCommand; - Command *myLastPositionCommand; + Command *myLastPositionCommand; Command *myNextStaffCommand; Command *myPrevStaffCommand; Command *myNextBarCommand; diff --git a/source/dialogs/CMakeLists.txt b/source/dialogs/CMakeLists.txt index 790bf3577..4dc71d35a 100644 --- a/source/dialogs/CMakeLists.txt +++ b/source/dialogs/CMakeLists.txt @@ -6,6 +6,7 @@ set( srcs artificialharmonicdialog.cpp barlinedialog.cpp benddialog.cpp + bulkconverterdialog.cpp chordnamedialog.cpp crashdialog.cpp directiondialog.cpp @@ -41,6 +42,7 @@ set( headers artificialharmonicdialog.h barlinedialog.h benddialog.h + bulkconverterdialog.h chordnamedialog.h crashdialog.h directiondialog.h @@ -76,6 +78,7 @@ set( moc_headers artificialharmonicdialog.h barlinedialog.h benddialog.h + bulkconverterdialog.h chordnamedialog.h crashdialog.h directiondialog.h @@ -104,6 +107,7 @@ set( forms artificialharmonicdialog.ui barlinedialog.ui benddialog.ui + bulkconverterdialog.ui chordnamedialog.ui crashdialog.ui directiondialog.ui diff --git a/source/dialogs/bulkconverterdialog.cpp b/source/dialogs/bulkconverterdialog.cpp new file mode 100644 index 000000000..4dc5bdd0e --- /dev/null +++ b/source/dialogs/bulkconverterdialog.cpp @@ -0,0 +1,236 @@ +/* + * Copyright (C) 2020 Simon Symeonidis + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +#include "bulkconverterdialog.h" +#include "ui_bulkconverterdialog.h" + +#include "formats/powertab/common.h" + +#include +#include + +#include +#include +#include +#include + +std::optional convertFile(const boost::filesystem::path& src, + const boost::filesystem::path& dst, + std::unique_ptr& ffm) +{ + QFileInfo fileInfo(QString::fromStdString(src.string())); + std::optional format = ffm->findFormat( + fileInfo.suffix().toStdString()); + + if (!format) return "bad format"; + + Score score; + try + { + ffm->importFile(score, src, *format); + } + catch (const std::exception &e) + { + return "could not import file: " + src.string(); + } + + try + { + ffm->exportFile(score, dst, FileFormat(getPowerTabFileFormat())); + } + catch (const std::exception &e) + { + return "could not export file: " + dst.string(); + } + + return std::nullopt; +} + +BulkConverterWorker::BulkConverterWorker(boost::filesystem::path& source, + boost::filesystem::path& destination, + bool dryRun, + std::unique_ptr& fileFormatManager) + : QObject(), mySrc(source), myDst(destination), myDryRun(dryRun), + myFileCount(0), myFileFormatManager(fileFormatManager) +{ +} + +BulkConverterWorker::~BulkConverterWorker() +{ +} + +void BulkConverterWorker::walkAndConvert() +{ + std::vector children; + const std::string basePath = mySrc.string(); + + children.emplace_back(mySrc); + + do { + boost::filesystem::path p = children.back(); + children.pop_back(); + + if (!boost::filesystem::is_directory(p)) continue; + + auto dir_it = boost::filesystem::directory_iterator(p); + + for(auto& entry : boost::make_iterator_range(dir_it, {})) { + if (boost::filesystem::is_directory(entry)) { + children.emplace_back(entry); + continue; + } + + // TODO: this should probably handle any supported format. + // check to see if there is a list of file formats + // like this + if (!(boost::filesystem::is_regular_file(entry) && + boost::filesystem::extension(entry) == ".ptb")) + continue; + + myFileCount++; + + auto filePath = entry.path().string(); + const size_t pos = filePath.find(basePath); + if (pos != std::string::npos) + filePath.erase(pos, basePath.length()); + + auto toPath = myDst / boost::filesystem::path(filePath); + toPath.replace_extension("pt2"); + + if (myDryRun) continue; + + auto destBaseDir = toPath.parent_path(); + if (!boost::filesystem::exists(destBaseDir)) + boost::filesystem::create_directory(destBaseDir); + + std::optional error = convertFile(entry, toPath, myFileFormatManager); + if (error != std::nullopt) { + QString err = QString::fromStdString(error.value()); + emit errorMessage("error processing file: " + err); + } + + emit progress((int) myFileCount); + } + } while (!children.empty()); + + emit done(); +} + +BulkConverterDialog::BulkConverterDialog(QWidget *parent, + std::unique_ptr& manager) + : QDialog(parent), ui(new Ui::BulkConverterDialog), + myBulkWorkerThread(nullptr), myBulkConverterWorker(nullptr), + myFileFormatManager(manager) +{ + ui->setupUi(this); + + auto flags = windowFlags(); + setWindowFlags(flags | Qt::WindowStaysOnTopHint); + + connect(ui->fileLocatorSource, &QAbstractButton::clicked, [=]() { + setPath(ui->sourcePathEdit); + }); + + connect(ui->fileLocatorDestination, &QAbstractButton::clicked, [=]() { + setPath(ui->destinationPathEdit); + }); + + connect(ui->convertButton, &QAbstractButton::clicked, [=](){ convert(); }); + + connect(ui->exitButton, &QAbstractButton::clicked, this, &QDialog::reject); +} + +BulkConverterDialog::~BulkConverterDialog() +{ + if (myBulkWorkerThread) { + myBulkWorkerThread->quit(); + myBulkWorkerThread->wait(); + } + delete ui; +} + +void BulkConverterDialog::setPath(QLineEdit *edit) +{ + auto dialogOptions = QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks; + QString path = QFileDialog::getExistingDirectory( + this, tr("Directory To Convert"), QDir::currentPath(), dialogOptions); + + edit->setText(path); + QString src = ui->sourcePathEdit->text(); + QString dst = ui->destinationPathEdit->text(); + const bool bothEditsHaveValues = !src.isEmpty() && !dst.isEmpty(); + ui->convertButton->setEnabled(bothEditsHaveValues); +} + +void BulkConverterDialog::convert() +{ + ui->convertButton->setEnabled(false); + + boost::filesystem::path src = Paths::fromQString(ui->sourcePathEdit->text()); + boost::filesystem::path dst = Paths::fromQString(ui->destinationPathEdit->text()); + + { + // set default max. + BulkConverterWorker bcw(src, dst, true, myFileFormatManager); + bcw.walkAndConvert(); + const std::size_t fileCountToConvert = bcw.fileCount(); + ui->progressBar->setMaximum((int) fileCountToConvert); + ui->progressBar->setValue(0); + } + + // create thread to offload work in the background, such that we can + // update the progress bar with each step informing the user of the + // current state of bulk conversion. + myBulkWorkerThread = new QThread; + myBulkConverterWorker = new BulkConverterWorker(src, dst, false, myFileFormatManager); + myBulkConverterWorker->moveToThread(myBulkWorkerThread); + + connect(myBulkWorkerThread, &QThread::started, + myBulkConverterWorker, &BulkConverterWorker::walkAndConvert); + + connect(myBulkWorkerThread, &QThread::finished, + myBulkWorkerThread, &QObject::deleteLater); + + connect(myBulkConverterWorker, &BulkConverterWorker::progress, + this, &BulkConverterDialog::progress); + + connect(myBulkConverterWorker, &BulkConverterWorker::done, + myBulkConverterWorker, &QObject::deleteLater); + + connect(myBulkConverterWorker, &BulkConverterWorker::done, + this, &BulkConverterDialog::enableConvertButton); + + connect(myBulkConverterWorker, &BulkConverterWorker::errorMessage, + this, &BulkConverterDialog::consumeErrorMessage); + + myBulkWorkerThread->start(); +} + +void BulkConverterDialog::progress(int pos) +{ + ui->progressBar->setValue(pos); +} + +void BulkConverterDialog::enableConvertButton(void) +{ + ui->convertButton->setEnabled(true); +} + +void BulkConverterDialog::consumeErrorMessage(QString error) +{ + ui->logger->appendPlainText(error); +} diff --git a/source/dialogs/bulkconverterdialog.h b/source/dialogs/bulkconverterdialog.h new file mode 100644 index 000000000..43a225ca4 --- /dev/null +++ b/source/dialogs/bulkconverterdialog.h @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2020 Simon Symeonidis + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +#ifndef DIALOGS_BULKCONVERTERDIALOG_H +#define DIALOGS_BULKCONVERTERDIALOG_H + +#include +#include +#include +#include + +#include +#include + +#include + +#include + +namespace Ui { + class BulkConverterDialog; +} + +class BulkConverterWorker : public QObject +{ + Q_OBJECT + +public: + explicit BulkConverterWorker(boost::filesystem::path& source, + boost::filesystem::path& destination, + bool dryRun, + std::unique_ptr& fileFormatManager); + ~BulkConverterWorker(); + + inline void setFileCount(std::size_t f) { myFileCount = f; } + inline std::size_t fileCount() { return myFileCount; } + +public slots: + void walkAndConvert(); + +signals: + void progress(int pos); + void done(void); + void errorMessage(QString error); + +private: + boost::filesystem::path mySrc; + boost::filesystem::path myDst; + bool myDryRun; + std::size_t myFileCount; + std::unique_ptr& myFileFormatManager; +}; + +class BulkConverterDialog : public QDialog { + Q_OBJECT + +public: + explicit BulkConverterDialog(QWidget *parent, + std::unique_ptr& fileFormatManager); + ~BulkConverterDialog(); + +public slots: + void progress(int pos); + void consumeErrorMessage(QString error); + +private slots: + void setPath(QLineEdit*); + void convert(); + void enableConvertButton(void); + +private: + Ui::BulkConverterDialog *ui; + QThread* myBulkWorkerThread; + BulkConverterWorker* myBulkConverterWorker; + std::unique_ptr& myFileFormatManager;; +}; + +#endif diff --git a/source/dialogs/bulkconverterdialog.ui b/source/dialogs/bulkconverterdialog.ui new file mode 100644 index 000000000..b82452d9e --- /dev/null +++ b/source/dialogs/bulkconverterdialog.ui @@ -0,0 +1,155 @@ + + + BulkConverterDialog + + + + 0 + 0 + 356 + 299 + + + + Dialog + + + + + 10 + 110 + 331 + 23 + + + + 0 + + + true + + + + + + 299 + 30 + 41 + 21 + + + + ... + + + + + false + + + + 10 + 30 + 281 + 21 + + + + + + + 10 + 10 + 111 + 16 + + + + Source Directory + + + + + false + + + + 10 + 80 + 281 + 21 + + + + + + + 10 + 60 + 281 + 16 + + + + Destination Directory + + + + + + 300 + 80 + 41 + 21 + + + + ... + + + + + + 10 + 180 + 331 + 101 + + + + true + + + + + false + + + + 10 + 150 + 241 + 21 + + + + &Convert + + + + + + 260 + 150 + 80 + 21 + + + + E&xit + + + + + +