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:
@@ -1,4 +1,5 @@
|
||||
#include "MainWindow.h"
|
||||
|
||||
#include "../converter/rawloader/RawLoader.h"
|
||||
#include "../converter/preprocess/Preprocessor.h"
|
||||
#include "../converter/negative/NegativeDetector.h"
|
||||
@@ -7,6 +8,7 @@
|
||||
#include "../converter/crop/CropProcessor.h"
|
||||
#include "../converter/output/OutputWriter.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QFileDialog>
|
||||
#include <QHBoxLayout>
|
||||
#include <QMessageBox>
|
||||
@@ -19,70 +21,163 @@
|
||||
|
||||
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)
|
||||
: QMainWindow{parent}
|
||||
{
|
||||
setup_ui();
|
||||
setup_pipeline();
|
||||
setWindowTitle("Photo Converter");
|
||||
resize(800, 600);
|
||||
setWindowTitle("Photo Converter – Analog Negative to Digital Positive");
|
||||
resize(900, 680);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// setup_ui
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
void MainWindow::setup_ui() {
|
||||
auto* central = new QWidget(this);
|
||||
auto* layout = new QVBoxLayout(central);
|
||||
auto* root_layout = new QVBoxLayout(central);
|
||||
|
||||
// ── Top toolbar ──────────────────────────────────────────────────────────
|
||||
auto* toolbar = new QHBoxLayout();
|
||||
|
||||
// Button bar
|
||||
auto* button_layout = new QHBoxLayout();
|
||||
open_button_ = new QPushButton("Open Files...", this);
|
||||
convert_button_ = new QPushButton("Convert", 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);
|
||||
|
||||
button_layout->addWidget(open_button_);
|
||||
button_layout->addWidget(output_dir_button_);
|
||||
button_layout->addWidget(convert_button_);
|
||||
layout->addLayout(button_layout);
|
||||
toolbar->addWidget(open_button_);
|
||||
toolbar->addWidget(output_dir_button_);
|
||||
toolbar->addWidget(convert_button_);
|
||||
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_->setAlignment(Qt::AlignCenter);
|
||||
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_->setRange(0, 100);
|
||||
progress_bar_->setValue(0);
|
||||
layout->addWidget(progress_bar_);
|
||||
root_layout->addWidget(progress_bar_);
|
||||
|
||||
// Status
|
||||
// ── Status line ──────────────────────────────────────────────────────────
|
||||
status_label_ = new QLabel("Ready", this);
|
||||
layout->addWidget(status_label_);
|
||||
root_layout->addWidget(status_label_);
|
||||
|
||||
setCentralWidget(central);
|
||||
|
||||
// Connections
|
||||
// ── Signal/slot connections ───────────────────────────────────────────────
|
||||
connect(open_button_, &QPushButton::clicked, this, &MainWindow::on_open_files);
|
||||
connect(convert_button_, &QPushButton::clicked, this, &MainWindow::on_convert);
|
||||
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>();
|
||||
// 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
|
||||
}
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Slots
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
void MainWindow::on_open_files() {
|
||||
QStringList files = QFileDialog::getOpenFileNames(
|
||||
this, "Open Image Files", QString(), kFileFilter);
|
||||
const QStringList files = QFileDialog::getOpenFileNames(
|
||||
this, "Open Image Files", QString{}, kFileFilter);
|
||||
|
||||
if (files.isEmpty()) return;
|
||||
|
||||
@@ -92,88 +187,204 @@ void MainWindow::on_open_files() {
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
output_dir_ = dir.toStdString();
|
||||
output_dir_button_->setText(
|
||||
QString::fromStdString(std::format("Output: {}", output_dir_)));
|
||||
|
||||
convert_button_->setEnabled(!input_files_.empty() && !output_dir_.empty());
|
||||
}
|
||||
|
||||
void MainWindow::on_convert() {
|
||||
if (input_files_.empty() || output_dir_.empty()) return;
|
||||
|
||||
// Disable buttons during conversion.
|
||||
convert_button_->setEnabled(false);
|
||||
RawLoader loader;
|
||||
open_button_->setEnabled(false);
|
||||
batch_button_->setEnabled(false);
|
||||
progress_bar_->setValue(0);
|
||||
|
||||
int processed = 0;
|
||||
for (const auto& file : input_files_) {
|
||||
status_label_->setText(
|
||||
QString::fromStdString(std::format("Processing: {}", file)));
|
||||
// Resolve output format from combo selection.
|
||||
const auto fmt = static_cast<OutputFormat>(
|
||||
format_combo_->currentData().toInt());
|
||||
|
||||
// Load
|
||||
auto load_result = loader.load(file);
|
||||
if (!load_result.has_value()) {
|
||||
QMessageBox::warning(this, "Load Error",
|
||||
QString::fromStdString(load_result.error().format()));
|
||||
continue;
|
||||
// Create worker + thread.
|
||||
worker_thread_ = new QThread(this);
|
||||
worker_ = new ConversionWorker(input_files_, output_dir_, fmt, /*quality=*/95);
|
||||
worker_->moveToThread(worker_thread_);
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
// Build pipeline with output stage
|
||||
auto run_pipeline = std::make_unique<Pipeline>();
|
||||
run_pipeline->add_stage(std::make_unique<Preprocessor>());
|
||||
run_pipeline->add_stage(std::make_unique<NegativeDetector>());
|
||||
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}));
|
||||
const AppConfig& app_cfg = cfg_result.value();
|
||||
|
||||
// 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();
|
||||
});
|
||||
// Discover files.
|
||||
const auto extensions = app_cfg.parsed_extensions();
|
||||
std::vector<std::string> discovered;
|
||||
|
||||
if (!result.has_value()) {
|
||||
QMessageBox::warning(this, "Processing Error",
|
||||
QString::fromStdString(result.error().format()));
|
||||
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());
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
if (app_cfg.batch.recursive) {
|
||||
for (const auto& e : std::filesystem::recursive_directory_iterator{app_cfg.batch.input_dir}) {
|
||||
scan_entry(e);
|
||||
}
|
||||
} else {
|
||||
++processed;
|
||||
update_preview(result.value().rgb);
|
||||
for (const auto& e : std::filesystem::directory_iterator{app_cfg.batch.input_dir}) {
|
||||
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);
|
||||
status_label_->setText(
|
||||
QString::fromStdString(std::format("Done: {}/{} files processed",
|
||||
processed, input_files_.size())));
|
||||
convert_button_->setEnabled(true);
|
||||
QString::fromStdString(
|
||||
std::format("Done: {}/{} file(s) converted successfully",
|
||||
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) {
|
||||
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;
|
||||
image.convertTo(display, CV_8UC3, 1.0 / 257.0);
|
||||
cv::cvtColor(display, display, cv::COLOR_BGR2RGB);
|
||||
|
||||
QImage qimg(display.data, display.cols, display.rows,
|
||||
static_cast<int>(display.step), QImage::Format_RGB888);
|
||||
// Wrap in QImage (no copy – display must stay alive until pixmap is created).
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,29 +2,87 @@
|
||||
|
||||
#include "../converter/pipeline/Pipeline.h"
|
||||
#include "../converter/pipeline/ImageData.h"
|
||||
#include "../converter/output/OutputWriter.h"
|
||||
#include "../config/AppConfig.h"
|
||||
|
||||
#include <QMainWindow>
|
||||
#include <QComboBox>
|
||||
#include <QGroupBox>
|
||||
#include <QLabel>
|
||||
#include <QMainWindow>
|
||||
#include <QProgressBar>
|
||||
#include <QPushButton>
|
||||
#include <QThread>
|
||||
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
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.
|
||||
*
|
||||
* Provides:
|
||||
* - File selection dialog (RAW + standard image filters)
|
||||
* - Image preview (before/after)
|
||||
* - Pipeline execution with progress bar
|
||||
* - Output format selection
|
||||
* - Batch processing support
|
||||
* - Pipeline execution with progress bar on a background thread
|
||||
* - Output format selection (PNG 16-bit, PNG 8-bit, TIFF 16-bit, JPEG)
|
||||
* - 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
|
||||
* and only handles user interaction and display.
|
||||
* The GUI is thin: it delegates all processing to the Pipeline and only
|
||||
* handles user interaction and display.
|
||||
*/
|
||||
class MainWindow : public QMainWindow {
|
||||
Q_OBJECT
|
||||
@@ -37,32 +95,55 @@ private slots:
|
||||
void on_open_files();
|
||||
void on_convert();
|
||||
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:
|
||||
void setup_ui();
|
||||
void setup_pipeline();
|
||||
void update_preview(const cv::Mat& image);
|
||||
|
||||
// UI elements
|
||||
// ── UI elements ──────────────────────────────────────────────────────────
|
||||
QLabel* preview_label_{nullptr};
|
||||
QProgressBar* progress_bar_{nullptr};
|
||||
QPushButton* open_button_{nullptr};
|
||||
QPushButton* convert_button_{nullptr};
|
||||
QPushButton* output_dir_button_{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::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 =
|
||||
"All Supported (*.cr2 *.cr3 *.nef *.arw *.dng *.orf *.rw2 *.raf *.pef "
|
||||
"*.jpg *.jpeg *.png *.tif *.tiff);;"
|
||||
"RAW (*.cr2 *.cr3 *.nef *.arw *.dng *.orf *.rw2 *.raf *.pef);;"
|
||||
"Images (*.jpg *.jpeg *.png *.tif *.tiff);;"
|
||||
"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
|
||||
|
||||
46
src/main.cpp
46
src/main.cpp
@@ -1,35 +1,49 @@
|
||||
#include "cli/CliRunner.h"
|
||||
#include "gui/MainWindow.h"
|
||||
|
||||
#include <QApplication>
|
||||
|
||||
#include <algorithm>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
|
||||
#ifndef NO_GUI
|
||||
#include "gui/MainWindow.h"
|
||||
#include <QApplication>
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @brief Application entry point.
|
||||
*
|
||||
* Supports two modes:
|
||||
* - GUI mode (default): launches the Qt MainWindow
|
||||
* - CLI mode (--cli flag): batch processes files without GUI
|
||||
* Supports two operating modes:
|
||||
*
|
||||
* **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[]) {
|
||||
// Check if CLI mode is requested
|
||||
// Determine whether CLI/batch mode was requested.
|
||||
bool cli_mode = false;
|
||||
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;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (cli_mode) {
|
||||
// CLI batch mode (no Qt dependency)
|
||||
// ── CLI / Batch mode (no Qt dependency) ─────────────────────────────
|
||||
auto config_result = photoconv::CliRunner::parse_args(argc, argv);
|
||||
if (!config_result.has_value()) {
|
||||
// "Help requested" is not an error – exit 0.
|
||||
const bool is_help = config_result.error().message == "Help requested";
|
||||
if (!is_help) {
|
||||
std::cerr << config_result.error().format() << std::endl;
|
||||
return 1;
|
||||
}
|
||||
return is_help ? 0 : 1;
|
||||
}
|
||||
|
||||
photoconv::CliRunner runner;
|
||||
@@ -39,16 +53,24 @@ int main(int argc, char* argv[]) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Exit code 0 if at least one file was converted, 1 otherwise.
|
||||
return result.value() > 0 ? 0 : 1;
|
||||
}
|
||||
|
||||
// GUI mode
|
||||
QApplication app(argc, argv);
|
||||
#ifndef NO_GUI
|
||||
// ── GUI mode ─────────────────────────────────────────────────────────────
|
||||
QApplication app{argc, argv};
|
||||
app.setApplicationName("Photo Converter");
|
||||
app.setApplicationVersion("0.1.0");
|
||||
app.setOrganizationName("photo-converter");
|
||||
|
||||
photoconv::MainWindow window;
|
||||
window.show();
|
||||
|
||||
return app.exec();
|
||||
#else
|
||||
std::cerr << "This build was compiled without GUI support.\n"
|
||||
"Use --cli or --batch mode.\n";
|
||||
return 1;
|
||||
#endif
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user