diff --git a/CMakeLists.txt b/CMakeLists.txt index 1163433..123e07f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -17,7 +17,7 @@ if(SANITIZE_ADDRESS_LEAK) set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -fsanitize=address -fsanitize=leak") endif(SANITIZE_ADDRESS_LEAK) -find_package(Qt6 COMPONENTS Widgets Sql OpenGLWidgets REQUIRED) +find_package(Qt6 COMPONENTS Widgets Sql OpenGLWidgets Qml REQUIRED) find_library(GSL_LIB gsl REQUIRED) find_library(GSLCBLAS_LIB gslcblas REQUIRED) find_library(EXIF_LIB exif REQUIRED) @@ -29,6 +29,7 @@ add_subdirectory(libXISF) set(TENMON_SRC about.cpp about.h + batchprocessing.cpp batchprocessing.h batchprocessing.ui database.cpp database.h databaseview.cpp databaseview.h delete.cpp @@ -44,6 +45,7 @@ set(TENMON_SRC markedfiles.cpp markedfiles.h rawimage.cpp rawimage.h rawimage_sse.cpp + scriptengine.cpp scriptengine.h settingsdialog.cpp settingsdialog.h starfit.cpp starfit.h statusbar.cpp statusbar.h @@ -67,11 +69,12 @@ elseif(APPLE) set_source_files_properties(${tenmon_ICON} PROPERTIES MACOSX_PACKAGE_LOCATION "Resources") else() set(tenmon_ICON "") + find_package(Qt6 COMPONENTS DBus REQUIRED) find_package(PkgConfig REQUIRED) pkg_search_module(GIO REQUIRED gio-2.0) endif() -add_executable(tenmon WIN32 MACOSX_BUNDLE ${tenmon_ICON} ${TENMON_SRC}) +qt_add_executable(tenmon WIN32 MACOSX_BUNDLE ${tenmon_ICON} ${TENMON_SRC}) find_path(FITS_INCLUDE fitsio2.h PATH_SUFFIXES cfitsio REQUIRED) target_include_directories(tenmon PRIVATE ${FITS_INCLUDE} ${CMAKE_BINARY_DIR} ${libXISF_SOURCE_DIR}) @@ -80,16 +83,16 @@ if(UNIX AND NOT APPLE) target_include_directories(tenmon PRIVATE ${GIO_INCLUDE_DIRS}) endif() -target_link_libraries(tenmon Qt6::Widgets Qt6::Sql Qt6::OpenGLWidgets ${GSL_LIB} ${GSLCBLAS_LIB} ${EXIF_LIB} ${FITS_LIB} ${RAW_LIB} ${WCS_LIB} XISF) +target_link_libraries(tenmon PRIVATE Qt6::Widgets Qt6::Sql Qt6::OpenGLWidgets Qt6::Qml ${GSL_LIB} ${GSLCBLAS_LIB} ${EXIF_LIB} ${FITS_LIB} ${RAW_LIB} ${WCS_LIB} XISF) if(APPLE) - target_link_libraries(tenmon "-framework CoreFoundation") -else() - target_link_libraries(tenmon ${GIO_LDFLAGS}) + target_link_libraries(tenmon PRIVATE "-framework CoreFoundation") +elseif(UNIX) + target_link_libraries(tenmon PRIVATE Qt6::DBus ${GIO_LDFLAGS}) endif(APPLE) if(LIBRAW_STATIC) add_compile_definitions("LIBRAW_NODLL") - target_link_libraries(tenmon jasper) + target_link_libraries(tenmon PRIVATE jasper) endif() install(TARGETS tenmon BUNDLE DESTINATION .) diff --git a/batchprocessing.cpp b/batchprocessing.cpp new file mode 100644 index 0000000..f2d369e --- /dev/null +++ b/batchprocessing.cpp @@ -0,0 +1,208 @@ +#include "batchprocessing.h" +#include "ui_batchprocessing.h" +#include +#include +#include +#include +#include +#include "scriptengine.h" + +#ifdef Q_OS_LINUX +#include +#include +#include +#include +#endif + +void scanDirectory(const QString &path, QStringList &files) +{ + QFileInfo info(path); + if(info.isDir()) + { + QDir dir(path); + QStringList entries = dir.entryList(QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot); + for(QString &entry : entries) + scanDirectory(dir.absoluteFilePath(entry), files); + } + else if(info.isFile()) + { + files.append(path); + } +} + +QStringList scanDirectories(const QStringList &paths) +{ + QStringList files; + + for(const QString &path : paths) + scanDirectory(path, files); + + return files; +} + +void BatchProcessing::scanScriptDir() +{ + _ui->scriptsList->clear(); + QDir dir(_scriptBasePath); + for(const QString &script : dir.entryList(QDir::Files | QDir::Readable)) + { + _ui->scriptsList->addItem(script); + } +} + +BatchProcessing::BatchProcessing(QWidget *parent) : QDialog(parent) +{ + _ui = new Ui::BatchProcessing; + _ui->setupUi(this); + + QStringList scriptsPath = QStandardPaths::standardLocations(QStandardPaths::AppDataLocation); + if(scriptsPath.size()) + { + QDir dir(scriptsPath.first()); + if(!dir.exists("scripts")) + { + if(!dir.mkpath("scripts")) + qWarning() << "Failed to create scripts directory"; + } + dir.cd("scripts"); + + _scriptBasePath = dir.absolutePath() + "/"; + scanScriptDir(); + _fileWatcher.addPath(_scriptBasePath); + connect(&_fileWatcher, &QFileSystemWatcher::directoryChanged, this, &BatchProcessing::scanScriptDir); + } + else + { + qWarning() << "Failed to get app data location"; + } + + connect(_ui->addFilesButton, &QPushButton::released, this, &BatchProcessing::addFiles); + connect(_ui->addDirButton, &QPushButton::released, this, &BatchProcessing::addDir); + connect(_ui->removeButton, &QPushButton::released, this, &BatchProcessing::removePath); + connect(_ui->removeAllButton, &QPushButton::released, this, &BatchProcessing::removeAllPaths); + connect(_ui->startButton, &QPushButton::released, this, &BatchProcessing::runScript); + connect(_ui->stopButton, &QPushButton::released, this, &BatchProcessing::stopScript); + connect(_ui->browseButton, &QPushButton::released, this, &BatchProcessing::browse); + connect(_ui->openScriptsButton, &QPushButton::released, this, &BatchProcessing::openScriptDir); + + QSettings settings; + _ui->outputPath->setText(settings.value("batchprocessing/outputpath", QStandardPaths::standardLocations(QStandardPaths::PicturesLocation).first()).toString()); +} + +BatchProcessing::~BatchProcessing() +{ + delete _engineThread; + QSettings settings; + settings.setValue("batchprocessing/outputpath", _ui->outputPath->text()); + delete _ui; +} + +void BatchProcessing::closeEvent(QCloseEvent *event) +{ + if(_engineThread) + { + QMessageBox::StandardButton ret = QMessageBox::question(this, tr("Interrupt running script?"), tr("Interrupt running script?")); + if(ret == QMessageBox::StandardButton::Yes) + { + _engineThread->interrupt(); + event->accept(); + } + else + { + event->ignore(); + } + } + else + { + event->accept(); + } +} + +void BatchProcessing::addFiles() +{ + QStringList files = QFileDialog::getOpenFileNames(this, tr("Select files"), "/home/nou/Obrázky/astro"); + _ui->pathsList->addItems(files); +} + +void BatchProcessing::addDir() +{ + QString dir = QFileDialog::getExistingDirectory(this, tr("Select directory"), "/home/nou/Obrázky/astro"); + if(!dir.isEmpty()) + _ui->pathsList->addItem(dir); +} + +void BatchProcessing::removePath() +{ + for(auto &item : _ui->pathsList->selectedItems()) + delete item; +} + +void BatchProcessing::removeAllPaths() +{ + _ui->pathsList->clear(); +} + +void BatchProcessing::browse() +{ + QString output = QFileDialog::getExistingDirectory(this, tr("Select output directory"), "/home/nou/Obrázky"); + if(!output.isEmpty()) + _ui->outputPath->setText(output); +} + +void BatchProcessing::openScriptDir() +{ +#ifdef Q_OS_LINUX + QDBusConnection con = QDBusConnection::sessionBus(); + QDBusMessage message = QDBusMessage::createMethodCall("org.freedesktop.FileManager1", "/org/freedesktop/FileManager1", "org.freedesktop.FileManager1", "ShowFolders"); + QList args = {QStringList(QUrl::fromLocalFile(_scriptBasePath).toString()), QString()}; + message.setArguments(args); + con.call(message); +#endif +#ifdef Q_OS_WINDOWS + QProcess::startDetached("explorer.exe", {QDir::toNativeSeparators(_scriptBasePath)}); +#endif +} + +void BatchProcessing::runScript() +{ + _ui->log->clear(); + auto selectedItems = _ui->scriptsList->selectedItems(); + if(selectedItems.size()) + { + _engineThread = new Script::ScriptEngineThread(this); + connect(_engineThread, &Script::ScriptEngineThread::newMessage, _ui->log, &QTextEdit::append); + connect(_engineThread, &Script::ScriptEngineThread::finished, this, &BatchProcessing::scriptFinished); + QStringList paths; + for(int i=0; i<_ui->pathsList->count(); i++) + paths.append(_ui->pathsList->item(i)->text()); + + QFileInfo outDir(_ui->outputPath->text()); + if(outDir.exists() && outDir.isWritable()) + { + _engineThread->setParams(_scriptBasePath + selectedItems.first()->text(), scanDirectories(paths), _ui->outputPath->text()); + _engineThread->start(); + _ui->startButton->setEnabled(false); + _ui->stopButton->setEnabled(true); + } + else + { + QMessageBox::warning(this, tr("Invalid output directory"), tr("Output directory path doesn't exist or is not writable")); + } + } +} + +void BatchProcessing::stopScript() +{ + qDebug() << "Stop script"; + if(_engineThread) + _engineThread->interrupt(); +} + +void BatchProcessing::scriptFinished() +{ + _ui->startButton->setEnabled(true); + _ui->stopButton->setEnabled(false); + qDebug() << "script finished"; + delete _engineThread; + _engineThread = nullptr; +} diff --git a/batchprocessing.h b/batchprocessing.h new file mode 100644 index 0000000..6abe9d7 --- /dev/null +++ b/batchprocessing.h @@ -0,0 +1,36 @@ +#ifndef BATCHPROCESSING_H +#define BATCHPROCESSING_H + +#include +#include +#include "scriptengine.h" + +namespace Ui { class BatchProcessing; } + +class BatchProcessing : public QDialog +{ + Ui::BatchProcessing *_ui; + QString _scriptBasePath; + QFileSystemWatcher _fileWatcher; + Script::ScriptEngineThread *_engineThread = nullptr; +private slots: + void scanScriptDir(); +public: + explicit BatchProcessing(QWidget *parent = nullptr); + ~BatchProcessing(); +protected: + void closeEvent(QCloseEvent *event); +public slots: + void scriptDirChanged(); + void addFiles(); + void addDir(); + void removePath(); + void removeAllPaths(); + void browse(); + void openScriptDir(); + void runScript(); + void stopScript(); + void scriptFinished(); +}; + +#endif // BATCHPROCESSING_H diff --git a/batchprocessing.ui b/batchprocessing.ui new file mode 100644 index 0000000..b2357c3 --- /dev/null +++ b/batchprocessing.ui @@ -0,0 +1,226 @@ + + + BatchProcessing + + + + 0 + 0 + 1024 + 768 + + + + Batch Processing + + + + + + + + Input files and directories + + + + + + + + 0 + 1 + + + + QAbstractItemView::MultiSelection + + + + + + + + + Add files + + + + + + + Add directories + + + + + + + Remove + + + + + + + Remove all + + + + + + + + + + + + + Output directory + + + + + + + true + + + + + + + Browse + + + + + + + + + + + Scripts + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Open scripts + + + + + + + + + + 0 + 1 + + + + + + + + Log + + + + + + + + 0 + 4 + + + + + FreeMono + + + + true + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Start script + + + + + + + false + + + Stop script + + + + + + + Close + + + + + + + + + + + closeButton + released() + BatchProcessing + close() + + + 973 + 745 + + + 511 + 383 + + + + + diff --git a/database.cpp b/database.cpp index d41aea5..274ab75 100644 --- a/database.cpp +++ b/database.cpp @@ -10,12 +10,12 @@ Database::Database(QObject *parent) : QObject(parent) { } -bool Database::init() +bool Database::init(const QLatin1String &connectionName) { QString path = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation); QDir dir(path); - QSqlDatabase m_database = QSqlDatabase::addDatabase("QSQLITE"); + QSqlDatabase m_database = QSqlDatabase::addDatabase("QSQLITE", connectionName); if(!dir.mkpath(".")) return false; diff --git a/database.h b/database.h index 458a13a..9953be3 100644 --- a/database.h +++ b/database.h @@ -24,7 +24,7 @@ class Database : public QObject int m_progress; public: explicit Database(QObject *parent = 0); - bool init(); + bool init(const QLatin1String &connectionName = QLatin1String(QSqlDatabase::defaultConnection)); bool mark(const QString &filename); bool unmark(const QString &filename); bool mark(const QStringList &filenames); diff --git a/mainwindow.cpp b/mainwindow.cpp index f170929..4beb4cb 100644 --- a/mainwindow.cpp +++ b/mainwindow.cpp @@ -25,6 +25,7 @@ #include "statusbar.h" #include "settingsdialog.h" #include "histogram.h" +#include "batchprocessing.h" #ifdef __linux__ #include @@ -151,6 +152,11 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) fileMenu->addAction(tr("Index directory"), this, SLOT(indexDir())); fileMenu->addAction(tr("Reindex files"), this, SLOT(reindex())); fileMenu->addAction(tr("Export database to CSV"), this, &MainWindow::exportCSV); + fileMenu->addAction(tr("Batch processing"), [this](){ + BatchProcessing *batchProcessing = new BatchProcessing(this); + batchProcessing->exec(); + delete batchProcessing; + }, Qt::Key_B | Qt::CTRL); fileMenu->addSeparator(); QAction *liveModeAction = fileMenu->addAction(tr("Live mode"), this, SLOT(liveMode(bool))); liveModeAction->setCheckable(true); diff --git a/scriptengine.cpp b/scriptengine.cpp new file mode 100644 index 0000000..55e8e18 --- /dev/null +++ b/scriptengine.cpp @@ -0,0 +1,224 @@ +#include "scriptengine.h" +#include +#include +#include +#include "loadrunable.h" + +namespace Script +{ + +ScriptEngine::ScriptEngine(QObject *parent) : QObject(parent) + , _jsEngine(new QJSEngine(this)) + , _database(new Database(this)) +{ + QJSValue engine = _jsEngine->newQObject(this); + _jsEngine->globalObject().setProperty("engine", engine); + _database->init(QLatin1String("scriptengine")); +} + +void ScriptEngine::setParams(const QString &scriptPath, const QStringList &paths, const QString &outputDir) +{ + _scriptPath = scriptPath; + _paths = paths; + _outputDir = outputDir; +} + +void ScriptEngine::reportError(const QString &message) +{ + _jsEngine->throwError(message); +} + +const QString &ScriptEngine::outputDir() const +{ + return _outputDir; +} + +void ScriptEngine::interrupt() +{ + _jsEngine->setInterrupted(true); +} + +void ScriptEngine::log(const QString &message) +{ + emit newMessage(message); +} + +void ScriptEngine::mark(File *file) +{ + _database->mark(file->absoluteFilePath()); +} + +void ScriptEngine::unmark(File *file) +{ + _database->unmark(file->absoluteFilePath()); +} + +bool ScriptEngine::isMarked(const File *file) const +{ + return _database->isMarked(file->absoluteFilePath()); +} + +void ScriptEngine::run() +{ + QJSValue jsPaths = _jsEngine->newArray(_paths.size()); + for(qsizetype i=0; i<_paths.size(); i++) + jsPaths.setProperty(i, _jsEngine->newQObject(new File(_paths[i], this))); + + _jsEngine->globalObject().setProperty("files", jsPaths); + + QFile scriptFile(_scriptPath); + if(!scriptFile.open(QIODevice::ReadOnly)) + return; + + QTextStream stream(&scriptFile); + QString contents = stream.readAll(); + scriptFile.close(); + QJSValue result = _jsEngine->evaluate(contents, _scriptPath); + qDebug() << result.isError() << result.toString(); + if(result.isError()) + { + QString error = result.property("name").toString() + " on line " + result.property("lineNumber").toString() + " : " + result.toString(); + error += "\n" + result.property("stack").toString(); + emit newMessage(error); + } + + emit finished(); +} + +void File::loadFitsKeywords() +{ + if(!_fitsKeywordsLoaded) + { + _fitsKeywordsLoaded = true; + ImageInfoData info; + if(suffix() == "xisf") + { + readXISFHeader(_path, info); + } + else if(suffix() == "fits") + { + readFITSHeader(_path, info); + } + else return; + + for(const FITSRecord &record : info.fitsHeader) + { + _fitsKeywords[record.key] = record.value.toString(); + } + } +} + +File::File(const QString &path, Script::ScriptEngine *engine) : + _engine(engine), + _path(path), + _info(path) +{ +} + +QString File::fileName() const +{ + return _info.fileName(); +} + +QString File::absoluteFilePath() const +{ + return _info.absoluteFilePath(); +} + +QString File::absolutePath() const +{ + return _info.absolutePath(); +} + +QString File::baseName() const +{ + return _info.baseName(); +} + +QString File::completeBaseName() const +{ + return _info.completeBaseName(); +} + +QString File::suffix() const +{ + return _info.suffix(); +} + +qint64 File::size() const +{ + return _info.size(); +} + +QStringList File::fitsKeywords() +{ + QThread::msleep(500); + loadFitsKeywords(); + return _fitsKeywords.keys(); +} + +QString File::fitsValue(const QString &key) +{ + loadFitsKeywords(); + if(_fitsKeywords.contains(key)) + return _fitsKeywords[key]; + else + return QString(); +} + +bool File::isMarked() const +{ + return _engine->isMarked(this); +} + +bool File::copy(const QString &newpath) const +{ + return QFile::copy(_path, _engine->outputDir() + newpath); +} + +bool File::move(const QString &newpath) const +{ + return QFile::rename(_path, _engine->outputDir() + newpath); +} + +bool File::convertTo(const QString &format) +{ + _engine->reportError("Not implemented"); + return false; +} + +ScriptEngineThread::ScriptEngineThread(QObject *parent) : QObject(parent) +{ + _thread = new QThread(); + _thread->setObjectName("ScriptEngine"); + _engine = new ScriptEngine; + _engine->moveToThread(_thread); + connect(_engine, &ScriptEngine::finished, _thread, &QThread::quit); + connect(_engine, &ScriptEngine::newMessage, this, &ScriptEngineThread::newMessage); + connect(_thread, &QThread::started, _engine, &ScriptEngine::run); + connect(_thread, &QThread::finished, _engine, &ScriptEngine::deleteLater); + connect(_thread, &QThread::finished, _thread, &QThread::deleteLater); + connect(_thread, &QThread::finished, this, &ScriptEngineThread::finished); +} + +ScriptEngineThread::~ScriptEngineThread() +{ + if(_engine)_engine->interrupt(); +} + +void ScriptEngineThread::setParams(const QString &scriptPath, const QStringList &paths, const QString &outputDir) +{ + _engine->setParams(scriptPath, paths, outputDir); +} + +void ScriptEngineThread::start() +{ + _thread->start(); +} + +void ScriptEngineThread::interrupt() +{ + if(_engine)_engine->interrupt(); +} + +} diff --git a/scriptengine.h b/scriptengine.h new file mode 100644 index 0000000..8cdcd5b --- /dev/null +++ b/scriptengine.h @@ -0,0 +1,84 @@ +#ifndef SCRIPTENGINE_H +#define SCRIPTENGINE_H + +#include +#include +#include +#include +#include "database.h" + +namespace Script +{ + +class File; + +class ScriptEngine : public QObject +{ + Q_OBJECT + QJSEngine *_jsEngine; + Database *_database; + QString _scriptPath; + QString _outputDir; + QStringList _paths; +public: + explicit ScriptEngine(QObject *parent = nullptr); + void setParams(const QString &scriptPath, const QStringList &paths, const QString &outputDir); + void reportError(const QString &message); + const QString& outputDir() const; + void interrupt(); + Q_INVOKABLE void log(const QString &message); + Q_INVOKABLE void mark(File *file); + Q_INVOKABLE void unmark(File *file); + Q_INVOKABLE bool isMarked(const File *file) const; +public slots: + void run(); +signals: + void newMessage(const QString &message); + void finished(); +}; + +class ScriptEngineThread : public QObject +{ + Q_OBJECT + QThread *_thread; + ScriptEngine *_engine; +public: + ScriptEngineThread(QObject *parent = nullptr); + ~ScriptEngineThread(); + void setParams(const QString &scriptPath, const QStringList &paths, const QString &outputDir); + void start(); + void interrupt(); +signals: + void newMessage(const QString &message); + void finished(); +}; + +class File : public QObject +{ + Q_OBJECT + ScriptEngine *_engine; + QString _path; + QFileInfo _info; + bool _fitsKeywordsLoaded = false; + QMap _fitsKeywords; + void loadFitsKeywords(); +public: + explicit File(const QString &path, ScriptEngine *engine); + Q_INVOKABLE QString fileName() const; + Q_INVOKABLE QString absoluteFilePath() const; + Q_INVOKABLE QString absolutePath() const; + Q_INVOKABLE QString baseName() const; + Q_INVOKABLE QString completeBaseName() const; + Q_INVOKABLE QString suffix() const; + Q_INVOKABLE qint64 size() const; + Q_INVOKABLE QStringList fitsKeywords(); + Q_INVOKABLE QString fitsValue(const QString &key); + Q_INVOKABLE bool isMarked() const; + Q_INVOKABLE bool copy(const QString &newpath) const; + Q_INVOKABLE bool move(const QString &newpath) const; + Q_INVOKABLE bool convertTo(const QString &format); +}; + +} + +#endif // SCRIPTENGINE_H