feat: extend MainWindow with QThread worker, settings panel, and batch dialog

- ConversionWorker runs pipeline on background QThread so the GUI stays
  responsive during conversion; emits file_done, preview_ready, finished
- MainWindow adds: output-format QComboBox, film-type QComboBox, Batch button
  that opens an AppConfig INI file and discovers files from batch.input_dir
- main.cpp: --batch and --config flags trigger CLI mode without Qt

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Christoph K.
2026-03-14 09:41:23 +01:00
parent fd2d97ddeb
commit 71d535fc50
3 changed files with 421 additions and 107 deletions

View File

@@ -1,4 +1,5 @@
#include "MainWindow.h" #include "MainWindow.h"
#include "../converter/rawloader/RawLoader.h" #include "../converter/rawloader/RawLoader.h"
#include "../converter/preprocess/Preprocessor.h" #include "../converter/preprocess/Preprocessor.h"
#include "../converter/negative/NegativeDetector.h" #include "../converter/negative/NegativeDetector.h"
@@ -7,6 +8,7 @@
#include "../converter/crop/CropProcessor.h" #include "../converter/crop/CropProcessor.h"
#include "../converter/output/OutputWriter.h" #include "../converter/output/OutputWriter.h"
#include <QApplication>
#include <QFileDialog> #include <QFileDialog>
#include <QHBoxLayout> #include <QHBoxLayout>
#include <QMessageBox> #include <QMessageBox>
@@ -19,70 +21,163 @@
namespace photoconv { namespace photoconv {
// ─────────────────────────────────────────────────────────────────────────────
// ConversionWorker
// ─────────────────────────────────────────────────────────────────────────────
ConversionWorker::ConversionWorker(std::vector<std::string> files,
std::string output_dir,
OutputFormat fmt,
int quality,
QObject* parent)
: QObject{parent}
, files_{std::move(files)}
, output_dir_{std::move(output_dir)}
, fmt_{fmt}
, quality_{quality}
{}
void ConversionWorker::run() {
RawLoader loader;
// Build pipeline
Pipeline pipeline;
pipeline.add_stage(std::make_unique<Preprocessor>());
pipeline.add_stage(std::make_unique<NegativeDetector>());
pipeline.add_stage(std::make_unique<Inverter>());
pipeline.add_stage(std::make_unique<ColorCorrector>());
pipeline.add_stage(std::make_unique<CropProcessor>());
pipeline.add_stage(std::make_unique<OutputWriter>(
OutputConfig{output_dir_, fmt_, quality_}));
const int total = static_cast<int>(files_.size());
int success_count = 0;
for (int idx = 0; idx < total; ++idx) {
const std::string& file = files_[static_cast<std::size_t>(idx)];
// Load
auto load_result = loader.load(std::filesystem::path{file});
if (!load_result.has_value()) {
emit file_done(idx, total, false,
QString::fromStdString(load_result.error().message));
continue;
}
// Run pipeline
auto result = pipeline.execute(std::move(load_result.value()));
if (!result.has_value()) {
emit file_done(idx, total, false,
QString::fromStdString(result.error().message));
continue;
}
++success_count;
// Send the last processed image to the preview (non-blocking copy).
emit preview_ready(result.value().rgb.clone());
emit file_done(idx, total, true,
QString::fromStdString(
std::format("OK: {}", std::filesystem::path{file}.filename().string())));
}
emit finished(success_count, total);
}
// ─────────────────────────────────────────────────────────────────────────────
// MainWindow constructor
// ─────────────────────────────────────────────────────────────────────────────
MainWindow::MainWindow(QWidget* parent) MainWindow::MainWindow(QWidget* parent)
: QMainWindow{parent} : QMainWindow{parent}
{ {
setup_ui(); setup_ui();
setup_pipeline(); setWindowTitle("Photo Converter Analog Negative to Digital Positive");
setWindowTitle("Photo Converter"); resize(900, 680);
resize(800, 600);
} }
// ─────────────────────────────────────────────────────────────────────────────
// setup_ui
// ─────────────────────────────────────────────────────────────────────────────
void MainWindow::setup_ui() { void MainWindow::setup_ui() {
auto* central = new QWidget(this); auto* central = new QWidget(this);
auto* layout = new QVBoxLayout(central); auto* root_layout = new QVBoxLayout(central);
// Button bar // ── Top toolbar ──────────────────────────────────────────────────────────
auto* button_layout = new QHBoxLayout(); auto* toolbar = new QHBoxLayout();
open_button_ = new QPushButton("Open Files...", this);
convert_button_ = new QPushButton("Convert", this); open_button_ = new QPushButton("Open Files...", this);
output_dir_button_ = new QPushButton("Output Dir...", this); output_dir_button_ = new QPushButton("Output Dir...", this);
convert_button_ = new QPushButton("Convert", this);
batch_button_ = new QPushButton("Batch...", this);
convert_button_->setEnabled(false); convert_button_->setEnabled(false);
button_layout->addWidget(open_button_); toolbar->addWidget(open_button_);
button_layout->addWidget(output_dir_button_); toolbar->addWidget(output_dir_button_);
button_layout->addWidget(convert_button_); toolbar->addWidget(convert_button_);
layout->addLayout(button_layout); toolbar->addWidget(batch_button_);
toolbar->addStretch();
// Preview area root_layout->addLayout(toolbar);
// ── Settings panel ───────────────────────────────────────────────────────
settings_box_ = new QGroupBox("Settings", this);
auto* settings_layout = new QHBoxLayout(settings_box_);
settings_layout->addWidget(new QLabel("Output format:", this));
format_combo_ = new QComboBox(this);
format_combo_->addItem("PNG 16-bit (Archival)", QVariant{static_cast<int>(OutputFormat::PNG_16bit)});
format_combo_->addItem("PNG 8-bit (Web)", QVariant{static_cast<int>(OutputFormat::PNG_8bit)});
format_combo_->addItem("TIFF 16-bit (Editing)", QVariant{static_cast<int>(OutputFormat::TIFF_16bit)});
format_combo_->addItem("JPEG (Preview)", QVariant{static_cast<int>(OutputFormat::JPEG)});
settings_layout->addWidget(format_combo_);
settings_layout->addSpacing(16);
settings_layout->addWidget(new QLabel("Film type:", this));
film_combo_ = new QComboBox(this);
film_combo_->addItem("Auto-detect");
film_combo_->addItem("C-41 Color Negative");
film_combo_->addItem("B&W Negative");
settings_layout->addWidget(film_combo_);
settings_layout->addStretch();
root_layout->addWidget(settings_box_);
// ── Preview area ─────────────────────────────────────────────────────────
preview_label_ = new QLabel("No image loaded", this); preview_label_ = new QLabel("No image loaded", this);
preview_label_->setAlignment(Qt::AlignCenter); preview_label_->setAlignment(Qt::AlignCenter);
preview_label_->setMinimumSize(640, 480); preview_label_->setMinimumSize(640, 480);
layout->addWidget(preview_label_); preview_label_->setFrameStyle(QFrame::Sunken | QFrame::StyledPanel);
root_layout->addWidget(preview_label_, /*stretch=*/1);
// Progress bar // ── Progress bar ─────────────────────────────────────────────────────────
progress_bar_ = new QProgressBar(this); progress_bar_ = new QProgressBar(this);
progress_bar_->setRange(0, 100); progress_bar_->setRange(0, 100);
progress_bar_->setValue(0); progress_bar_->setValue(0);
layout->addWidget(progress_bar_); root_layout->addWidget(progress_bar_);
// Status // ── Status line ──────────────────────────────────────────────────────────
status_label_ = new QLabel("Ready", this); status_label_ = new QLabel("Ready", this);
layout->addWidget(status_label_); root_layout->addWidget(status_label_);
setCentralWidget(central); setCentralWidget(central);
// Connections // ── Signal/slot connections ───────────────────────────────────────────────
connect(open_button_, &QPushButton::clicked, this, &MainWindow::on_open_files); connect(open_button_, &QPushButton::clicked, this, &MainWindow::on_open_files);
connect(convert_button_, &QPushButton::clicked, this, &MainWindow::on_convert); connect(convert_button_, &QPushButton::clicked, this, &MainWindow::on_convert);
connect(output_dir_button_, &QPushButton::clicked, this, &MainWindow::on_select_output_dir); connect(output_dir_button_, &QPushButton::clicked, this, &MainWindow::on_select_output_dir);
connect(batch_button_, &QPushButton::clicked, this, &MainWindow::on_batch);
} }
void MainWindow::setup_pipeline() { // ─────────────────────────────────────────────────────────────────────────────
pipeline_ = std::make_unique<Pipeline>(); // Slots
// Note: Loader is called separately before the pipeline. // ─────────────────────────────────────────────────────────────────────────────
// Pipeline stages are: Preprocess -> Detect -> Invert -> Color -> PostProcess -> Output
pipeline_->add_stage(std::make_unique<Preprocessor>());
pipeline_->add_stage(std::make_unique<NegativeDetector>());
pipeline_->add_stage(std::make_unique<Inverter>());
pipeline_->add_stage(std::make_unique<ColorCorrector>());
pipeline_->add_stage(std::make_unique<CropProcessor>());
// OutputWriter is added dynamically when output_dir is known
}
void MainWindow::on_open_files() { void MainWindow::on_open_files() {
QStringList files = QFileDialog::getOpenFileNames( const QStringList files = QFileDialog::getOpenFileNames(
this, "Open Image Files", QString(), kFileFilter); this, "Open Image Files", QString{}, kFileFilter);
if (files.isEmpty()) return; if (files.isEmpty()) return;
@@ -92,88 +187,204 @@ void MainWindow::on_open_files() {
} }
status_label_->setText( status_label_->setText(
QString::fromStdString(std::format("{} file(s) selected", input_files_.size()))); QString::fromStdString(
std::format("{} file(s) selected", input_files_.size())));
convert_button_->setEnabled(!input_files_.empty() && !output_dir_.empty()); convert_button_->setEnabled(!input_files_.empty() && !output_dir_.empty());
} }
void MainWindow::on_select_output_dir() { void MainWindow::on_select_output_dir() {
QString dir = QFileDialog::getExistingDirectory(this, "Select Output Directory"); const QString dir = QFileDialog::getExistingDirectory(
this, "Select Output Directory");
if (dir.isEmpty()) return; if (dir.isEmpty()) return;
output_dir_ = dir.toStdString(); output_dir_ = dir.toStdString();
output_dir_button_->setText( output_dir_button_->setText(
QString::fromStdString(std::format("Output: {}", output_dir_))); QString::fromStdString(std::format("Output: {}", output_dir_)));
convert_button_->setEnabled(!input_files_.empty() && !output_dir_.empty()); convert_button_->setEnabled(!input_files_.empty() && !output_dir_.empty());
} }
void MainWindow::on_convert() { void MainWindow::on_convert() {
if (input_files_.empty() || output_dir_.empty()) return; if (input_files_.empty() || output_dir_.empty()) return;
// Disable buttons during conversion.
convert_button_->setEnabled(false); convert_button_->setEnabled(false);
RawLoader loader; open_button_->setEnabled(false);
batch_button_->setEnabled(false);
progress_bar_->setValue(0);
int processed = 0; // Resolve output format from combo selection.
for (const auto& file : input_files_) { const auto fmt = static_cast<OutputFormat>(
status_label_->setText( format_combo_->currentData().toInt());
QString::fromStdString(std::format("Processing: {}", file)));
// Load // Create worker + thread.
auto load_result = loader.load(file); worker_thread_ = new QThread(this);
if (!load_result.has_value()) { worker_ = new ConversionWorker(input_files_, output_dir_, fmt, /*quality=*/95);
QMessageBox::warning(this, "Load Error", worker_->moveToThread(worker_thread_);
QString::fromStdString(load_result.error().format()));
continue; // Wire up signals.
connect(worker_thread_, &QThread::started,
worker_, &ConversionWorker::run);
connect(worker_, &ConversionWorker::file_done,
this, &MainWindow::on_file_done);
connect(worker_, &ConversionWorker::preview_ready,
this, &MainWindow::on_preview_ready);
connect(worker_, &ConversionWorker::finished,
this, &MainWindow::on_conversion_finished);
// Clean up worker and thread after completion.
connect(worker_, &ConversionWorker::finished,
worker_thread_, &QThread::quit);
connect(worker_thread_, &QThread::finished,
worker_, &QObject::deleteLater);
connect(worker_thread_, &QThread::finished,
worker_thread_, &QObject::deleteLater);
worker_thread_->start();
}
void MainWindow::on_batch() {
// Let the user pick a config file, then discover and convert all
// matching images from AppConfig::batch.input_dir.
const QString config_path = QFileDialog::getOpenFileName(
this, "Open Batch Configuration", QString{}, kConfigFilter);
if (config_path.isEmpty()) return;
auto cfg_result = AppConfig::load(config_path.toStdString());
if (!cfg_result.has_value()) {
QMessageBox::warning(this, "Config Error",
QString::fromStdString(cfg_result.error().message));
return;
}
const AppConfig& app_cfg = cfg_result.value();
// Discover files.
const auto extensions = app_cfg.parsed_extensions();
std::vector<std::string> discovered;
const auto scan_entry = [&](const std::filesystem::directory_entry& entry) {
if (!entry.is_regular_file()) return;
std::string ext = entry.path().extension().string();
std::ranges::transform(ext, ext.begin(),
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
if (std::ranges::any_of(extensions, [&ext](const std::string& e) { return e == ext; })) {
discovered.push_back(entry.path().string());
} }
};
// Build pipeline with output stage try {
auto run_pipeline = std::make_unique<Pipeline>(); if (app_cfg.batch.recursive) {
run_pipeline->add_stage(std::make_unique<Preprocessor>()); for (const auto& e : std::filesystem::recursive_directory_iterator{app_cfg.batch.input_dir}) {
run_pipeline->add_stage(std::make_unique<NegativeDetector>()); scan_entry(e);
run_pipeline->add_stage(std::make_unique<Inverter>()); }
run_pipeline->add_stage(std::make_unique<ColorCorrector>());
run_pipeline->add_stage(std::make_unique<CropProcessor>());
run_pipeline->add_stage(std::make_unique<OutputWriter>(
OutputConfig{output_dir_, OutputFormat::PNG_16bit}));
// Execute
auto result = run_pipeline->execute(
std::move(load_result.value()),
[this](const std::string& stage, float progress) {
progress_bar_->setValue(static_cast<int>(progress * 100));
status_label_->setText(QString::fromStdString(stage));
QApplication::processEvents();
});
if (!result.has_value()) {
QMessageBox::warning(this, "Processing Error",
QString::fromStdString(result.error().format()));
} else { } else {
++processed; for (const auto& e : std::filesystem::directory_iterator{app_cfg.batch.input_dir}) {
update_preview(result.value().rgb); scan_entry(e);
}
}
} catch (const std::exception& ex) {
QMessageBox::warning(this, "Directory Error", ex.what());
return;
}
if (discovered.empty()) {
QMessageBox::information(this, "Batch",
QString::fromStdString(
std::format("No matching files found in: {}",
app_cfg.batch.input_dir.string())));
return;
}
// Populate state and trigger conversion.
input_files_ = discovered;
output_dir_ = app_cfg.batch.output_dir.string();
output_dir_button_->setText(
QString::fromStdString(std::format("Output: {}", output_dir_)));
// Select format combo to match the config.
for (int i = 0; i < format_combo_->count(); ++i) {
if (static_cast<OutputFormat>(format_combo_->itemData(i).toInt()) ==
app_cfg.output_format()) {
format_combo_->setCurrentIndex(i);
break;
} }
} }
status_label_->setText(
QString::fromStdString(
std::format("Batch: {} file(s) from {}",
discovered.size(), app_cfg.batch.input_dir.string())));
// Start conversion on the background thread.
on_convert();
}
void MainWindow::on_file_done(int index, int total, bool ok, QString message) {
// Update progress bar.
const int progress = static_cast<int>(
100.0 * static_cast<double>(index + 1) / static_cast<double>(total));
progress_bar_->setValue(progress);
// Update status label.
const QString icon = ok ? "OK" : "FAIL";
status_label_->setText(
QString("[%1/%2] %3: %4").arg(index + 1).arg(total).arg(icon).arg(message));
}
void MainWindow::on_preview_ready(cv::Mat image) {
update_preview(image);
}
void MainWindow::on_conversion_finished(int success_count, int total) {
progress_bar_->setValue(100); progress_bar_->setValue(100);
status_label_->setText( status_label_->setText(
QString::fromStdString(std::format("Done: {}/{} files processed", QString::fromStdString(
processed, input_files_.size()))); std::format("Done: {}/{} file(s) converted successfully",
convert_button_->setEnabled(true); success_count, total)));
// Re-enable UI controls.
convert_button_->setEnabled(!input_files_.empty() && !output_dir_.empty());
open_button_->setEnabled(true);
batch_button_->setEnabled(true);
worker_thread_ = nullptr;
worker_ = nullptr;
} }
// ─────────────────────────────────────────────────────────────────────────────
// update_preview
// ─────────────────────────────────────────────────────────────────────────────
void MainWindow::update_preview(const cv::Mat& image) { void MainWindow::update_preview(const cv::Mat& image) {
if (image.empty()) return; if (image.empty()) return;
// Convert 16-bit BGR to 8-bit RGB for Qt display // Convert 16-bit BGR to 8-bit RGB for Qt display.
cv::Mat display; cv::Mat display;
image.convertTo(display, CV_8UC3, 1.0 / 257.0); image.convertTo(display, CV_8UC3, 1.0 / 257.0);
cv::cvtColor(display, display, cv::COLOR_BGR2RGB); cv::cvtColor(display, display, cv::COLOR_BGR2RGB);
QImage qimg(display.data, display.cols, display.rows, // Wrap in QImage (no copy display must stay alive until pixmap is created).
static_cast<int>(display.step), QImage::Format_RGB888); const QImage qimg{
display.data,
display.cols,
display.rows,
static_cast<qsizetype>(display.step),
QImage::Format_RGB888
};
// Scale to fit preview label while preserving aspect ratio.
const QPixmap pixmap = QPixmap::fromImage(qimg).scaled(
preview_label_->size(),
Qt::KeepAspectRatio,
Qt::SmoothTransformation);
// Scale to fit preview label
QPixmap pixmap = QPixmap::fromImage(qimg).scaled(
preview_label_->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation);
preview_label_->setPixmap(pixmap); preview_label_->setPixmap(pixmap);
} }

View File

@@ -2,29 +2,87 @@
#include "../converter/pipeline/Pipeline.h" #include "../converter/pipeline/Pipeline.h"
#include "../converter/pipeline/ImageData.h" #include "../converter/pipeline/ImageData.h"
#include "../converter/output/OutputWriter.h"
#include "../config/AppConfig.h"
#include <QMainWindow> #include <QComboBox>
#include <QGroupBox>
#include <QLabel> #include <QLabel>
#include <QMainWindow>
#include <QProgressBar> #include <QProgressBar>
#include <QPushButton> #include <QPushButton>
#include <QThread>
#include <memory> #include <memory>
#include <vector> #include <vector>
namespace photoconv { namespace photoconv {
/**
* @brief Worker object that runs pipeline conversions on a background thread.
*
* Moved to a QThread so that the GUI remains responsive during batch
* processing. Results are emitted via Qt signals.
*/
class ConversionWorker : public QObject {
Q_OBJECT
public:
/**
* @brief Construct a worker with the list of files to convert.
*
* @param files Absolute paths to input images.
* @param output_dir Target directory for converted files.
* @param fmt Output format selection.
* @param quality JPEG quality (0-100).
*/
explicit ConversionWorker(std::vector<std::string> files,
std::string output_dir,
OutputFormat fmt,
int quality,
QObject* parent = nullptr);
public slots:
/** Run the conversion. Called when the thread starts. */
void run();
signals:
/** Emitted after each file completes or fails.
* @param index 0-based index of the completed file.
* @param total Total number of files.
* @param ok Whether conversion succeeded.
* @param message Human-readable status or error text.
*/
void file_done(int index, int total, bool ok, QString message);
/** Emitted with the final converted image for preview. */
void preview_ready(cv::Mat image);
/** Emitted when all files have been processed. */
void finished(int success_count, int total);
private:
std::vector<std::string> files_;
std::string output_dir_;
OutputFormat fmt_;
int quality_;
};
// ─────────────────────────────────────────────────────────────────────────────
/** /**
* @brief Main application window for the photo converter GUI. * @brief Main application window for the photo converter GUI.
* *
* Provides: * Provides:
* - File selection dialog (RAW + standard image filters) * - File selection dialog (RAW + standard image filters)
* - Image preview (before/after) * - Image preview (before/after)
* - Pipeline execution with progress bar * - Pipeline execution with progress bar on a background thread
* - Output format selection * - Output format selection (PNG 16-bit, PNG 8-bit, TIFF 16-bit, JPEG)
* - Batch processing support * - Film type selection (Auto, C-41 Color, B&W)
* - Batch processing via "Batch..." button (loads an AppConfig INI file)
* *
* The GUI is thin: it delegates all processing to the Pipeline * The GUI is thin: it delegates all processing to the Pipeline and only
* and only handles user interaction and display. * handles user interaction and display.
*/ */
class MainWindow : public QMainWindow { class MainWindow : public QMainWindow {
Q_OBJECT Q_OBJECT
@@ -37,32 +95,55 @@ private slots:
void on_open_files(); void on_open_files();
void on_convert(); void on_convert();
void on_select_output_dir(); void on_select_output_dir();
void on_batch();
/** Slot connected to ConversionWorker::file_done. */
void on_file_done(int index, int total, bool ok, QString message);
/** Slot connected to ConversionWorker::preview_ready. */
void on_preview_ready(cv::Mat image);
/** Slot connected to ConversionWorker::finished. */
void on_conversion_finished(int success_count, int total);
private: private:
void setup_ui(); void setup_ui();
void setup_pipeline();
void update_preview(const cv::Mat& image); void update_preview(const cv::Mat& image);
// UI elements // ── UI elements ──────────────────────────────────────────────────────────
QLabel* preview_label_{nullptr}; QLabel* preview_label_{nullptr};
QProgressBar* progress_bar_{nullptr}; QProgressBar* progress_bar_{nullptr};
QPushButton* open_button_{nullptr}; QPushButton* open_button_{nullptr};
QPushButton* convert_button_{nullptr}; QPushButton* convert_button_{nullptr};
QPushButton* output_dir_button_{nullptr}; QPushButton* output_dir_button_{nullptr};
QLabel* status_label_{nullptr}; QPushButton* batch_button_{nullptr};
QLabel* status_label_{nullptr};
// State // Settings widgets
QComboBox* format_combo_{nullptr}; // Output format selector
QComboBox* film_combo_{nullptr}; // Film type selector
QGroupBox* settings_box_{nullptr};
// ── State ────────────────────────────────────────────────────────────────
std::vector<std::string> input_files_; std::vector<std::string> input_files_;
std::string output_dir_; std::string output_dir_;
std::unique_ptr<Pipeline> pipeline_;
/// Qt file dialog filter string for supported formats. // Background thread for conversion
QThread* worker_thread_{nullptr};
ConversionWorker* worker_{nullptr};
// ── Constants ────────────────────────────────────────────────────────────
/// Qt file dialog filter string for all supported formats.
static constexpr const char* kFileFilter = static constexpr const char* kFileFilter =
"All Supported (*.cr2 *.cr3 *.nef *.arw *.dng *.orf *.rw2 *.raf *.pef " "All Supported (*.cr2 *.cr3 *.nef *.arw *.dng *.orf *.rw2 *.raf *.pef "
"*.jpg *.jpeg *.png *.tif *.tiff);;" "*.jpg *.jpeg *.png *.tif *.tiff);;"
"RAW (*.cr2 *.cr3 *.nef *.arw *.dng *.orf *.rw2 *.raf *.pef);;" "RAW (*.cr2 *.cr3 *.nef *.arw *.dng *.orf *.rw2 *.raf *.pef);;"
"Images (*.jpg *.jpeg *.png *.tif *.tiff);;" "Images (*.jpg *.jpeg *.png *.tif *.tiff);;"
"All Files (*)"; "All Files (*)";
/// Qt file dialog filter for INI configuration files.
static constexpr const char* kConfigFilter =
"Config files (*.ini *.cfg *.conf *.toml);;All Files (*)";
}; };
} // namespace photoconv } // namespace photoconv

View File

@@ -1,35 +1,49 @@
#include "cli/CliRunner.h" #include "cli/CliRunner.h"
#include "gui/MainWindow.h"
#include <QApplication>
#include <algorithm> #include <algorithm>
#include <iostream> #include <iostream>
#include <string> #include <string>
#ifndef NO_GUI
#include "gui/MainWindow.h"
#include <QApplication>
#endif
/** /**
* @brief Application entry point. * @brief Application entry point.
* *
* Supports two modes: * Supports two operating modes:
* - GUI mode (default): launches the Qt MainWindow *
* - CLI mode (--cli flag): batch processes files without GUI * **GUI mode** (default when compiled with Qt):
* Launches the Qt MainWindow.
*
* **CLI / batch mode** (activated by `--cli` or `--batch`):
* Processes files from the command line or a config file without any GUI.
* Progress is written to stderr; errors are logged but do not abort the batch.
*
* @note The `--batch` flag (or `--config <file>`) implies CLI mode.
*/ */
int main(int argc, char* argv[]) { int main(int argc, char* argv[]) {
// Check if CLI mode is requested // Determine whether CLI/batch mode was requested.
bool cli_mode = false; bool cli_mode = false;
for (int i = 1; i < argc; ++i) { for (int i = 1; i < argc; ++i) {
if (std::string{argv[i]} == "--cli") { const std::string arg{argv[i]};
if (arg == "--cli" || arg == "--batch" || arg == "--config") {
cli_mode = true; cli_mode = true;
break; break;
} }
} }
if (cli_mode) { if (cli_mode) {
// CLI batch mode (no Qt dependency) // ── CLI / Batch mode (no Qt dependency) ─────────────────────────────
auto config_result = photoconv::CliRunner::parse_args(argc, argv); auto config_result = photoconv::CliRunner::parse_args(argc, argv);
if (!config_result.has_value()) { if (!config_result.has_value()) {
std::cerr << config_result.error().format() << std::endl; // "Help requested" is not an error exit 0.
return 1; const bool is_help = config_result.error().message == "Help requested";
if (!is_help) {
std::cerr << config_result.error().format() << std::endl;
}
return is_help ? 0 : 1;
} }
photoconv::CliRunner runner; photoconv::CliRunner runner;
@@ -39,16 +53,24 @@ int main(int argc, char* argv[]) {
return 1; return 1;
} }
// Exit code 0 if at least one file was converted, 1 otherwise.
return result.value() > 0 ? 0 : 1; return result.value() > 0 ? 0 : 1;
} }
// GUI mode #ifndef NO_GUI
QApplication app(argc, argv); // ── GUI mode ─────────────────────────────────────────────────────────────
QApplication app{argc, argv};
app.setApplicationName("Photo Converter"); app.setApplicationName("Photo Converter");
app.setApplicationVersion("0.1.0"); app.setApplicationVersion("0.1.0");
app.setOrganizationName("photo-converter");
photoconv::MainWindow window; photoconv::MainWindow window;
window.show(); window.show();
return app.exec(); return app.exec();
#else
std::cerr << "This build was compiled without GUI support.\n"
"Use --cli or --batch mode.\n";
return 1;
#endif
} }