From ba949ddb83ba6ea2314910c860bb8cdeb1109f2e Mon Sep 17 00:00:00 2001
From: Aleksandr Motsjonov
Date: Sun, 22 Mar 2026 12:50:41 +1100
Subject: [PATCH 1/9] GUI v1
Signed-off-by: Aleksandr Motsjonov
---
.github/workflows/gui.yml | 69 +
CMakeLists.txt | 12 +-
README.md | 17 +-
src/bindings/CMakeLists.txt | 2 +-
src/rawtoaces/CMakeLists.txt | 2 +-
src/rawtoaces_core/CMakeLists.txt | 2 +-
src/rawtoaces_gui/CMakeLists.txt | 35 +
src/rawtoaces_gui/conversion_thread.cpp | 43 +
src/rawtoaces_gui/conversion_thread.h | 38 +
src/rawtoaces_gui/main.cpp | 29 +
src/rawtoaces_gui/main_window.cpp | 1999 +++++++++++++++++++++++
src/rawtoaces_gui/main_window.h | 131 ++
src/rawtoaces_util/CMakeLists.txt | 2 +-
tests/CMakeLists.txt | 2 +-
14 files changed, 2374 insertions(+), 9 deletions(-)
create mode 100644 .github/workflows/gui.yml
create mode 100644 src/rawtoaces_gui/CMakeLists.txt
create mode 100644 src/rawtoaces_gui/conversion_thread.cpp
create mode 100644 src/rawtoaces_gui/conversion_thread.h
create mode 100644 src/rawtoaces_gui/main.cpp
create mode 100644 src/rawtoaces_gui/main_window.cpp
create mode 100644 src/rawtoaces_gui/main_window.h
diff --git a/.github/workflows/gui.yml b/.github/workflows/gui.yml
new file mode 100644
index 00000000..82e8e7e2
--- /dev/null
+++ b/.github/workflows/gui.yml
@@ -0,0 +1,69 @@
+name: GUI
+
+permissions:
+ contents: read
+
+on:
+ push:
+ pull_request:
+
+jobs:
+ gui-ubuntu:
+ name: "GUI Ubuntu"
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
+
+ - name: Dependencies
+ run: |
+ sudo apt-get update
+ sudo apt-get -q install -y \
+ libceres-dev \
+ nlohmann-json3-dev \
+ libopencv-dev \
+ openimageio-tools libopenimageio-dev \
+ exiftool \
+ liblensfun-dev \
+ liblensfun-data-v1 \
+ qt6-base-dev \
+ qt6-base-dev-tools
+
+ - name: Configure
+ run: >
+ cmake -S . -B build
+ -D CMAKE_BUILD_TYPE=Release
+ -D RTA_BUILD_GUI=ON
+ -D RTA_BUILD_TESTS=ON
+ -D RTA_BUILD_PYTHON_BINDINGS=OFF
+
+ - name: Build
+ run: cmake --build build --config Release
+
+ - name: Binary smoke
+ run: test -x build/src/rawtoaces_gui/rawtoaces_gui
+
+ gui-macos:
+ name: "GUI macOS"
+ runs-on: macos-latest
+ steps:
+ - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
+
+ - name: Dependencies
+ run: |
+ brew install cmake ceres-solver nlohmann-json openimageio nanobind robin-map qt@6 exiftool lensfun
+ python3 -m pip install --break-system-packages pytest || python3 -m pip install pytest
+
+ - name: Configure
+ run: |
+ QT_PREFIX="$(brew --prefix qt@6)"
+ cmake -S . -B build \
+ -D CMAKE_BUILD_TYPE=Release \
+ -D RTA_BUILD_GUI=ON \
+ -D RTA_BUILD_TESTS=ON \
+ -D CMAKE_PREFIX_PATH="$QT_PREFIX"
+
+ - name: Build
+ run: cmake --build build --config Release
+
+ - name: Binary smoke
+ run: test -x build/src/rawtoaces_gui/rawtoaces_gui
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 30274675..4ce23a92 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -1,4 +1,4 @@
-cmake_minimum_required(VERSION 3.12)
+cmake_minimum_required(VERSION 3.16)
project( RAWTOACES )
if( NOT DEFINED CMAKE_CXX_STANDARD )
@@ -78,6 +78,7 @@ option( RTA_BUILD_TESTS "Build unit tests" ON )
option( RTA_INSTALL_DATABASE "Download and install the spectral measurements database" ON )
option( ENABLE_COVERAGE "Enable code coverage reporting" OFF )
option( RTA_ENABLE_LENSFUN "Use lensfun for lens correction" ON )
+option( RTA_BUILD_GUI "Build Qt 6 GUI application (rawtoaces_gui)" OFF )
set ( RTA_SANITISER_MODE "none" CACHE STRING "Dynamic analysis sanitiser mode ('none', 'address', 'memory', or 'thread')" )
if ( ENABLE_SHARED )
@@ -102,12 +103,19 @@ add_subdirectory("src/${RAWTOACES_CORE_LIB}")
add_subdirectory("src/${RAWTOACES_UTIL_LIB}")
add_subdirectory("src/rawtoaces")
+if ( RTA_BUILD_TESTS )
+ enable_testing()
+endif()
+
+if ( RTA_BUILD_GUI )
+ add_subdirectory("src/rawtoaces_gui")
+endif()
+
if ( RTA_BUILD_PYTHON_BINDINGS )
add_subdirectory(src/bindings)
endif ( RTA_BUILD_PYTHON_BINDINGS )
if ( RTA_BUILD_TESTS )
- enable_testing()
add_subdirectory(tests)
endif ( RTA_BUILD_TESTS )
diff --git a/README.md b/README.md
index b1d2838d..dfa87374 100644
--- a/README.md
+++ b/README.md
@@ -3,6 +3,7 @@
[](https://github.com/AcademySoftwareFoundation/rawtoaces/actions/workflows/ci.yml)
+[](https://github.com/AcademySoftwareFoundation/rawtoaces/actions/workflows/gui.yml)
[](https://github.com/AcademySoftwareFoundation/rawtoaces/actions/workflows/github-code-scanning/codeql)
[](https://www.bestpractices.dev/projects/11185)
[](https://codecov.io/gh/AcademySoftwareFoundation/rawtoaces)
@@ -35,16 +36,18 @@ The source code contains the following:
* [`config/`](./config) - CMake configuration files
* [`docs/`](./docs) - Credits and changes information
* [`include/`](./include) - Public header files for the `rawtoaces` library
-* [`src/`](./src) - Source code for the core library, utility library, and the command line tool
+* [`src/`](./src) - Source code for the core library, utility library, command line tool, and optional Qt GUI (`rawtoaces_gui`)
* [`unittest/`](./unittest) - Unit tests for `rawtoaces`
+
## Prerequisites
To build `rawtoaces` you would need to satisfy these dependencies:
| Library | Min Version| Purpose | Link to installation instruction |
| ------- | -----------| -------- | -------------------------------- |
-| `cmake` | `3.12` | | [CMake download](https://cmake.org/download/)|
+| `cmake` | `3.16` | | [CMake download](https://cmake.org/download/)|
+| Qt 6 (optional) | `6.5` | Widgets module for `rawtoaces_gui` | [Qt documentation](https://doc.qt.io/qt-6/) |
| `ceres` | `1.12.0` | Ceres Solver is an open source library for solving Non-linear Least Squares problems with bounds constraints and unconstrained optimization problems. It processes non-linear regression for rawtoaces. | [Ceres Solver installation](http://ceres-solver.org/installation.html)|
| `OpenImageIO` | `3.0` | OpenImageIO is an open source library providing vast functionality for image processing. rawtoaces relies on OpenImageIO for reading raw files, saving AcesContainer files, and also all pixel operations. | [OpenImageIO installation](https://github.com/AcademySoftwareFoundation/OpenImageIO/blob/main/INSTALL.md) |
| `nlohmann-json` | `3.6` | nlohmann-json is a simple header-only library for parsing JSON files. | [nlohmann-json integration](https://github.com/nlohmann/json#integration) |
@@ -116,6 +119,16 @@ $ sudo cmake --install build # Optional if you want it to be accessible system w
The default process will install `librawtoaces_core_${rawtoaces_version}.dylib` and `librawtoaces_util_${rawtoaces_version}.dylib` to `/usr/local/lib`, a few header files to `/usr/local/include/rawtoaces` and a number of data files into `/usr/local/include/rawtoaces/data`.
+#### Qt GUI (optional)
+
+Build the desktop application `rawtoaces_gui` with `-DRTA_BUILD_GUI=ON` (requires Qt 6 Widgets). On macOS with Homebrew Qt, point CMake at the prefix, for example:
+
+```sh
+$ cmake -S . -B build -DRTA_BUILD_GUI=ON -DCMAKE_PREFIX_PATH="$(brew --prefix qt@6)"
+$ cmake --build build
+```
+
+
#### Docker
Assuming you have [Docker](https://www.docker.com/) installed, installing and
diff --git a/src/bindings/CMakeLists.txt b/src/bindings/CMakeLists.txt
index 6aa8eaff..587bdda6 100644
--- a/src/bindings/CMakeLists.txt
+++ b/src/bindings/CMakeLists.txt
@@ -1,4 +1,4 @@
-cmake_minimum_required ( VERSION 3.12 )
+cmake_minimum_required ( VERSION 3.16 )
include_directories ( "${CMAKE_CURRENT_SOURCE_DIR}" )
nanobind_add_module( rawtoaces_bindings
diff --git a/src/rawtoaces/CMakeLists.txt b/src/rawtoaces/CMakeLists.txt
index ec7116c2..bb8ddb70 100644
--- a/src/rawtoaces/CMakeLists.txt
+++ b/src/rawtoaces/CMakeLists.txt
@@ -1,4 +1,4 @@
-cmake_minimum_required(VERSION 3.12)
+cmake_minimum_required(VERSION 3.16)
# Include coverage support if enabled
if( ENABLE_COVERAGE )
diff --git a/src/rawtoaces_core/CMakeLists.txt b/src/rawtoaces_core/CMakeLists.txt
index 3cdac713..8818e1ab 100644
--- a/src/rawtoaces_core/CMakeLists.txt
+++ b/src/rawtoaces_core/CMakeLists.txt
@@ -1,4 +1,4 @@
-cmake_minimum_required(VERSION 3.12)
+cmake_minimum_required(VERSION 3.16)
include_directories( "${CMAKE_CURRENT_SOURCE_DIR}" )
# Include coverage support if enabled
diff --git a/src/rawtoaces_gui/CMakeLists.txt b/src/rawtoaces_gui/CMakeLists.txt
new file mode 100644
index 00000000..9474c78f
--- /dev/null
+++ b/src/rawtoaces_gui/CMakeLists.txt
@@ -0,0 +1,35 @@
+cmake_minimum_required(VERSION 3.16)
+
+find_package(Qt6 6.5 REQUIRED COMPONENTS Widgets)
+
+set(CMAKE_AUTOMOC ON)
+
+qt_add_library(rawtoaces_gui_lib STATIC
+ main_window.cpp
+ conversion_thread.cpp
+)
+
+target_link_libraries(rawtoaces_gui_lib PUBLIC
+ ${RAWTOACES_UTIL_LIB}
+ Qt6::Widgets
+)
+
+target_include_directories(rawtoaces_gui_lib PUBLIC
+ "${CMAKE_CURRENT_SOURCE_DIR}"
+)
+
+target_compile_definitions(rawtoaces_gui_lib PUBLIC
+ $<$:RTA_GUI_HAS_LENSFUN=1>
+)
+
+qt_add_executable(rawtoaces_gui
+ main.cpp
+)
+
+target_link_libraries(rawtoaces_gui PRIVATE rawtoaces_gui_lib)
+
+if(WIN32)
+ set_target_properties(rawtoaces_gui PROPERTIES WIN32_EXECUTABLE TRUE)
+endif()
+
+install(TARGETS rawtoaces_gui DESTINATION ${INSTALL_BIN_DIR})
diff --git a/src/rawtoaces_gui/conversion_thread.cpp b/src/rawtoaces_gui/conversion_thread.cpp
new file mode 100644
index 00000000..18515410
--- /dev/null
+++ b/src/rawtoaces_gui/conversion_thread.cpp
@@ -0,0 +1,43 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright Contributors to the rawtoaces Project.
+
+#include "conversion_thread.h"
+
+ConversionThread::ConversionThread( QObject *parent ) : QThread( parent ) {}
+
+void ConversionThread::setJob(
+ rta::util::ImageConverter::Settings settings, QStringList paths )
+{
+ m_settings = std::move( settings );
+ m_paths = std::move( paths );
+ m_cancel.store( false );
+}
+
+void ConversionThread::requestCancel()
+{
+ m_cancel.store( true );
+}
+
+void ConversionThread::run()
+{
+ const int total = static_cast( m_paths.size() );
+ for ( int i = 0; i < total; ++i )
+ {
+ if ( m_cancel.load() )
+ {
+ emit batchFinished();
+ return;
+ }
+
+ const QString path = m_paths.at( i );
+ emit fileStarted( i, path );
+
+ rta::util::ImageConverter converter;
+ converter.settings = m_settings;
+ const bool ok = converter.process_image( path.toStdString() );
+ emit fileFinished(
+ i, ok, QString::fromStdString( converter.last_error_message ) );
+ emit progress( i + 1, total );
+ }
+ emit batchFinished();
+}
diff --git a/src/rawtoaces_gui/conversion_thread.h b/src/rawtoaces_gui/conversion_thread.h
new file mode 100644
index 00000000..4a69657e
--- /dev/null
+++ b/src/rawtoaces_gui/conversion_thread.h
@@ -0,0 +1,38 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright Contributors to the rawtoaces Project.
+
+#pragma once
+
+#include
+
+#include
+#include
+
+#include
+
+class ConversionThread final : public QThread
+{
+ Q_OBJECT
+
+public:
+ explicit ConversionThread( QObject *parent = nullptr );
+
+ void setJob(
+ rta::util::ImageConverter::Settings settings, QStringList paths );
+
+ void requestCancel();
+
+signals:
+ void fileStarted( int index, QString path );
+ void fileFinished( int index, bool ok, QString message );
+ void progress( int done, int total );
+ void batchFinished();
+
+protected:
+ void run() override;
+
+private:
+ rta::util::ImageConverter::Settings m_settings{};
+ QStringList m_paths;
+ std::atomic_bool m_cancel{ false };
+};
diff --git a/src/rawtoaces_gui/main.cpp b/src/rawtoaces_gui/main.cpp
new file mode 100644
index 00000000..535e7c1f
--- /dev/null
+++ b/src/rawtoaces_gui/main.cpp
@@ -0,0 +1,29 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright Contributors to the rawtoaces Project.
+
+#include "main_window.h"
+
+#include
+
+#ifndef WIN32
+#include
+#else
+#include
+#endif
+
+int main( int argc, char *argv[] )
+{
+#ifndef WIN32
+ setenv( "TZ", "UTC", 1 );
+#else
+ _putenv( const_cast( "TZ=UTC" ) );
+#endif
+
+ QApplication application( argc, argv );
+ QApplication::setApplicationName( QStringLiteral( "rawtoaces" ) );
+ QApplication::setOrganizationName( QStringLiteral( "rawtoaces" ) );
+
+ MainWindow mainWindow;
+ mainWindow.show();
+ return application.exec();
+}
diff --git a/src/rawtoaces_gui/main_window.cpp b/src/rawtoaces_gui/main_window.cpp
new file mode 100644
index 00000000..e5df7d89
--- /dev/null
+++ b/src/rawtoaces_gui/main_window.cpp
@@ -0,0 +1,1999 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright Contributors to the rawtoaces Project.
+
+#include "main_window.h"
+#include "conversion_thread.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+
+#include
+#include
+
+namespace
+{
+using S = rta::util::ImageConverter::Settings;
+
+/// `QFormLayout` often leaves a bare `QCheckBox` top-aligned in a tall label row
+/// (style-dependent). Hosting it in an expanding row with `AlignVCenter`
+/// matches the label column’s vertical center without per-platform pixel tweaks.
+QWidget *wrapCheckBoxForFormRow( QCheckBox *checkBox )
+{
+ if ( checkBox == nullptr )
+ {
+ return nullptr;
+ }
+ auto *host = new QWidget;
+ auto *lay = new QHBoxLayout( host );
+ lay->setContentsMargins( 0, 0, 0, 0 );
+ lay->setSpacing( 0 );
+ lay->setAlignment( Qt::AlignVCenter );
+ lay->addWidget( checkBox, 0, Qt::AlignVCenter );
+ lay->addStretch( 1 );
+ host->setSizePolicy( QSizePolicy::Expanding, QSizePolicy::MinimumExpanding );
+ return host;
+}
+
+/// Fixed width for numeric fields so similar controls align across the window.
+constexpr int kStdNumericFieldWidth = 112;
+
+/// Inset from the scroll viewport edges on settings tabs (Raw, Colour, Lens,
+/// Output). Vertical gaps between sections do **not** use `QVBoxLayout::spacing`
+/// because each `QGroupBox` carries style-dependent chrome; instead each
+/// **logical** block (including one full two-column row) sits in
+/// `wrapSettingsSectionTail` with a fixed bottom margin (`kSettingsSectionTailGap`).
+constexpr int kSettingsTabPageMargin = 6;
+constexpr int kSettingsSectionTailGap = 6;
+
+void applySettingsTabPageMarginsOnly( QVBoxLayout *outerColumn )
+{
+ if ( outerColumn == nullptr )
+ {
+ return;
+ }
+ outerColumn->setContentsMargins( kSettingsTabPageMargin,
+ kSettingsTabPageMargin,
+ kSettingsTabPageMargin,
+ kSettingsTabPageMargin );
+}
+
+void applySettingsTabPageChrome( QVBoxLayout *outerColumn )
+{
+ applySettingsTabPageMarginsOnly( outerColumn );
+ outerColumn->setSpacing( 0 );
+}
+
+/// Uniform space **below** a settings “section” (one full-width group or one
+/// entire side-by-side row). Style engine margins on `QGroupBox` differ by
+/// width and pairing; this wrapper is the single place that defines rhythm.
+QWidget *wrapSettingsSectionTail( QWidget *content )
+{
+ if ( content == nullptr )
+ {
+ return nullptr;
+ }
+ auto *wrap = new QWidget;
+ auto *lay = new QVBoxLayout( wrap );
+ lay->setContentsMargins( 0, 0, 0, kSettingsSectionTailGap );
+ lay->setSpacing( 0 );
+ lay->addWidget( content );
+ return wrap;
+}
+
+/// `QHBoxLayout` + `Qt::AlignTop` keeps each `QGroupBox` at its content height
+/// while the row height follows the taller column, leaving bare space under the
+/// short box (looks like extra padding above the next full-width section).
+/// Filling the row vertically keeps both frames aligned; inner forms stay
+/// top-pinned via `mountFormInGroupBox`.
+void styleGroupBoxForSettingsRowPair( QGroupBox *box )
+{
+ if ( box == nullptr )
+ {
+ return;
+ }
+ box->setSizePolicy( QSizePolicy::Preferred,
+ QSizePolicy::MinimumExpanding );
+}
+
+QWidget *wrapScroll( QWidget *inner )
+{
+ auto *scroll = new QScrollArea;
+ scroll->setWidgetResizable( true );
+ scroll->setFrameShape( QFrame::NoFrame );
+ scroll->setWidget( inner );
+ return scroll;
+}
+
+/// Fixed pixel width for compact numeric fields. Uses setFixedWidth (not only
+/// setMaximumWidth) so e.g. QDoubleSpinBox widgets match despite different decimals.
+void setFieldMaxWidth( QWidget *widget, int widthPixels )
+{
+ if ( widget == nullptr || widthPixels <= 0 )
+ {
+ return;
+ }
+ widget->setFixedWidth( widthPixels );
+ widget->setSizePolicy( QSizePolicy::Fixed, QSizePolicy::Fixed );
+}
+
+/// macOS (`QMacStyle`) and Fusion differ on default `QFormLayout` label alignment.
+/// Use a single policy: leading form, **right-aligned** labels, **vertically
+/// centered** next to fields. Standalone checkboxes use `wrapCheckBoxForFormRow`.
+void polishFormLayout( QFormLayout *form )
+{
+ if ( form == nullptr )
+ {
+ return;
+ }
+ form->setFormAlignment( Qt::AlignLeading | Qt::AlignTop );
+ form->setLabelAlignment( Qt::AlignRight | Qt::AlignVCenter );
+}
+
+/// Rows whose field is a nested `QFormLayout` need the *outer* label top-aligned
+/// so it lines up with the first inner row (baseline / default center looks wrong).
+void alignFormLabelTopForField( QFormLayout *form, QWidget *field )
+{
+ if ( form == nullptr || field == nullptr )
+ {
+ return;
+ }
+ for ( int r = 0; r < form->rowCount(); ++r )
+ {
+ QLayoutItem *const fieldItem = form->itemAt( r, QFormLayout::FieldRole );
+ if ( fieldItem == nullptr || fieldItem->widget() != field )
+ {
+ continue;
+ }
+ QLayoutItem *const labelItem = form->itemAt( r, QFormLayout::LabelRole );
+ if ( labelItem != nullptr )
+ {
+ labelItem->setAlignment( Qt::AlignRight | Qt::AlignTop );
+ }
+ break;
+ }
+}
+
+/// Pair with `alignFormLabelTopForField` for tall / stacked fields so the field
+/// column stays top-aligned with the outer label.
+void alignFormFieldTopForField( QFormLayout *form, QWidget *field )
+{
+ if ( form == nullptr || field == nullptr )
+ {
+ return;
+ }
+ for ( int r = 0; r < form->rowCount(); ++r )
+ {
+ QLayoutItem *const fieldItem = form->itemAt( r, QFormLayout::FieldRole );
+ if ( fieldItem == nullptr || fieldItem->widget() != field )
+ {
+ continue;
+ }
+ fieldItem->setAlignment( Qt::AlignLeft | Qt::AlignTop );
+ break;
+ }
+}
+
+/// macOS often stretches a `QFormLayout` that is the direct layout of a wide
+/// `QGroupBox`, centering short rows. Put the form on a child widget with
+/// horizontal Maximum width and anchor it to the top-leading corner.
+void mountFormInGroupBox( QGroupBox *group, QFormLayout **outForm )
+{
+ if ( group == nullptr || outForm == nullptr )
+ {
+ return;
+ }
+ auto *outer = new QVBoxLayout( group );
+ auto *inner = new QWidget;
+ *outForm = new QFormLayout( inner );
+ polishFormLayout( *outForm );
+ inner->setSizePolicy( QSizePolicy::Maximum, QSizePolicy::Preferred );
+ outer->addWidget( inner, 0, Qt::AlignLeft | Qt::AlignTop );
+}
+
+/// Same as `mountFormInGroupBox`, but the form host grows horizontally (paths).
+void mountFormInGroupBoxFullWidth( QGroupBox *group, QFormLayout **outForm )
+{
+ if ( group == nullptr || outForm == nullptr )
+ {
+ return;
+ }
+ auto *outer = new QVBoxLayout( group );
+ auto *inner = new QWidget;
+ *outForm = new QFormLayout( inner );
+ polishFormLayout( *outForm );
+ inner->setSizePolicy( QSizePolicy::Expanding, QSizePolicy::Preferred );
+ // Default alignment (0): item fills the layout cell horizontally. Passing
+ // only Qt::AlignTop still omits a horizontal flag, so QBoxLayout centers
+ // the widget and keeps it at its width hint — same “narrow path rows” bug.
+ outer->addWidget( inner );
+}
+
+void addLabeledSpinRows( QFormLayout *form,
+ QSpinBox *boxes[4],
+ const QStringList &rowLabels,
+ int maxFieldWidth )
+{
+ for ( int i = 0; i < 4; ++i )
+ {
+ if ( maxFieldWidth > 0 )
+ {
+ setFieldMaxWidth( boxes[i], maxFieldWidth );
+ }
+ form->addRow( rowLabels.at( i ), boxes[i] );
+ }
+}
+
+QStringList flattenBatches( const std::vector> &batches )
+{
+ QStringList out;
+ for ( const auto &batch: batches )
+ {
+ for ( const auto &p: batch )
+ {
+ out.push_back( QString::fromStdString( p ) );
+ }
+ }
+ return out;
+}
+
+/// Override paths from the spectral data line edit (`;` / `:` separated).
+/// Empty field → empty vector so the library fills from env / defaults (same as
+/// not overriding spectral data paths in the converter).
+std::vector spectralDatabaseDirsFromLineEdit( const QString &text )
+{
+ const QString trimmed = text.trimmed();
+ if ( trimmed.isEmpty() )
+ {
+ return {};
+ }
+ std::vector out;
+ const QStringList parts = trimmed.split(
+ QRegularExpression( QStringLiteral( "[;:]" ) ), Qt::SkipEmptyParts );
+ for ( const QString &part: parts )
+ {
+ const QString one = part.trimmed();
+ if ( one.isEmpty() )
+ {
+ continue;
+ }
+ out.push_back( one.toStdString() );
+ }
+ return out;
+}
+
+} // namespace
+
+MainWindow::MainWindow( QWidget *parent ) : QMainWindow( parent )
+{
+ setObjectName( QStringLiteral( "rawtoacesMainWindow" ) );
+ setWindowTitle( tr( "rawtoaces" ) );
+
+ m_mainSplitter = new QSplitter( Qt::Vertical );
+
+ auto *top = new QWidget;
+ auto *topLay = new QVBoxLayout( top );
+ topLay->setSpacing( 6 );
+
+ auto *inputGroup = new QGroupBox( tr( "Input files" ) );
+ auto *inputLay = new QVBoxLayout( inputGroup );
+ inputLay->setSpacing( 6 );
+
+ auto *fileRow = new QHBoxLayout;
+ m_fileList = new QListWidget;
+ m_fileList->setObjectName( QStringLiteral( "guiFileList" ) );
+ m_fileList->setSelectionMode( QAbstractItemView::ExtendedSelection );
+ m_fileList->setMinimumHeight( 48 );
+ m_fileList->setSizePolicy( QSizePolicy::Expanding, QSizePolicy::Expanding );
+ fileRow->addWidget( m_fileList, 1 );
+ auto *fileBtnCol = new QVBoxLayout;
+ fileBtnCol->setSpacing( 6 );
+ auto *addFiles = new QPushButton( tr( "Add files…" ) );
+ auto *addFolder = new QPushButton( tr( "Add folder…" ) );
+ auto *removeBtn = new QPushButton( tr( "Remove" ) );
+ auto *clearBtn = new QPushButton( tr( "Clear" ) );
+ fileBtnCol->addWidget( addFiles );
+ fileBtnCol->addWidget( addFolder );
+ fileBtnCol->addWidget( removeBtn );
+ fileBtnCol->addWidget( clearBtn );
+ fileBtnCol->addStretch();
+ fileRow->addLayout( fileBtnCol );
+ inputLay->addLayout( fileRow );
+ topLay->addWidget( inputGroup );
+
+ connect( addFiles, &QPushButton::clicked, this, &MainWindow::onAddFiles );
+ connect( addFolder, &QPushButton::clicked, this, &MainWindow::onAddFolder );
+ connect( removeBtn, &QPushButton::clicked, this, &MainWindow::onRemoveSelected );
+ connect( clearBtn, &QPushButton::clicked, this, &MainWindow::onClearFiles );
+
+ m_log = new QTextEdit;
+ m_log->setObjectName( QStringLiteral( "guiLog" ) );
+ m_log->setReadOnly( true );
+ m_log->setPlaceholderText(
+ tr( "Conversion progress and messages will appear here." ) );
+ m_log->setMinimumHeight( 72 );
+
+ m_settingsTabs = new QTabWidget;
+ m_settingsTabs->setObjectName( QStringLiteral( "guiSettingsTabs" ) );
+ m_settingsTabs->setDocumentMode( true );
+
+ // --- Raw tab (decode pipeline first) ---
+ auto *rawInner = new QWidget;
+ auto *rawOuter = new QVBoxLayout( rawInner );
+ applySettingsTabPageChrome( rawOuter );
+
+ auto *rawLevels = new QGroupBox( tr( "Levels && exposure" ) );
+ QFormLayout *rawLay = nullptr;
+ mountFormInGroupBox( rawLevels, &rawLay );
+ rawLay->setHorizontalSpacing( 12 );
+ rawLay->setVerticalSpacing( 8 );
+ // `FieldsStayAtSizeHint` keeps checkbox hosts at a tiny preferred width and
+ // clips macOS indicators; expanding policy still respects fixed-width spins.
+ rawLay->setFieldGrowthPolicy( QFormLayout::ExpandingFieldsGrow );
+ m_autoBright = new QCheckBox;
+ m_autoBright->setToolTip(
+ tr( "LibRaw automatic brightening; other level options still apply." ) );
+ m_adjustMaximum = new QDoubleSpinBox;
+ m_adjustMaximum->setRange( 0.0, 1.0 );
+ m_adjustMaximum->setDecimals( 4 );
+ m_adjustMaximum->setValue( 0.75 );
+ setFieldMaxWidth( m_adjustMaximum, kStdNumericFieldWidth );
+
+ m_blackLevelFromMetadata = new QCheckBox( tr( "Take from metadata" ) );
+ m_blackLevelFromMetadata->setChecked( true );
+ m_blackLevelFromMetadata->setToolTip(
+ tr( "Use the black level from RAW metadata; when off, use the value "
+ "below." ) );
+ m_blackLevel = new QSpinBox;
+ m_blackLevel->setRange( 0, 2147483647 );
+ m_blackLevel->setValue( 0 );
+ m_blackLevel->setEnabled( false );
+ m_blackLevel->setToolTip(
+ tr( "Override sensor black level when metadata is not used." ) );
+ setFieldMaxWidth( m_blackLevel, kStdNumericFieldWidth );
+
+ m_saturationFromMetadata = new QCheckBox( tr( "Take from metadata" ) );
+ m_saturationFromMetadata->setChecked( true );
+ m_saturationFromMetadata->setToolTip(
+ tr( "Use the saturation (clip / white) level from RAW metadata; when off, "
+ "use the value below." ) );
+ m_saturationLevel = new QSpinBox;
+ m_saturationLevel->setRange( 1, 2147483647 );
+ m_saturationLevel->setValue( 16383 );
+ m_saturationLevel->setEnabled( false );
+ m_saturationLevel->setToolTip(
+ tr( "Raw value treated as saturated when not using metadata." ) );
+ setFieldMaxWidth( m_saturationLevel, kStdNumericFieldWidth );
+
+ auto *blackLevelBlock = new QWidget;
+ auto *blackLevelVBox = new QVBoxLayout( blackLevelBlock );
+ blackLevelVBox->setContentsMargins( 0, 0, 0, 0 );
+ blackLevelVBox->setSpacing( 8 );
+ blackLevelVBox->addWidget( m_blackLevelFromMetadata );
+ blackLevelVBox->addWidget( m_blackLevel );
+
+ auto *saturationBlock = new QWidget;
+ auto *saturationVBox = new QVBoxLayout( saturationBlock );
+ saturationVBox->setContentsMargins( 0, 0, 0, 0 );
+ saturationVBox->setSpacing( 8 );
+ saturationVBox->addWidget( m_saturationFromMetadata );
+ saturationVBox->addWidget( m_saturationLevel );
+
+ rawLay->addRow( tr( "Auto bright:" ), wrapCheckBoxForFormRow( m_autoBright ) );
+ rawLay->addRow( tr( "Adjust maximum threshold:" ), m_adjustMaximum );
+ rawLay->addRow( tr( "Black level:" ), blackLevelBlock );
+ rawLay->addRow( tr( "Saturation level:" ), saturationBlock );
+ alignFormLabelTopForField( rawLay, blackLevelBlock );
+ alignFormLabelTopForField( rawLay, saturationBlock );
+ connect( m_blackLevelFromMetadata,
+ &QCheckBox::toggled,
+ this,
+ &MainWindow::updateBlackSaturationUi );
+ connect( m_saturationFromMetadata,
+ &QCheckBox::toggled,
+ this,
+ &MainWindow::updateBlackSaturationUi );
+ updateBlackSaturationUi();
+
+ auto *rawChroma = new QGroupBox( tr( "Chromatic aberration && size" ) );
+ QFormLayout *chForm = nullptr;
+ mountFormInGroupBox( rawChroma, &chForm );
+ chForm->setHorizontalSpacing( 12 );
+ chForm->setVerticalSpacing( 8 );
+ chForm->setFieldGrowthPolicy( QFormLayout::ExpandingFieldsGrow );
+ m_chromaR = new QDoubleSpinBox;
+ m_chromaB = new QDoubleSpinBox;
+ m_chromaR->setRange( 0.0, 1.0e6 );
+ m_chromaB->setRange( 0.0, 1.0e6 );
+ m_chromaR->setDecimals( 4 );
+ m_chromaB->setDecimals( 4 );
+ m_chromaR->setValue( 1.0 );
+ m_chromaB->setValue( 1.0 );
+ m_halfSize = new QCheckBox;
+ m_highlightMode = new QComboBox;
+ m_highlightMode->setObjectName( QStringLiteral( "guiHighlightMode" ) );
+ m_highlightMode->addItem( tr( "0 — Clip" ), 0 );
+ m_highlightMode->addItem( tr( "1 — Unclip" ), 1 );
+ m_highlightMode->addItem( tr( "2 — Blend" ), 2 );
+ for ( int h = 3; h <= 9; ++h )
+ {
+ m_highlightMode->addItem(
+ tr( "%1 — Rebuild (level %2)" ).arg( h ).arg( h ), h );
+ }
+ m_highlightMode->setMaximumWidth( kStdNumericFieldWidth * 3 );
+ m_highlightMode->setToolTip(
+ tr( "LibRaw highlight recovery: 0 clip, 1 unclip, 2 blend; 3–9 are "
+ "rebuild levels with increasing strength." ) );
+ setFieldMaxWidth( m_chromaR, kStdNumericFieldWidth );
+ setFieldMaxWidth( m_chromaB, kStdNumericFieldWidth );
+ chForm->addRow( tr( "Red channel multiplier:" ), m_chromaR );
+ chForm->addRow( tr( "Blue channel multiplier:" ), m_chromaB );
+ chForm->addRow( tr( "Half-size decode:" ),
+ wrapCheckBoxForFormRow( m_halfSize ) );
+ chForm->addRow( tr( "Highlight mode:" ), m_highlightMode );
+
+ auto *rawCrop = new QGroupBox( tr( "Crop, orientation && denoise" ) );
+ QFormLayout *crForm = nullptr;
+ mountFormInGroupBox( rawCrop, &crForm );
+ crForm->setHorizontalSpacing( 12 );
+ crForm->setVerticalSpacing( 8 );
+ crForm->setFieldGrowthPolicy( QFormLayout::FieldsStayAtSizeHint );
+ for ( int i = 0; i < 4; ++i )
+ {
+ m_cropBox[i] = new QSpinBox;
+ m_cropBox[i]->setRange( -1000000000, 1000000000 );
+ }
+ auto *cropRegionInner = new QWidget;
+ auto *cropRegionForm = new QFormLayout( cropRegionInner );
+ polishFormLayout( cropRegionForm );
+ cropRegionForm->setLabelAlignment( Qt::AlignRight | Qt::AlignTop );
+ cropRegionForm->setContentsMargins( 0, 0, 0, 0 );
+ cropRegionForm->setHorizontalSpacing( 12 );
+ cropRegionForm->setFieldGrowthPolicy(
+ QFormLayout::FieldsStayAtSizeHint );
+ addLabeledSpinRows(
+ cropRegionForm,
+ m_cropBox,
+ { tr( "X:" ), tr( "Y:" ), tr( "Width:" ), tr( "Height:" ) },
+ kStdNumericFieldWidth );
+ m_cropMode = new QComboBox;
+ m_cropMode->addItems( { tr( "off" ), tr( "soft" ), tr( "hard" ) } );
+ m_cropMode->setCurrentIndex( 1 );
+ m_cropMode->setMaximumWidth( kStdNumericFieldWidth * 2 );
+ m_flip = new QComboBox;
+ m_flip->setObjectName( QStringLiteral( "guiFlip" ) );
+ m_flip->addItem( tr( "0 — No override (use file metadata)" ), 0 );
+ m_flip->addItem( tr( "1 — Normal (0°)" ), 1 );
+ m_flip->addItem( tr( "2 — Mirror horizontal" ), 2 );
+ m_flip->addItem( tr( "3 — Rotate 180°" ), 3 );
+ m_flip->addItem( tr( "4 — Mirror vertical" ), 4 );
+ m_flip->addItem( tr( "5 — Mirror horizontal, rotate 270° CW" ), 5 );
+ m_flip->addItem( tr( "6 — Rotate 90° CCW" ), 6 );
+ m_flip->addItem( tr( "7 — Mirror horizontal, rotate 90° CW" ), 7 );
+ m_flip->addItem( tr( "8 — Rotate 90° CW" ), 8 );
+ m_flip->setMaximumWidth( kStdNumericFieldWidth * 4 );
+ m_flip->setToolTip(
+ tr( "Override orientation. 0 uses metadata; 1–8 are EXIF orientation "
+ "codes (e.g. 3 = 180°, 6 = 90° CCW, 8 = 90° CW)." ) );
+ m_denoise = new QDoubleSpinBox;
+ m_denoise->setRange( 0.0, 1.0e9 );
+ m_denoise->setDecimals( 4 );
+ m_denoise->setValue( 0.0 );
+ crForm->addRow( tr( "Crop region (pixels):" ), cropRegionInner );
+ alignFormLabelTopForField( crForm, cropRegionInner );
+ crForm->addRow( tr( "Crop mode:" ), m_cropMode );
+ crForm->addRow( tr( "Flip:" ), m_flip );
+ crForm->addRow( tr( "Denoise threshold:" ), m_denoise );
+ setFieldMaxWidth( m_denoise, kStdNumericFieldWidth );
+
+ auto *rawDemo = new QGroupBox( tr( "Demosaic" ) );
+ QFormLayout *dmForm = nullptr;
+ mountFormInGroupBox( rawDemo, &dmForm );
+ dmForm->setHorizontalSpacing( 12 );
+ dmForm->setFieldGrowthPolicy( QFormLayout::FieldsStayAtSizeHint );
+ m_demosaic = new QComboBox;
+ const QStringList demosaicNames = {
+ QStringLiteral( "linear" ), QStringLiteral( "VNG" ),
+ QStringLiteral( "PPG" ), QStringLiteral( "AHD" ),
+ QStringLiteral( "DCB" ), QStringLiteral( "AHD-Mod" ),
+ QStringLiteral( "AFD" ), QStringLiteral( "VCD" ),
+ QStringLiteral( "Mixed" ), QStringLiteral( "LMMSE" ),
+ QStringLiteral( "AMaZE" ), QStringLiteral( "DHT" ),
+ QStringLiteral( "AAHD" ), QStringLiteral( "AHD" ) };
+ m_demosaic->addItems( demosaicNames );
+ m_demosaic->setMaximumWidth( kStdNumericFieldWidth * 2 );
+ dmForm->addRow( tr( "Algorithm:" ), m_demosaic );
+
+ styleGroupBoxForSettingsRowPair( rawLevels );
+ styleGroupBoxForSettingsRowPair( rawChroma );
+ auto *rawTopRow = new QWidget;
+ auto *rawTopLay = new QHBoxLayout( rawTopRow );
+ rawTopLay->setContentsMargins( 0, 0, 0, 0 );
+ rawTopLay->setSpacing( 12 );
+ rawTopLay->addWidget( rawLevels, 1 );
+ rawTopLay->addWidget( rawChroma, 1 );
+
+ styleGroupBoxForSettingsRowPair( rawCrop );
+ styleGroupBoxForSettingsRowPair( rawDemo );
+ auto *rawMidRow = new QWidget;
+ auto *rawMidLay = new QHBoxLayout( rawMidRow );
+ rawMidLay->setContentsMargins( 0, 0, 0, 0 );
+ rawMidLay->setSpacing( 12 );
+ rawMidLay->addWidget( rawCrop, 3 );
+ rawMidLay->addWidget( rawDemo, 1 );
+
+ rawOuter->addWidget( wrapSettingsSectionTail( rawTopRow ) );
+ rawOuter->addWidget( wrapSettingsSectionTail( rawMidRow ) );
+ rawOuter->addStretch();
+
+ m_settingsTabs->addTab( wrapScroll( rawInner ), tr( "Raw" ) );
+
+ // --- Colour tab ---
+ auto *colourInner = new QWidget;
+ auto *colourOuter = new QVBoxLayout( colourInner );
+ applySettingsTabPageChrome( colourOuter );
+
+ auto *grpSpectral = new QGroupBox( tr( "Spectral data" ) );
+ QFormLayout *spectralForm = nullptr;
+ mountFormInGroupBoxFullWidth( grpSpectral, &spectralForm );
+ spectralForm->setHorizontalSpacing( 12 );
+ spectralForm->setVerticalSpacing( 8 );
+ spectralForm->setFieldGrowthPolicy( QFormLayout::ExpandingFieldsGrow );
+ m_dataDir = new QLineEdit;
+ m_dataDir->setObjectName( QStringLiteral( "guiDataDir" ) );
+ m_dataDir->setPlaceholderText( tr( "Empty = default search paths" ) );
+ m_dataDir->setToolTip(
+ tr( "Override directories for camera / illuminant spectral data. "
+ "Separate multiple paths with ';' or ':'. Empty uses library defaults "
+ "and environment." ) );
+ auto *dataBrowse = new QPushButton( tr( "Browse…" ) );
+ auto *dataWrap = new QWidget;
+ dataWrap->setSizePolicy( QSizePolicy::Expanding, QSizePolicy::Preferred );
+ auto *dataHBox = new QHBoxLayout( dataWrap );
+ dataHBox->setContentsMargins( 0, 0, 0, 0 );
+ dataHBox->setSpacing( 8 );
+ dataHBox->addWidget( m_dataDir, 1 );
+ dataHBox->addWidget( dataBrowse );
+ spectralForm->addRow( tr( "Data directory:" ), dataWrap );
+ connect( dataBrowse, &QPushButton::clicked, this, &MainWindow::onBrowseDataDir );
+
+ auto *grpWb = new QGroupBox( tr( "White balance" ) );
+ QFormLayout *wbForm = nullptr;
+ mountFormInGroupBox( grpWb, &wbForm );
+ wbForm->setHorizontalSpacing( 12 );
+ wbForm->setVerticalSpacing( 8 );
+ wbForm->setFieldGrowthPolicy( QFormLayout::FieldsStayAtSizeHint );
+ m_wbMethod = new QComboBox;
+ m_wbMethod->setObjectName( QStringLiteral( "guiWbMethod" ) );
+ m_wbMethod->addItems(
+ { tr( "metadata" ), tr( "illuminant" ), tr( "box" ), tr( "custom" ) } );
+ m_wbMethod->setMaximumWidth( kStdNumericFieldWidth * 2 );
+ m_illuminant = new QLineEdit;
+ m_illuminant->setPlaceholderText( tr( "e.g. D55, 3200K" ) );
+ m_wbIlluminantWrap = new QWidget;
+ auto *illumHBox = new QHBoxLayout( m_wbIlluminantWrap );
+ illumHBox->setContentsMargins( 0, 0, 0, 0 );
+ illumHBox->addWidget( m_illuminant, 1 );
+ for ( int i = 0; i < 4; ++i )
+ {
+ m_wbBox[i] = new QSpinBox;
+ m_wbBox[i]->setRange( -1000000000, 1000000000 );
+ }
+ m_wbBoxRegionWrap = new QWidget;
+ auto *wbBoxForm = new QFormLayout( m_wbBoxRegionWrap );
+ polishFormLayout( wbBoxForm );
+ wbBoxForm->setLabelAlignment( Qt::AlignRight | Qt::AlignTop );
+ wbBoxForm->setContentsMargins( 0, 0, 0, 0 );
+ wbBoxForm->setHorizontalSpacing( 12 );
+ wbBoxForm->setFieldGrowthPolicy( QFormLayout::FieldsStayAtSizeHint );
+ addLabeledSpinRows(
+ wbBoxForm,
+ m_wbBox,
+ { tr( "X:" ), tr( "Y:" ), tr( "Width:" ), tr( "Height:" ) },
+ kStdNumericFieldWidth );
+
+ m_wbCustomGainsWrap = new QWidget;
+ auto *cwbForm = new QFormLayout( m_wbCustomGainsWrap );
+ polishFormLayout( cwbForm );
+ cwbForm->setLabelAlignment( Qt::AlignRight | Qt::AlignTop );
+ cwbForm->setContentsMargins( 0, 0, 0, 0 );
+ cwbForm->setHorizontalSpacing( 12 );
+ cwbForm->setFieldGrowthPolicy( QFormLayout::FieldsStayAtSizeHint );
+ for ( int i = 0; i < 4; ++i )
+ {
+ m_customWb[i] = new QDoubleSpinBox;
+ m_customWb[i]->setRange( 0.0, 1.0e6 );
+ m_customWb[i]->setDecimals( 6 );
+ m_customWb[i]->setValue( 1.0 );
+ setFieldMaxWidth( m_customWb[i], kStdNumericFieldWidth );
+ }
+ cwbForm->addRow( tr( "Red:" ), m_customWb[0] );
+ cwbForm->addRow( tr( "Green 1:" ), m_customWb[1] );
+ cwbForm->addRow( tr( "Green 2:" ), m_customWb[2] );
+ cwbForm->addRow( tr( "Blue:" ), m_customWb[3] );
+
+ wbForm->addRow( tr( "Method:" ), m_wbMethod );
+ wbForm->addRow( tr( "Illuminant:" ), m_wbIlluminantWrap );
+ wbForm->addRow( tr( "Region for 'box' mode:" ), m_wbBoxRegionWrap );
+ wbForm->addRow( tr( "Custom gains:" ), m_wbCustomGainsWrap );
+ alignFormLabelTopForField( wbForm, m_wbBoxRegionWrap );
+ alignFormLabelTopForField( wbForm, m_wbCustomGainsWrap );
+ m_wbIlluminantLabel =
+ qobject_cast( wbForm->labelForField( m_wbIlluminantWrap ) );
+ m_wbBoxRegionLabel =
+ qobject_cast( wbForm->labelForField( m_wbBoxRegionWrap ) );
+ m_wbCustomGainsLabel =
+ qobject_cast( wbForm->labelForField( m_wbCustomGainsWrap ) );
+ connect( m_wbMethod,
+ &QComboBox::currentIndexChanged,
+ this,
+ &MainWindow::updateWbMethodDependentUi );
+ updateWbMethodDependentUi();
+
+ auto *grpMat = new QGroupBox( tr( "Colour matrix && camera" ) );
+ QFormLayout *matForm = nullptr;
+ mountFormInGroupBox( grpMat, &matForm );
+ matForm->setHorizontalSpacing( 12 );
+ matForm->setVerticalSpacing( 8 );
+ matForm->setFieldGrowthPolicy( QFormLayout::FieldsStayAtSizeHint );
+ m_matrixMethod = new QComboBox;
+ m_matrixMethod->addItems( { tr( "auto" ),
+ tr( "spectral" ),
+ tr( "metadata" ),
+ tr( "Adobe" ),
+ tr( "custom" ) } );
+ m_matrixMethod->setMaximumWidth( kStdNumericFieldWidth * 2 );
+ matForm->addRow( tr( "Matrix method:" ), m_matrixMethod );
+
+ m_customMatrixWrap = new QWidget;
+ auto *matGrid = new QGridLayout( m_customMatrixWrap );
+ matGrid->setContentsMargins( 0, 0, 0, 0 );
+ for ( int r = 0; r < 3; ++r )
+ {
+ for ( int c = 0; c < 3; ++c )
+ {
+ m_customMat[r][c] = new QDoubleSpinBox;
+ m_customMat[r][c]->setRange( -1.0e6, 1.0e6 );
+ m_customMat[r][c]->setDecimals( 6 );
+ m_customMat[r][c]->setValue( r == c ? 1.0 : 0.0 );
+ setFieldMaxWidth( m_customMat[r][c], kStdNumericFieldWidth );
+ matGrid->addWidget( m_customMat[r][c], r, c );
+ }
+ }
+ matForm->addRow( tr( "Custom 3×3 matrix:" ), m_customMatrixWrap );
+ m_customMatrixLabel =
+ qobject_cast( matForm->labelForField( m_customMatrixWrap ) );
+ connect( m_matrixMethod,
+ &QComboBox::currentIndexChanged,
+ this,
+ &MainWindow::updateMatrixMethodDependentUi );
+ updateMatrixMethodDependentUi();
+
+ m_customCameraMake = new QLineEdit;
+ m_customCameraModel = new QLineEdit;
+ auto *makeWrap = new QWidget;
+ auto *makeHBox = new QHBoxLayout( makeWrap );
+ makeHBox->setContentsMargins( 0, 0, 0, 0 );
+ makeHBox->addWidget( m_customCameraMake, 1 );
+ auto *modelWrap = new QWidget;
+ auto *modelHBox = new QHBoxLayout( modelWrap );
+ modelHBox->setContentsMargins( 0, 0, 0, 0 );
+ modelHBox->addWidget( m_customCameraModel, 1 );
+ matForm->addRow( tr( "Override camera make:" ), makeWrap );
+ matForm->addRow( tr( "Override camera model:" ), modelWrap );
+
+ auto *grpTone = new QGroupBox( tr( "Tone && scale" ) );
+ QFormLayout *toneForm = nullptr;
+ mountFormInGroupBox( grpTone, &toneForm );
+ toneForm->setHorizontalSpacing( 12 );
+ toneForm->setFieldGrowthPolicy( QFormLayout::FieldsStayAtSizeHint );
+ m_headroom = new QDoubleSpinBox;
+ m_headroom->setRange( 0.0, 1.0e6 );
+ m_headroom->setDecimals( 3 );
+ m_headroom->setValue( 6.0 );
+ m_scale = new QDoubleSpinBox;
+ m_scale->setRange( 0.0, 1.0e6 );
+ m_scale->setDecimals( 6 );
+ m_scale->setValue( 1.0 );
+ toneForm->addRow( tr( "Headroom:" ), m_headroom );
+ toneForm->addRow( tr( "Scale:" ), m_scale );
+ setFieldMaxWidth( m_headroom, kStdNumericFieldWidth );
+ setFieldMaxWidth( m_scale, kStdNumericFieldWidth );
+
+ styleGroupBoxForSettingsRowPair( grpWb );
+ styleGroupBoxForSettingsRowPair( grpTone );
+ auto *colourTopRow = new QWidget;
+ auto *colourTopLay = new QHBoxLayout( colourTopRow );
+ colourTopLay->setContentsMargins( 0, 0, 0, 0 );
+ colourTopLay->setSpacing( 12 );
+ colourTopLay->addWidget( grpWb, 3 );
+ colourTopLay->addWidget( grpTone, 1 );
+
+ colourOuter->addWidget( wrapSettingsSectionTail( grpSpectral ) );
+ colourOuter->addWidget( wrapSettingsSectionTail( colourTopRow ) );
+ colourOuter->addWidget( wrapSettingsSectionTail( grpMat ) );
+ colourOuter->addStretch();
+
+ m_settingsTabs->addTab( wrapScroll( colourInner ), tr( "Colour" ) );
+
+#ifdef RTA_GUI_HAS_LENSFUN
+ auto *lensInner = new QWidget;
+ auto *lensOuter = new QVBoxLayout( lensInner );
+ applySettingsTabPageChrome( lensOuter );
+ auto *lensFlagsBox = new QGroupBox( tr( "Lens corrections" ) );
+ QFormLayout *lensLay = nullptr;
+ mountFormInGroupBox( lensFlagsBox, &lensLay );
+ lensLay->setHorizontalSpacing( 12 );
+ lensLay->setVerticalSpacing( 8 );
+ lensLay->setFieldGrowthPolicy( QFormLayout::ExpandingFieldsGrow );
+ m_lensCorrAberration = new QCheckBox( tr( "Chromatic aberration" ) );
+ m_lensCorrDistortion = new QCheckBox( tr( "Distortion" ) );
+ m_lensCorrVignetting = new QCheckBox( tr( "Vignetting" ) );
+ m_requireLens = new QCheckBox;
+ auto *lensCorrStack = new QWidget;
+ auto *lensCorrVBox = new QVBoxLayout( lensCorrStack );
+ lensCorrVBox->setContentsMargins( 0, 0, 0, 0 );
+ lensCorrVBox->setSpacing( 4 );
+ lensCorrVBox->addWidget( m_lensCorrAberration );
+ lensCorrVBox->addWidget( m_lensCorrDistortion );
+ lensCorrVBox->addWidget( m_lensCorrVignetting );
+ lensLay->addRow( tr( "Correction types:" ), lensCorrStack );
+ lensLay->addRow( tr( "Fail if correction unavailable:" ),
+ wrapCheckBoxForFormRow( m_requireLens ) );
+ // Tall field: align label with the first checkbox, not vertical center of stack.
+ alignFormLabelTopForField( lensLay, lensCorrStack );
+ alignFormFieldTopForField( lensLay, lensCorrStack );
+
+ auto *lensOverride = new QGroupBox( tr( "Lens metadata" ) );
+ QFormLayout *ovLay = nullptr;
+ mountFormInGroupBox( lensOverride, &ovLay );
+ ovLay->setHorizontalSpacing( 12 );
+ ovLay->setVerticalSpacing( 8 );
+ ovLay->setFieldGrowthPolicy( QFormLayout::ExpandingFieldsGrow );
+ m_lensMetadataOverride = new QCheckBox;
+ m_lensMetadataOverride->setObjectName(
+ QStringLiteral( "guiLensMetadataOverride" ) );
+ m_lensMetadataOverride->setToolTip(
+ tr( "When enabled, use the make, model, aperture, focal length, and "
+ "focus distance below for lens correction; when off, converter "
+ "defaults apply (same as leaving CLI overrides unset)." ) );
+ ovLay->addRow( tr( "Override:" ),
+ wrapCheckBoxForFormRow( m_lensMetadataOverride ) );
+ connect( m_lensMetadataOverride,
+ &QCheckBox::toggled,
+ this,
+ &MainWindow::updateLensMetadataOverrideUi );
+ m_lensMake = new QLineEdit;
+ m_lensModel = new QLineEdit;
+ m_lensAperture = new QDoubleSpinBox;
+ m_lensAperture->setRange( 0.0, 1.0e6 );
+ m_lensAperture->setDecimals( 3 );
+ m_lensFocal = new QDoubleSpinBox;
+ m_lensFocal->setRange( 0.0, 1.0e6 );
+ m_lensFocal->setDecimals( 3 );
+ m_lensFocus = new QDoubleSpinBox;
+ m_lensFocus->setRange( 0.0, 1.0e9 );
+ m_lensFocus->setDecimals( 4 );
+ auto *lensMakeWrap = new QWidget;
+ auto *lensMakeHBox = new QHBoxLayout( lensMakeWrap );
+ lensMakeHBox->setContentsMargins( 0, 0, 0, 0 );
+ lensMakeHBox->addWidget( m_lensMake, 1 );
+ auto *lensModelWrap = new QWidget;
+ auto *lensModelHBox = new QHBoxLayout( lensModelWrap );
+ lensModelHBox->setContentsMargins( 0, 0, 0, 0 );
+ lensModelHBox->addWidget( m_lensModel, 1 );
+ ovLay->addRow( tr( "Make:" ), lensMakeWrap );
+ ovLay->addRow( tr( "Model:" ), lensModelWrap );
+ ovLay->addRow( tr( "Aperture (f-number):" ), m_lensAperture );
+ ovLay->addRow( tr( "Focal length (mm):" ), m_lensFocal );
+ ovLay->addRow( tr( "Focus distance:" ), m_lensFocus );
+ setFieldMaxWidth( m_lensAperture, kStdNumericFieldWidth );
+ setFieldMaxWidth( m_lensFocal, kStdNumericFieldWidth );
+ setFieldMaxWidth( m_lensFocus, kStdNumericFieldWidth );
+ updateLensMetadataOverrideUi();
+
+ styleGroupBoxForSettingsRowPair( lensFlagsBox );
+ styleGroupBoxForSettingsRowPair( lensOverride );
+ auto *lensTopRow = new QWidget;
+ auto *lensTopLay = new QHBoxLayout( lensTopRow );
+ lensTopLay->setContentsMargins( 0, 0, 0, 0 );
+ lensTopLay->setSpacing( 12 );
+ lensTopLay->addWidget( lensFlagsBox, 1 );
+ lensTopLay->addWidget( lensOverride, 1 );
+ lensOuter->addWidget( wrapSettingsSectionTail( lensTopRow ) );
+ lensOuter->addStretch();
+ m_settingsTabs->addTab( wrapScroll( lensInner ), tr( "Lens" ) );
+#else
+ m_lensCorrAberration = nullptr;
+ m_lensCorrDistortion = nullptr;
+ m_lensCorrVignetting = nullptr;
+ m_requireLens = nullptr;
+ m_lensMetadataOverride = nullptr;
+ m_lensMake = nullptr;
+ m_lensModel = nullptr;
+ m_lensAperture = nullptr;
+ m_lensFocal = nullptr;
+ m_lensFocus = nullptr;
+#endif
+
+ // --- Output & diagnostics tab (last; paths, write flags, logging) ---
+ auto *odInner = new QWidget;
+ auto *odVBox = new QVBoxLayout( odInner );
+ applySettingsTabPageChrome( odVBox );
+
+ auto *pathGroup = new QGroupBox( tr( "Output" ) );
+ QFormLayout *pathForm = nullptr;
+ mountFormInGroupBoxFullWidth( pathGroup, &pathForm );
+ pathForm->setHorizontalSpacing( 12 );
+ pathForm->setVerticalSpacing( 8 );
+ pathForm->setFieldGrowthPolicy( QFormLayout::ExpandingFieldsGrow );
+
+ m_outputDir = new QLineEdit;
+ m_outputDir->setObjectName( QStringLiteral( "guiOutputDir" ) );
+ m_outputDir->setPlaceholderText(
+ tr( "Empty = same folder as each RAW; or set a subfolder / path" ) );
+ m_outputDir->setToolTip(
+ tr( "Leave empty to write .exr next to the source file. "
+ "If set, output paths are resolved under each input file’s directory." ) );
+ auto *outBrowse = new QPushButton( tr( "Browse…" ) );
+ auto *outWrap = new QWidget;
+ outWrap->setSizePolicy( QSizePolicy::Expanding, QSizePolicy::Preferred );
+ auto *outHBox = new QHBoxLayout( outWrap );
+ outHBox->setContentsMargins( 0, 0, 0, 0 );
+ outHBox->setSpacing( 8 );
+ outHBox->addWidget( m_outputDir, 1 );
+ outHBox->addWidget( outBrowse );
+ pathForm->addRow( tr( "Output directory:" ), outWrap );
+ connect( outBrowse, &QPushButton::clicked, this, &MainWindow::onBrowseOutput );
+
+ m_overwrite = new QCheckBox;
+ m_createDirs = new QCheckBox;
+ m_overwrite->setToolTip(
+ tr( "Replace existing output files instead of skipping them." ) );
+ m_createDirs->setToolTip(
+ tr( "When an output directory is set, create missing parent folders "
+ "if they do not exist." ) );
+ pathForm->addRow( tr( "Overwrite existing files:" ),
+ wrapCheckBoxForFormRow( m_overwrite ) );
+ pathForm->addRow( tr( "Create missing directories:" ),
+ wrapCheckBoxForFormRow( m_createDirs ) );
+
+ auto *logGroup = new QGroupBox( tr( "Logging && cache" ) );
+ QFormLayout *diagLay = nullptr;
+ mountFormInGroupBox( logGroup, &diagLay );
+ diagLay->setHorizontalSpacing( 12 );
+ diagLay->setVerticalSpacing( 8 );
+ diagLay->setFieldGrowthPolicy( QFormLayout::ExpandingFieldsGrow );
+ m_useTiming = new QCheckBox;
+ m_disableCache = new QCheckBox;
+ m_disableExiftool = new QCheckBox;
+ m_verbosity = new QComboBox;
+ m_verbosity->setObjectName( QStringLiteral( "guiVerbosity" ) );
+ m_verbosity->addItem( tr( "Quiet" ), 0 );
+ m_verbosity->addItem( tr( "Progress" ), 1 );
+ m_verbosity->addItem( tr( "Detailed" ), 2 );
+ m_verbosity->addItem( tr( "Solver report" ), 3 );
+ m_verbosity->addItem( tr( "Solver trace" ), 4 );
+ m_verbosity->setCurrentIndex( 0 );
+ m_verbosity->setMaximumWidth( kStdNumericFieldWidth * 2 );
+ m_verbosity->setToolTip(
+ tr( "How much is printed to the log and terminal. "
+ "Progress: per-step messages. Detailed: adds configuration summary. "
+ "Solver report: Ceres summary and IDT matrix. "
+ "Solver trace: also Ceres minimizer progress." ) );
+ diagLay->addRow( tr( "Log timing:" ), wrapCheckBoxForFormRow( m_useTiming ) );
+ diagLay->addRow( tr( "Disable cache:" ),
+ wrapCheckBoxForFormRow( m_disableCache ) );
+ diagLay->addRow( tr( "Disable exiftool:" ),
+ wrapCheckBoxForFormRow( m_disableExiftool ) );
+ diagLay->addRow( tr( "Verbosity:" ), m_verbosity );
+
+ odVBox->addWidget( wrapSettingsSectionTail( pathGroup ) );
+ odVBox->addWidget( wrapSettingsSectionTail( logGroup ) );
+ odVBox->addStretch();
+ m_settingsTabs->addTab( wrapScroll( odInner ), tr( "Output && diagnostics" ) );
+
+ auto *runRow = new QWidget;
+ auto *runLay = new QHBoxLayout( runRow );
+ runLay->setContentsMargins( 0, 0, 0, 0 );
+ m_convertButton = new QPushButton( tr( "Convert" ) );
+ m_convertButton->setObjectName( QStringLiteral( "guiConvertButton" ) );
+ m_convertButton->setDefault( true );
+ m_convertButton->setAutoDefault( true );
+ m_cancelButton = new QPushButton( tr( "Cancel" ) );
+ m_cancelButton->setObjectName( QStringLiteral( "guiCancelButton" ) );
+ m_cancelButton->setAutoDefault( false );
+ m_cancelButton->setEnabled( false );
+ m_progress = new QProgressBar;
+ m_progress->setObjectName( QStringLiteral( "guiProgressBar" ) );
+ m_progress->setRange( 0, 1 );
+ m_progress->setValue( 0 );
+ m_progress->setTextVisible( true );
+ m_progress->setMaximumHeight( 24 );
+ m_progress->setSizePolicy( QSizePolicy::Expanding, QSizePolicy::Fixed );
+ runLay->addWidget( m_convertButton );
+ runLay->addWidget( m_cancelButton );
+ runLay->addWidget( m_progress, 1 );
+ connect( m_convertButton, &QPushButton::clicked, this, &MainWindow::onConvert );
+ connect( m_cancelButton, &QPushButton::clicked, this, &MainWindow::onCancel );
+
+ auto *bottom = new QWidget;
+ auto *bottomLay = new QVBoxLayout( bottom );
+ bottomLay->setSpacing( 8 );
+ bottomLay->addWidget( runRow );
+ bottomLay->addWidget( m_log, 1 );
+
+ m_mainSplitter->addWidget( top );
+ m_mainSplitter->addWidget( m_settingsTabs );
+ m_mainSplitter->addWidget( bottom );
+ m_mainSplitter->setStretchFactor( 0, 0 );
+ m_mainSplitter->setStretchFactor( 1, 6 );
+ m_mainSplitter->setStretchFactor( 2, 2 );
+ m_mainSplitter->setSizes( { 120, 540, 200 } );
+
+ setCentralWidget( m_mainSplitter );
+
+ alignFormFieldTopForField( rawLay, blackLevelBlock );
+ alignFormFieldTopForField( rawLay, saturationBlock );
+
+ auto *fileMenu = menuBar()->addMenu( tr( "File" ) );
+ fileMenu->addAction( tr( "Add files…" ), this, &MainWindow::onAddFiles );
+ fileMenu->addAction( tr( "Add folder…" ), this, &MainWindow::onAddFolder );
+ fileMenu->addSeparator();
+ fileMenu->addAction( tr( "Quit" ), this, &QWidget::close );
+
+ auto *helpMenu = menuBar()->addMenu( tr( "Help" ) );
+ helpMenu->addAction( tr( "About" ), this, &MainWindow::onAbout );
+
+ resize( 1100, 820 );
+
+ loadPreferences();
+}
+
+rta::util::ImageConverter::Settings MainWindow::buildSettingsFromUi() const
+{
+ S s;
+
+ switch ( m_wbMethod->currentIndex() )
+ {
+ case 0:
+ s.WB_method = S::WBMethod::Metadata;
+ break;
+ case 1:
+ s.WB_method = S::WBMethod::Illuminant;
+ break;
+ case 2:
+ s.WB_method = S::WBMethod::Box;
+ break;
+ default:
+ s.WB_method = S::WBMethod::Custom;
+ break;
+ }
+
+ switch ( m_matrixMethod->currentIndex() )
+ {
+ case 0:
+ s.matrix_method = S::MatrixMethod::Auto;
+ break;
+ case 1:
+ s.matrix_method = S::MatrixMethod::Spectral;
+ break;
+ case 2:
+ s.matrix_method = S::MatrixMethod::Metadata;
+ break;
+ case 3:
+ s.matrix_method = S::MatrixMethod::Adobe;
+ break;
+ default:
+ s.matrix_method = S::MatrixMethod::Custom;
+ break;
+ }
+
+ s.illuminant = m_illuminant->text().toStdString();
+ if ( s.WB_method == S::WBMethod::Illuminant && s.illuminant.empty() )
+ {
+ s.illuminant = "D55";
+ }
+
+ for ( int i = 0; i < 4; ++i )
+ {
+ s.WB_box[i] = m_wbBox[i]->value();
+ }
+ for ( int i = 0; i < 4; ++i )
+ {
+ s.custom_WB[i] = static_cast( m_customWb[i]->value() );
+ }
+ for ( int r = 0; r < 3; ++r )
+ {
+ for ( int c = 0; c < 3; ++c )
+ {
+ s.custom_matrix[r][c] =
+ static_cast( m_customMat[r][c]->value() );
+ }
+ }
+
+ s.custom_camera_make = m_customCameraMake->text().toStdString();
+ s.custom_camera_model = m_customCameraModel->text().toStdString();
+ s.headroom = static_cast( m_headroom->value() );
+ s.scale = static_cast( m_scale->value() );
+
+ s.auto_bright = m_autoBright->isChecked();
+ s.adjust_maximum_threshold =
+ static_cast( m_adjustMaximum->value() );
+ s.black_level =
+ ( m_blackLevelFromMetadata != nullptr &&
+ m_blackLevelFromMetadata->isChecked() )
+ ? -1
+ : m_blackLevel->value();
+ s.saturation_level =
+ ( m_saturationFromMetadata != nullptr &&
+ m_saturationFromMetadata->isChecked() )
+ ? 0
+ : m_saturationLevel->value();
+ s.chromatic_aberration[0] =
+ static_cast( m_chromaR->value() );
+ s.chromatic_aberration[1] =
+ static_cast( m_chromaB->value() );
+ s.half_size = m_halfSize->isChecked();
+ s.highlight_mode = m_highlightMode->currentData().toInt();
+ for ( int i = 0; i < 4; ++i )
+ {
+ s.crop_box[i] = m_cropBox[i]->value();
+ }
+
+ switch ( m_cropMode->currentIndex() )
+ {
+ case 0:
+ s.crop_mode = S::CropMode::Off;
+ break;
+ case 1:
+ s.crop_mode = S::CropMode::Soft;
+ break;
+ default:
+ s.crop_mode = S::CropMode::Hard;
+ break;
+ }
+
+ s.flip = m_flip->currentData().toInt();
+ s.denoise_threshold = static_cast( m_denoise->value() );
+ s.demosaic_algorithm =
+ m_demosaic->currentText().toStdString();
+
+ s.overwrite = m_overwrite->isChecked();
+ s.create_dirs = m_createDirs->isChecked();
+ s.output_dir = m_outputDir->text().toStdString();
+
+ s.database_directories =
+ spectralDatabaseDirsFromLineEdit( m_dataDir->text() );
+
+#ifdef RTA_GUI_HAS_LENSFUN
+ if ( m_lensCorrAberration != nullptr )
+ {
+ s.lens_correction_types = S::LensCorrectionType::None;
+ if ( m_lensCorrAberration->isChecked() )
+ {
+ s.lens_correction_types |= S::LensCorrectionType::Aberration;
+ }
+ if ( m_lensCorrDistortion->isChecked() )
+ {
+ s.lens_correction_types |= S::LensCorrectionType::Distortion;
+ }
+ if ( m_lensCorrVignetting->isChecked() )
+ {
+ s.lens_correction_types |= S::LensCorrectionType::Vignetting;
+ }
+ s.require_lens_correction = m_requireLens->isChecked();
+ if ( m_lensMetadataOverride != nullptr
+ && m_lensMetadataOverride->isChecked() )
+ {
+ s.custom_lens_make = m_lensMake->text().toStdString();
+ s.custom_lens_model = m_lensModel->text().toStdString();
+ s.custom_aperture =
+ static_cast( m_lensAperture->value() );
+ s.custom_focal_length =
+ static_cast( m_lensFocal->value() );
+ s.custom_focus_distance =
+ static_cast( m_lensFocus->value() );
+ }
+ else
+ {
+ s.custom_lens_make.clear();
+ s.custom_lens_model.clear();
+ s.custom_aperture = 0.0f;
+ s.custom_focal_length = 0.0f;
+ s.custom_focus_distance = 0.0f;
+ }
+ }
+#endif
+
+ s.use_timing = m_useTiming->isChecked();
+ s.disable_cache = m_disableCache->isChecked();
+ s.disable_exiftool = m_disableExiftool->isChecked();
+ {
+ bool ok = false;
+ const int v = m_verbosity->currentData().toInt( &ok );
+ s.verbosity = ok ? v : 0;
+ }
+
+ return s;
+}
+
+void MainWindow::appendLog( const QString &line )
+{
+ m_log->append( line );
+}
+
+void MainWindow::setUiBusy( bool busy )
+{
+ m_convertButton->setEnabled( !busy );
+ m_cancelButton->setEnabled( busy );
+}
+
+void MainWindow::updateMatrixMethodDependentUi()
+{
+ // Combo order matches `buildSettingsFromUi`: …, custom (index 4).
+ const bool useCustomMatrix =
+ m_matrixMethod != nullptr && m_matrixMethod->currentIndex() == 4;
+ if ( m_customMatrixWrap != nullptr )
+ {
+ m_customMatrixWrap->setVisible( useCustomMatrix );
+ }
+ if ( m_customMatrixLabel != nullptr )
+ {
+ m_customMatrixLabel->setVisible( useCustomMatrix );
+ }
+}
+
+void MainWindow::updateWbMethodDependentUi()
+{
+ if ( m_wbMethod == nullptr )
+ {
+ return;
+ }
+ const int idx = m_wbMethod->currentIndex();
+ const bool showIlluminant = ( idx == 1 );
+ const bool showBoxRegion = ( idx == 2 );
+ const bool showCustomGains = ( idx == 3 );
+
+ auto showPair = []( QWidget *field, QLabel *lab, bool on ) {
+ if ( field != nullptr )
+ {
+ field->setVisible( on );
+ }
+ if ( lab != nullptr )
+ {
+ lab->setVisible( on );
+ }
+ };
+
+ showPair( m_wbIlluminantWrap, m_wbIlluminantLabel, showIlluminant );
+ showPair( m_wbBoxRegionWrap, m_wbBoxRegionLabel, showBoxRegion );
+ showPair( m_wbCustomGainsWrap, m_wbCustomGainsLabel, showCustomGains );
+}
+
+void MainWindow::updateBlackSaturationUi()
+{
+ if ( m_blackLevel != nullptr && m_blackLevelFromMetadata != nullptr )
+ {
+ m_blackLevel->setEnabled( !m_blackLevelFromMetadata->isChecked() );
+ }
+ if ( m_saturationLevel != nullptr && m_saturationFromMetadata != nullptr )
+ {
+ m_saturationLevel->setEnabled( !m_saturationFromMetadata->isChecked() );
+ }
+}
+
+void MainWindow::updateLensMetadataOverrideUi()
+{
+#ifdef RTA_GUI_HAS_LENSFUN
+ if ( m_lensMetadataOverride == nullptr || m_lensMake == nullptr )
+ {
+ return;
+ }
+ const bool on = m_lensMetadataOverride->isChecked();
+ m_lensMake->setEnabled( on );
+ m_lensModel->setEnabled( on );
+ m_lensAperture->setEnabled( on );
+ m_lensFocal->setEnabled( on );
+ m_lensFocus->setEnabled( on );
+#endif
+}
+
+void MainWindow::onAddFiles()
+{
+ const QStringList files = QFileDialog::getOpenFileNames( this );
+ for ( const QString &f: files )
+ {
+ m_fileList->addItem( f );
+ }
+}
+
+void MainWindow::onAddFolder()
+{
+ const QString dir = QFileDialog::getExistingDirectory( this );
+ if ( dir.isEmpty() )
+ {
+ return;
+ }
+ const std::vector paths = { dir.toStdString() };
+ const auto batches =
+ rta::util::collect_image_files( paths );
+ const QStringList flat = flattenBatches( batches );
+ for ( const QString &f: flat )
+ {
+ m_fileList->addItem( f );
+ }
+}
+
+void MainWindow::onRemoveSelected()
+{
+ for ( auto *item: m_fileList->selectedItems() )
+ {
+ delete m_fileList->takeItem( m_fileList->row( item ) );
+ }
+}
+
+void MainWindow::onClearFiles()
+{
+ m_fileList->clear();
+}
+
+void MainWindow::onBrowseOutput()
+{
+ const QString d = QFileDialog::getExistingDirectory( this );
+ if ( !d.isEmpty() )
+ {
+ m_outputDir->setText( d );
+ }
+}
+
+void MainWindow::onBrowseDataDir()
+{
+ const QString d = QFileDialog::getExistingDirectory( this );
+ if ( !d.isEmpty() )
+ {
+ m_dataDir->setText( d );
+ }
+}
+
+void MainWindow::onConvert()
+{
+ QStringList paths;
+ for ( int i = 0; i < m_fileList->count(); ++i )
+ {
+ paths << m_fileList->item( i )->text();
+ }
+ if ( paths.isEmpty() )
+ {
+ QMessageBox::warning(
+ this, tr( "rawtoaces" ), tr( "Add at least one input file." ) );
+ return;
+ }
+
+ const S settings = buildSettingsFromUi();
+
+ m_progress->setRange( 0, paths.size() );
+ m_progress->setValue( 0 );
+ appendLog( tr( "Starting batch (%1 files)…" ).arg( paths.size() ) );
+
+ auto *thread = new ConversionThread( this );
+ m_worker = thread;
+ connect( thread,
+ &ConversionThread::fileStarted,
+ this,
+ &MainWindow::onConversionFileStarted );
+ connect( thread,
+ &ConversionThread::fileFinished,
+ this,
+ &MainWindow::onConversionFileFinished );
+ connect( thread,
+ &ConversionThread::progress,
+ this,
+ &MainWindow::onConversionProgress );
+ connect( thread,
+ &ConversionThread::batchFinished,
+ this,
+ &MainWindow::onBatchFinished );
+ connect( thread, &QThread::finished, thread, &QObject::deleteLater );
+
+ thread->setJob( settings, paths );
+ setUiBusy( true );
+ thread->start();
+}
+
+void MainWindow::onCancel()
+{
+ if ( m_worker )
+ {
+ m_worker->requestCancel();
+ appendLog( tr( "Cancel requested after current file…" ) );
+ }
+}
+
+void MainWindow::onConversionFileStarted( int index, QString path )
+{
+ appendLog( tr( "[%1] %2" ).arg( index + 1 ).arg( path ) );
+}
+
+void MainWindow::onConversionFileFinished( int index, bool ok, QString message )
+{
+ if ( ok )
+ {
+ appendLog( tr( " OK" ) );
+ }
+ else
+ {
+ appendLog( tr( " FAILED: %1" ).arg( message ) );
+ }
+ Q_UNUSED( index );
+}
+
+void MainWindow::onConversionProgress( int done, int total )
+{
+ m_progress->setMaximum( total );
+ m_progress->setValue( done );
+}
+
+void MainWindow::onBatchFinished()
+{
+ appendLog( tr( "Batch finished." ) );
+ setUiBusy( false );
+ m_worker = nullptr;
+}
+
+void MainWindow::onAbout()
+{
+ QMessageBox::about(
+ this,
+ tr( "About rawtoaces" ),
+ tr( "rawtoaces GUI — ACES container output from camera RAW.\n"
+ "Settings match the same options as the rawtoaces image converter." ) );
+}
+
+namespace
+{
+constexpr auto kPrefsRootQLS = "rawtoaces_gui";
+constexpr int kPrefsFormatVersion = 1;
+
+void setComboBoxIndexClamped( QComboBox *comboBox, int index )
+{
+ if ( comboBox == nullptr || comboBox->count() <= 0 )
+ {
+ return;
+ }
+ comboBox->setCurrentIndex(
+ qBound( 0, index, comboBox->count() - 1 ) );
+}
+
+void setComboBoxCurrentByIntData( QComboBox *comboBox, int value )
+{
+ if ( comboBox == nullptr || comboBox->count() <= 0 )
+ {
+ return;
+ }
+ const int found = comboBox->findData( value );
+ comboBox->setCurrentIndex( found >= 0 ? found : 0 );
+}
+
+void setVerbosityComboFromLevel( QComboBox *comboBox, int level )
+{
+ if ( comboBox == nullptr )
+ {
+ return;
+ }
+ const int clamped = std::clamp( level, 0, 4 );
+ const int found = comboBox->findData( clamped );
+ comboBox->setCurrentIndex( found >= 0 ? found : 0 );
+}
+
+} // namespace
+
+void MainWindow::closeEvent( QCloseEvent *event )
+{
+ savePreferences();
+ QMainWindow::closeEvent( event );
+}
+
+void MainWindow::savePreferences() const
+{
+ QSettings settings;
+ settings.beginGroup( kPrefsRootQLS );
+ settings.setValue( QStringLiteral( "formatVersion" ),
+ kPrefsFormatVersion );
+
+ settings.beginGroup( QStringLiteral( "window" ) );
+ settings.setValue( QStringLiteral( "geometry" ), saveGeometry() );
+ if ( m_mainSplitter != nullptr )
+ {
+ settings.setValue( QStringLiteral( "splitter" ),
+ m_mainSplitter->saveState() );
+ }
+ if ( m_settingsTabs != nullptr )
+ {
+ settings.setValue( QStringLiteral( "settingsTab" ),
+ m_settingsTabs->currentIndex() );
+ }
+ settings.endGroup();
+
+ settings.beginGroup( QStringLiteral( "paths" ) );
+ if ( m_outputDir != nullptr )
+ {
+ settings.setValue( QStringLiteral( "outputDir" ),
+ m_outputDir->text() );
+ }
+ if ( m_dataDir != nullptr )
+ {
+ settings.setValue( QStringLiteral( "spectralDataOverride" ),
+ m_dataDir->text() );
+ }
+ settings.endGroup();
+
+ settings.beginGroup( QStringLiteral( "colour" ) );
+ if ( m_wbMethod != nullptr )
+ {
+ settings.setValue( QStringLiteral( "wbMethodIndex" ),
+ m_wbMethod->currentIndex() );
+ }
+ if ( m_illuminant != nullptr )
+ {
+ settings.setValue( QStringLiteral( "illuminant" ),
+ m_illuminant->text() );
+ }
+ for ( int i = 0; i < 4 && m_wbBox[i] != nullptr; ++i )
+ {
+ settings.setValue( QStringLiteral( "wbBox%1" ).arg( i ),
+ m_wbBox[i]->value() );
+ }
+ for ( int i = 0; i < 4 && m_customWb[i] != nullptr; ++i )
+ {
+ settings.setValue( QStringLiteral( "customWb%1" ).arg( i ),
+ m_customWb[i]->value() );
+ }
+ if ( m_matrixMethod != nullptr )
+ {
+ settings.setValue( QStringLiteral( "matrixMethodIndex" ),
+ m_matrixMethod->currentIndex() );
+ }
+ for ( int r = 0; r < 3; ++r )
+ {
+ for ( int c = 0; c < 3; ++c )
+ {
+ if ( m_customMat[r][c] != nullptr )
+ {
+ settings.setValue(
+ QStringLiteral( "customMatrix_%1_%2" ).arg( r ).arg( c ),
+ m_customMat[r][c]->value() );
+ }
+ }
+ }
+ if ( m_customCameraMake != nullptr )
+ {
+ settings.setValue( QStringLiteral( "customCameraMake" ),
+ m_customCameraMake->text() );
+ }
+ if ( m_customCameraModel != nullptr )
+ {
+ settings.setValue( QStringLiteral( "customCameraModel" ),
+ m_customCameraModel->text() );
+ }
+ if ( m_headroom != nullptr )
+ {
+ settings.setValue( QStringLiteral( "headroom" ),
+ m_headroom->value() );
+ }
+ if ( m_scale != nullptr )
+ {
+ settings.setValue( QStringLiteral( "scale" ), m_scale->value() );
+ }
+ settings.endGroup();
+
+ settings.beginGroup( QStringLiteral( "raw" ) );
+ if ( m_autoBright != nullptr )
+ {
+ settings.setValue( QStringLiteral( "autoBright" ),
+ m_autoBright->isChecked() );
+ }
+ if ( m_adjustMaximum != nullptr )
+ {
+ settings.setValue( QStringLiteral( "adjustMaximum" ),
+ m_adjustMaximum->value() );
+ }
+ if ( m_blackLevelFromMetadata != nullptr )
+ {
+ settings.setValue( QStringLiteral( "blackLevelFromMetadata" ),
+ m_blackLevelFromMetadata->isChecked() );
+ }
+ if ( m_blackLevel != nullptr )
+ {
+ settings.setValue( QStringLiteral( "blackLevel" ),
+ m_blackLevel->value() );
+ }
+ if ( m_saturationFromMetadata != nullptr )
+ {
+ settings.setValue( QStringLiteral( "saturationFromMetadata" ),
+ m_saturationFromMetadata->isChecked() );
+ }
+ if ( m_saturationLevel != nullptr )
+ {
+ settings.setValue( QStringLiteral( "saturationLevel" ),
+ m_saturationLevel->value() );
+ }
+ if ( m_chromaR != nullptr )
+ {
+ settings.setValue( QStringLiteral( "chromaR" ), m_chromaR->value() );
+ }
+ if ( m_chromaB != nullptr )
+ {
+ settings.setValue( QStringLiteral( "chromaB" ), m_chromaB->value() );
+ }
+ if ( m_halfSize != nullptr )
+ {
+ settings.setValue( QStringLiteral( "halfSize" ),
+ m_halfSize->isChecked() );
+ }
+ if ( m_highlightMode != nullptr )
+ {
+ settings.setValue( QStringLiteral( "highlightMode" ),
+ m_highlightMode->currentData().toInt() );
+ }
+ for ( int i = 0; i < 4 && m_cropBox[i] != nullptr; ++i )
+ {
+ settings.setValue( QStringLiteral( "cropBox%1" ).arg( i ),
+ m_cropBox[i]->value() );
+ }
+ if ( m_cropMode != nullptr )
+ {
+ settings.setValue( QStringLiteral( "cropModeIndex" ),
+ m_cropMode->currentIndex() );
+ }
+ if ( m_flip != nullptr )
+ {
+ settings.setValue( QStringLiteral( "flip" ),
+ m_flip->currentData().toInt() );
+ }
+ if ( m_denoise != nullptr )
+ {
+ settings.setValue( QStringLiteral( "denoise" ), m_denoise->value() );
+ }
+ if ( m_demosaic != nullptr )
+ {
+ settings.setValue( QStringLiteral( "demosaicAlgorithm" ),
+ m_demosaic->currentText() );
+ }
+ settings.endGroup();
+
+ settings.beginGroup( QStringLiteral( "output" ) );
+ if ( m_overwrite != nullptr )
+ {
+ settings.setValue( QStringLiteral( "overwrite" ),
+ m_overwrite->isChecked() );
+ }
+ if ( m_createDirs != nullptr )
+ {
+ settings.setValue( QStringLiteral( "createDirs" ),
+ m_createDirs->isChecked() );
+ }
+ settings.endGroup();
+
+ settings.beginGroup( QStringLiteral( "diagnostics" ) );
+ if ( m_useTiming != nullptr )
+ {
+ settings.setValue( QStringLiteral( "useTiming" ),
+ m_useTiming->isChecked() );
+ }
+ if ( m_disableCache != nullptr )
+ {
+ settings.setValue( QStringLiteral( "disableCache" ),
+ m_disableCache->isChecked() );
+ }
+ if ( m_disableExiftool != nullptr )
+ {
+ settings.setValue( QStringLiteral( "disableExiftool" ),
+ m_disableExiftool->isChecked() );
+ }
+ if ( m_verbosity != nullptr )
+ {
+ bool ok = false;
+ const int v = m_verbosity->currentData().toInt( &ok );
+ settings.setValue( QStringLiteral( "verbosity" ), ok ? v : 0 );
+ }
+ settings.endGroup();
+
+#ifdef RTA_GUI_HAS_LENSFUN
+ if ( m_lensCorrAberration != nullptr )
+ {
+ settings.beginGroup( QStringLiteral( "lens" ) );
+ settings.setValue( QStringLiteral( "corrAberration" ),
+ m_lensCorrAberration->isChecked() );
+ settings.setValue( QStringLiteral( "corrDistortion" ),
+ m_lensCorrDistortion->isChecked() );
+ settings.setValue( QStringLiteral( "corrVignetting" ),
+ m_lensCorrVignetting->isChecked() );
+ settings.setValue( QStringLiteral( "requireLens" ),
+ m_requireLens->isChecked() );
+ settings.setValue( QStringLiteral( "lensMetadataOverride" ),
+ m_lensMetadataOverride->isChecked() );
+ settings.setValue( QStringLiteral( "lensMake" ),
+ m_lensMake->text() );
+ settings.setValue( QStringLiteral( "lensModel" ),
+ m_lensModel->text() );
+ settings.setValue( QStringLiteral( "lensAperture" ),
+ m_lensAperture->value() );
+ settings.setValue( QStringLiteral( "lensFocal" ),
+ m_lensFocal->value() );
+ settings.setValue( QStringLiteral( "lensFocus" ),
+ m_lensFocus->value() );
+ settings.endGroup();
+ }
+#endif
+
+ settings.endGroup();
+ settings.sync();
+}
+
+void MainWindow::loadPreferences()
+{
+ QSettings settings;
+ settings.beginGroup( kPrefsRootQLS );
+ if ( !settings.contains( QStringLiteral( "formatVersion" ) ) )
+ {
+ settings.endGroup();
+ return;
+ }
+
+ settings.beginGroup( QStringLiteral( "window" ) );
+ const QByteArray geometry =
+ settings.value( QStringLiteral( "geometry" ) ).toByteArray();
+ if ( !geometry.isEmpty() )
+ {
+ restoreGeometry( geometry );
+ }
+ if ( m_mainSplitter != nullptr )
+ {
+ const QByteArray splitterState =
+ settings.value( QStringLiteral( "splitter" ) ).toByteArray();
+ if ( !splitterState.isEmpty() )
+ {
+ m_mainSplitter->restoreState( splitterState );
+ }
+ }
+ const int tabIndex =
+ settings.value( QStringLiteral( "settingsTab" ), 0 ).toInt();
+ settings.endGroup();
+
+ settings.beginGroup( QStringLiteral( "paths" ) );
+ if ( m_outputDir != nullptr )
+ {
+ m_outputDir->setText(
+ settings.value( QStringLiteral( "outputDir" ) ).toString() );
+ }
+ if ( m_dataDir != nullptr )
+ {
+ m_dataDir->setText(
+ settings.value( QStringLiteral( "spectralDataOverride" ) )
+ .toString() );
+ }
+ settings.endGroup();
+
+ settings.beginGroup( QStringLiteral( "colour" ) );
+ setComboBoxIndexClamped(
+ m_wbMethod,
+ settings.value( QStringLiteral( "wbMethodIndex" ), 0 ).toInt() );
+ if ( m_illuminant != nullptr )
+ {
+ m_illuminant->setText(
+ settings.value( QStringLiteral( "illuminant" ) ).toString() );
+ }
+ for ( int i = 0; i < 4 && m_wbBox[i] != nullptr; ++i )
+ {
+ m_wbBox[i]->setValue(
+ settings
+ .value( QStringLiteral( "wbBox%1" ).arg( i ), m_wbBox[i]->value() )
+ .toInt() );
+ }
+ for ( int i = 0; i < 4 && m_customWb[i] != nullptr; ++i )
+ {
+ m_customWb[i]->setValue(
+ settings
+ .value( QStringLiteral( "customWb%1" ).arg( i ),
+ m_customWb[i]->value() )
+ .toDouble() );
+ }
+ setComboBoxIndexClamped(
+ m_matrixMethod,
+ settings.value( QStringLiteral( "matrixMethodIndex" ), 0 ).toInt() );
+ for ( int r = 0; r < 3; ++r )
+ {
+ for ( int c = 0; c < 3; ++c )
+ {
+ if ( m_customMat[r][c] != nullptr )
+ {
+ m_customMat[r][c]->setValue(
+ settings
+ .value( QStringLiteral( "customMatrix_%1_%2" )
+ .arg( r )
+ .arg( c ),
+ m_customMat[r][c]->value() )
+ .toDouble() );
+ }
+ }
+ }
+ if ( m_customCameraMake != nullptr )
+ {
+ m_customCameraMake->setText(
+ settings.value( QStringLiteral( "customCameraMake" ) )
+ .toString() );
+ }
+ if ( m_customCameraModel != nullptr )
+ {
+ m_customCameraModel->setText(
+ settings.value( QStringLiteral( "customCameraModel" ) )
+ .toString() );
+ }
+ if ( m_headroom != nullptr )
+ {
+ m_headroom->setValue(
+ settings.value( QStringLiteral( "headroom" ), m_headroom->value() )
+ .toDouble() );
+ }
+ if ( m_scale != nullptr )
+ {
+ m_scale->setValue(
+ settings.value( QStringLiteral( "scale" ), m_scale->value() )
+ .toDouble() );
+ }
+ settings.endGroup();
+
+ settings.beginGroup( QStringLiteral( "raw" ) );
+ if ( m_autoBright != nullptr )
+ {
+ m_autoBright->setChecked(
+ settings.value( QStringLiteral( "autoBright" ),
+ m_autoBright->isChecked() )
+ .toBool() );
+ }
+ if ( m_adjustMaximum != nullptr )
+ {
+ m_adjustMaximum->setValue(
+ settings
+ .value( QStringLiteral( "adjustMaximum" ),
+ m_adjustMaximum->value() )
+ .toDouble() );
+ }
+ if ( m_blackLevelFromMetadata != nullptr )
+ {
+ m_blackLevelFromMetadata->setChecked(
+ settings
+ .value( QStringLiteral( "blackLevelFromMetadata" ),
+ m_blackLevelFromMetadata->isChecked() )
+ .toBool() );
+ }
+ if ( m_blackLevel != nullptr )
+ {
+ m_blackLevel->setValue(
+ settings
+ .value( QStringLiteral( "blackLevel" ), m_blackLevel->value() )
+ .toInt() );
+ }
+ if ( m_saturationFromMetadata != nullptr )
+ {
+ m_saturationFromMetadata->setChecked(
+ settings
+ .value( QStringLiteral( "saturationFromMetadata" ),
+ m_saturationFromMetadata->isChecked() )
+ .toBool() );
+ }
+ if ( m_saturationLevel != nullptr )
+ {
+ m_saturationLevel->setValue(
+ settings
+ .value( QStringLiteral( "saturationLevel" ),
+ m_saturationLevel->value() )
+ .toInt() );
+ }
+ if ( m_chromaR != nullptr )
+ {
+ m_chromaR->setValue(
+ settings.value( QStringLiteral( "chromaR" ), m_chromaR->value() )
+ .toDouble() );
+ }
+ if ( m_chromaB != nullptr )
+ {
+ m_chromaB->setValue(
+ settings.value( QStringLiteral( "chromaB" ), m_chromaB->value() )
+ .toDouble() );
+ }
+ if ( m_halfSize != nullptr )
+ {
+ m_halfSize->setChecked(
+ settings.value( QStringLiteral( "halfSize" ),
+ m_halfSize->isChecked() )
+ .toBool() );
+ }
+ if ( m_highlightMode != nullptr )
+ {
+ const int v = settings
+ .value( QStringLiteral( "highlightMode" ),
+ m_highlightMode->currentData().toInt() )
+ .toInt();
+ setComboBoxCurrentByIntData( m_highlightMode, std::clamp( v, 0, 9 ) );
+ }
+ for ( int i = 0; i < 4 && m_cropBox[i] != nullptr; ++i )
+ {
+ m_cropBox[i]->setValue(
+ settings
+ .value( QStringLiteral( "cropBox%1" ).arg( i ),
+ m_cropBox[i]->value() )
+ .toInt() );
+ }
+ setComboBoxIndexClamped(
+ m_cropMode,
+ settings.value( QStringLiteral( "cropModeIndex" ), 1 ).toInt() );
+ if ( m_flip != nullptr )
+ {
+ const int v =
+ settings
+ .value( QStringLiteral( "flip" ),
+ m_flip->currentData().toInt() )
+ .toInt();
+ setComboBoxCurrentByIntData( m_flip, std::clamp( v, 0, 8 ) );
+ }
+ if ( m_denoise != nullptr )
+ {
+ m_denoise->setValue(
+ settings.value( QStringLiteral( "denoise" ), m_denoise->value() )
+ .toDouble() );
+ }
+ if ( m_demosaic != nullptr )
+ {
+ const QString algo =
+ settings.value( QStringLiteral( "demosaicAlgorithm" ) )
+ .toString();
+ if ( !algo.isEmpty() )
+ {
+ const int demosaicIx = m_demosaic->findText( algo );
+ if ( demosaicIx >= 0 )
+ {
+ m_demosaic->setCurrentIndex( demosaicIx );
+ }
+ }
+ }
+ settings.endGroup();
+
+ settings.beginGroup( QStringLiteral( "output" ) );
+ if ( m_overwrite != nullptr )
+ {
+ m_overwrite->setChecked(
+ settings.value( QStringLiteral( "overwrite" ),
+ m_overwrite->isChecked() )
+ .toBool() );
+ }
+ if ( m_createDirs != nullptr )
+ {
+ m_createDirs->setChecked(
+ settings.value( QStringLiteral( "createDirs" ),
+ m_createDirs->isChecked() )
+ .toBool() );
+ }
+ settings.endGroup();
+
+ settings.beginGroup( QStringLiteral( "diagnostics" ) );
+ if ( m_useTiming != nullptr )
+ {
+ m_useTiming->setChecked(
+ settings.value( QStringLiteral( "useTiming" ),
+ m_useTiming->isChecked() )
+ .toBool() );
+ }
+ if ( m_disableCache != nullptr )
+ {
+ m_disableCache->setChecked(
+ settings.value( QStringLiteral( "disableCache" ),
+ m_disableCache->isChecked() )
+ .toBool() );
+ }
+ if ( m_disableExiftool != nullptr )
+ {
+ m_disableExiftool->setChecked(
+ settings.value( QStringLiteral( "disableExiftool" ),
+ m_disableExiftool->isChecked() )
+ .toBool() );
+ }
+ if ( m_verbosity != nullptr )
+ {
+ setVerbosityComboFromLevel(
+ m_verbosity,
+ settings.value( QStringLiteral( "verbosity" ), 0 ).toInt() );
+ }
+ settings.endGroup();
+
+#ifdef RTA_GUI_HAS_LENSFUN
+ if ( m_lensCorrAberration != nullptr )
+ {
+ settings.beginGroup( QStringLiteral( "lens" ) );
+ m_lensCorrAberration->setChecked(
+ settings.value( QStringLiteral( "corrAberration" ),
+ m_lensCorrAberration->isChecked() )
+ .toBool() );
+ m_lensCorrDistortion->setChecked(
+ settings.value( QStringLiteral( "corrDistortion" ),
+ m_lensCorrDistortion->isChecked() )
+ .toBool() );
+ m_lensCorrVignetting->setChecked(
+ settings.value( QStringLiteral( "corrVignetting" ),
+ m_lensCorrVignetting->isChecked() )
+ .toBool() );
+ m_requireLens->setChecked(
+ settings.value( QStringLiteral( "requireLens" ),
+ m_requireLens->isChecked() )
+ .toBool() );
+ m_lensMetadataOverride->setChecked(
+ settings
+ .value( QStringLiteral( "lensMetadataOverride" ), false )
+ .toBool() );
+ m_lensMake->setText(
+ settings.value( QStringLiteral( "lensMake" ) ).toString() );
+ m_lensModel->setText(
+ settings.value( QStringLiteral( "lensModel" ) ).toString() );
+ m_lensAperture->setValue(
+ settings
+ .value( QStringLiteral( "lensAperture" ),
+ m_lensAperture->value() )
+ .toDouble() );
+ m_lensFocal->setValue(
+ settings
+ .value( QStringLiteral( "lensFocal" ), m_lensFocal->value() )
+ .toDouble() );
+ m_lensFocus->setValue(
+ settings
+ .value( QStringLiteral( "lensFocus" ), m_lensFocus->value() )
+ .toDouble() );
+ settings.endGroup();
+ updateLensMetadataOverrideUi();
+ }
+#endif
+
+ settings.endGroup();
+
+ updateWbMethodDependentUi();
+ updateMatrixMethodDependentUi();
+ updateBlackSaturationUi();
+
+ if ( m_settingsTabs != nullptr )
+ {
+ const int tabCount = m_settingsTabs->count();
+ if ( tabCount > 0 )
+ {
+ m_settingsTabs->setCurrentIndex(
+ qBound( 0, tabIndex, tabCount - 1 ) );
+ }
+ }
+}
diff --git a/src/rawtoaces_gui/main_window.h b/src/rawtoaces_gui/main_window.h
new file mode 100644
index 00000000..24c2b1c2
--- /dev/null
+++ b/src/rawtoaces_gui/main_window.h
@@ -0,0 +1,131 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright Contributors to the rawtoaces Project.
+
+#pragma once
+
+#include
+#include
+
+#include
+
+class QListWidget;
+class QLineEdit;
+class QComboBox;
+class QSpinBox;
+class QDoubleSpinBox;
+class QCheckBox;
+class QPushButton;
+class QProgressBar;
+class QTextEdit;
+class QTabWidget;
+class QSplitter;
+class QCloseEvent;
+class QLabel;
+class ConversionThread;
+
+class MainWindow final : public QMainWindow
+{
+ Q_OBJECT
+
+public:
+ explicit MainWindow( QWidget *parent = nullptr );
+
+private slots:
+ void onAddFiles();
+ void onAddFolder();
+ void onRemoveSelected();
+ void onClearFiles();
+ void onBrowseOutput();
+ void onBrowseDataDir();
+ void onConvert();
+ void onCancel();
+ void onConversionFileStarted( int index, QString path );
+ void onConversionFileFinished( int index, bool ok, QString message );
+ void onConversionProgress( int done, int total );
+ void onBatchFinished();
+ void onAbout();
+ void updateMatrixMethodDependentUi();
+ void updateWbMethodDependentUi();
+ void updateBlackSaturationUi();
+ void updateLensMetadataOverrideUi();
+
+protected:
+ void closeEvent( QCloseEvent *event ) override;
+
+private:
+ rta::util::ImageConverter::Settings buildSettingsFromUi() const;
+ void appendLog( const QString &line );
+ void setUiBusy( bool busy );
+ void loadPreferences();
+ void savePreferences() const;
+
+ QListWidget *m_fileList = nullptr;
+ QLineEdit *m_outputDir = nullptr;
+ QLineEdit *m_dataDir = nullptr;
+ QPushButton *m_convertButton = nullptr;
+ QPushButton *m_cancelButton = nullptr;
+ QProgressBar *m_progress = nullptr;
+ QTextEdit *m_log = nullptr;
+
+ QSplitter *m_mainSplitter = nullptr;
+ QTabWidget *m_settingsTabs = nullptr;
+
+ QComboBox *m_wbMethod = nullptr;
+ QComboBox *m_matrixMethod = nullptr;
+ QLineEdit *m_illuminant = nullptr;
+ QSpinBox *m_wbBox[4]{};
+ QDoubleSpinBox *m_customWb[4]{};
+ QDoubleSpinBox *m_customMat[3][3]{};
+ QLineEdit *m_customCameraMake = nullptr;
+ QLineEdit *m_customCameraModel = nullptr;
+ QDoubleSpinBox *m_headroom = nullptr;
+ QDoubleSpinBox *m_scale = nullptr;
+
+ QCheckBox *m_autoBright = nullptr;
+ QDoubleSpinBox *m_adjustMaximum = nullptr;
+ QCheckBox *m_blackLevelFromMetadata = nullptr;
+ QSpinBox *m_blackLevel = nullptr;
+ QCheckBox *m_saturationFromMetadata = nullptr;
+ QSpinBox *m_saturationLevel = nullptr;
+ QDoubleSpinBox *m_chromaR = nullptr;
+ QDoubleSpinBox *m_chromaB = nullptr;
+ QCheckBox *m_halfSize = nullptr;
+ QComboBox *m_highlightMode = nullptr;
+ QSpinBox *m_cropBox[4]{};
+ QComboBox *m_cropMode = nullptr;
+ QComboBox *m_flip = nullptr;
+ QDoubleSpinBox *m_denoise = nullptr;
+ QComboBox *m_demosaic = nullptr;
+
+ QCheckBox *m_overwrite = nullptr;
+ QCheckBox *m_createDirs = nullptr;
+
+ QCheckBox *m_lensCorrAberration = nullptr;
+ QCheckBox *m_lensCorrDistortion = nullptr;
+ QCheckBox *m_lensCorrVignetting = nullptr;
+ QCheckBox *m_requireLens = nullptr;
+ QCheckBox *m_lensMetadataOverride = nullptr;
+ QLineEdit *m_lensMake = nullptr;
+ QLineEdit *m_lensModel = nullptr;
+ QDoubleSpinBox *m_lensAperture = nullptr;
+ QDoubleSpinBox *m_lensFocal = nullptr;
+ QDoubleSpinBox *m_lensFocus = nullptr;
+
+ QCheckBox *m_useTiming = nullptr;
+ QCheckBox *m_disableCache = nullptr;
+ QCheckBox *m_disableExiftool = nullptr;
+ QComboBox *m_verbosity = nullptr;
+
+ QPointer m_worker;
+
+ /// Shown only when matrix method is Custom (matches `ImageConverter` usage).
+ QWidget *m_customMatrixWrap = nullptr;
+ QLabel *m_customMatrixLabel = nullptr;
+
+ QWidget *m_wbIlluminantWrap = nullptr;
+ QLabel *m_wbIlluminantLabel = nullptr;
+ QWidget *m_wbBoxRegionWrap = nullptr;
+ QLabel *m_wbBoxRegionLabel = nullptr;
+ QWidget *m_wbCustomGainsWrap = nullptr;
+ QLabel *m_wbCustomGainsLabel = nullptr;
+};
diff --git a/src/rawtoaces_util/CMakeLists.txt b/src/rawtoaces_util/CMakeLists.txt
index d76d9a42..0e39c8c7 100644
--- a/src/rawtoaces_util/CMakeLists.txt
+++ b/src/rawtoaces_util/CMakeLists.txt
@@ -1,4 +1,4 @@
-cmake_minimum_required(VERSION 3.12)
+cmake_minimum_required(VERSION 3.16)
include_directories( "${CMAKE_CURRENT_SOURCE_DIR}" )
# Include coverage support if enabled
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index 46fbb1c8..f7d60511 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -1,4 +1,4 @@
-cmake_minimum_required(VERSION 3.12)
+cmake_minimum_required(VERSION 3.16)
# Enable coverage for all test targets if coverage is enabled
if( ENABLE_COVERAGE AND COVERAGE_SUPPORTED )
From 98fbfed9ab2baefa11d61697a16f58712a53147b Mon Sep 17 00:00:00 2001
From: Aleksandr Motsjonov
Date: Sun, 22 Mar 2026 12:56:38 +1100
Subject: [PATCH 2/9] formatt
Signed-off-by: Aleksandr Motsjonov
---
src/rawtoaces_gui/conversion_thread.cpp | 9 +-
src/rawtoaces_gui/conversion_thread.h | 4 +-
src/rawtoaces_gui/main.cpp | 4 +-
src/rawtoaces_gui/main_window.cpp | 728 ++++++++++++------------
src/rawtoaces_gui/main_window.h | 116 ++--
5 files changed, 439 insertions(+), 422 deletions(-)
diff --git a/src/rawtoaces_gui/conversion_thread.cpp b/src/rawtoaces_gui/conversion_thread.cpp
index 18515410..58a35fc1 100644
--- a/src/rawtoaces_gui/conversion_thread.cpp
+++ b/src/rawtoaces_gui/conversion_thread.cpp
@@ -3,7 +3,8 @@
#include "conversion_thread.h"
-ConversionThread::ConversionThread( QObject *parent ) : QThread( parent ) {}
+ConversionThread::ConversionThread( QObject *parent ) : QThread( parent )
+{}
void ConversionThread::setJob(
rta::util::ImageConverter::Settings settings, QStringList paths )
@@ -30,12 +31,12 @@ void ConversionThread::run()
}
const QString path = m_paths.at( i );
- emit fileStarted( i, path );
+ emit fileStarted( i, path );
rta::util::ImageConverter converter;
converter.settings = m_settings;
- const bool ok = converter.process_image( path.toStdString() );
- emit fileFinished(
+ const bool ok = converter.process_image( path.toStdString() );
+ emit fileFinished(
i, ok, QString::fromStdString( converter.last_error_message ) );
emit progress( i + 1, total );
}
diff --git a/src/rawtoaces_gui/conversion_thread.h b/src/rawtoaces_gui/conversion_thread.h
index 4a69657e..c89c54d5 100644
--- a/src/rawtoaces_gui/conversion_thread.h
+++ b/src/rawtoaces_gui/conversion_thread.h
@@ -17,8 +17,8 @@ class ConversionThread final : public QThread
public:
explicit ConversionThread( QObject *parent = nullptr );
- void setJob(
- rta::util::ImageConverter::Settings settings, QStringList paths );
+ void
+ setJob( rta::util::ImageConverter::Settings settings, QStringList paths );
void requestCancel();
diff --git a/src/rawtoaces_gui/main.cpp b/src/rawtoaces_gui/main.cpp
index 535e7c1f..c48353fb 100644
--- a/src/rawtoaces_gui/main.cpp
+++ b/src/rawtoaces_gui/main.cpp
@@ -6,9 +6,9 @@
#include
#ifndef WIN32
-#include
+# include
#else
-#include
+# include
#endif
int main( int argc, char *argv[] )
diff --git a/src/rawtoaces_gui/main_window.cpp b/src/rawtoaces_gui/main_window.cpp
index e5df7d89..e0779900 100644
--- a/src/rawtoaces_gui/main_window.cpp
+++ b/src/rawtoaces_gui/main_window.cpp
@@ -59,7 +59,8 @@ QWidget *wrapCheckBoxForFormRow( QCheckBox *checkBox )
lay->setAlignment( Qt::AlignVCenter );
lay->addWidget( checkBox, 0, Qt::AlignVCenter );
lay->addStretch( 1 );
- host->setSizePolicy( QSizePolicy::Expanding, QSizePolicy::MinimumExpanding );
+ host->setSizePolicy(
+ QSizePolicy::Expanding, QSizePolicy::MinimumExpanding );
return host;
}
@@ -71,8 +72,8 @@ constexpr int kStdNumericFieldWidth = 112;
/// because each `QGroupBox` carries style-dependent chrome; instead each
/// **logical** block (including one full two-column row) sits in
/// `wrapSettingsSectionTail` with a fixed bottom margin (`kSettingsSectionTailGap`).
-constexpr int kSettingsTabPageMargin = 6;
-constexpr int kSettingsSectionTailGap = 6;
+constexpr int kSettingsTabPageMargin = 6;
+constexpr int kSettingsSectionTailGap = 6;
void applySettingsTabPageMarginsOnly( QVBoxLayout *outerColumn )
{
@@ -80,10 +81,11 @@ void applySettingsTabPageMarginsOnly( QVBoxLayout *outerColumn )
{
return;
}
- outerColumn->setContentsMargins( kSettingsTabPageMargin,
- kSettingsTabPageMargin,
- kSettingsTabPageMargin,
- kSettingsTabPageMargin );
+ outerColumn->setContentsMargins(
+ kSettingsTabPageMargin,
+ kSettingsTabPageMargin,
+ kSettingsTabPageMargin,
+ kSettingsTabPageMargin );
}
void applySettingsTabPageChrome( QVBoxLayout *outerColumn )
@@ -120,8 +122,7 @@ void styleGroupBoxForSettingsRowPair( QGroupBox *box )
{
return;
}
- box->setSizePolicy( QSizePolicy::Preferred,
- QSizePolicy::MinimumExpanding );
+ box->setSizePolicy( QSizePolicy::Preferred, QSizePolicy::MinimumExpanding );
}
QWidget *wrapScroll( QWidget *inner )
@@ -168,12 +169,14 @@ void alignFormLabelTopForField( QFormLayout *form, QWidget *field )
}
for ( int r = 0; r < form->rowCount(); ++r )
{
- QLayoutItem *const fieldItem = form->itemAt( r, QFormLayout::FieldRole );
+ QLayoutItem *const fieldItem =
+ form->itemAt( r, QFormLayout::FieldRole );
if ( fieldItem == nullptr || fieldItem->widget() != field )
{
continue;
}
- QLayoutItem *const labelItem = form->itemAt( r, QFormLayout::LabelRole );
+ QLayoutItem *const labelItem =
+ form->itemAt( r, QFormLayout::LabelRole );
if ( labelItem != nullptr )
{
labelItem->setAlignment( Qt::AlignRight | Qt::AlignTop );
@@ -192,7 +195,8 @@ void alignFormFieldTopForField( QFormLayout *form, QWidget *field )
}
for ( int r = 0; r < form->rowCount(); ++r )
{
- QLayoutItem *const fieldItem = form->itemAt( r, QFormLayout::FieldRole );
+ QLayoutItem *const fieldItem =
+ form->itemAt( r, QFormLayout::FieldRole );
if ( fieldItem == nullptr || fieldItem->widget() != field )
{
continue;
@@ -237,10 +241,11 @@ void mountFormInGroupBoxFullWidth( QGroupBox *group, QFormLayout **outForm )
outer->addWidget( inner );
}
-void addLabeledSpinRows( QFormLayout *form,
- QSpinBox *boxes[4],
- const QStringList &rowLabels,
- int maxFieldWidth )
+void addLabeledSpinRows(
+ QFormLayout *form,
+ QSpinBox *boxes[4],
+ const QStringList &rowLabels,
+ int maxFieldWidth )
{
for ( int i = 0; i < 4; ++i )
{
@@ -252,7 +257,8 @@ void addLabeledSpinRows( QFormLayout *form,
}
}
-QStringList flattenBatches( const std::vector> &batches )
+QStringList
+flattenBatches( const std::vector> &batches )
{
QStringList out;
for ( const auto &batch: batches )
@@ -276,7 +282,7 @@ std::vector spectralDatabaseDirsFromLineEdit( const QString &text )
return {};
}
std::vector out;
- const QStringList parts = trimmed.split(
+ const QStringList parts = trimmed.split(
QRegularExpression( QStringLiteral( "[;:]" ) ), Qt::SkipEmptyParts );
for ( const QString &part: parts )
{
@@ -299,7 +305,7 @@ MainWindow::MainWindow( QWidget *parent ) : QMainWindow( parent )
m_mainSplitter = new QSplitter( Qt::Vertical );
- auto *top = new QWidget;
+ auto *top = new QWidget;
auto *topLay = new QVBoxLayout( top );
topLay->setSpacing( 6 );
@@ -308,7 +314,7 @@ MainWindow::MainWindow( QWidget *parent ) : QMainWindow( parent )
inputLay->setSpacing( 6 );
auto *fileRow = new QHBoxLayout;
- m_fileList = new QListWidget;
+ m_fileList = new QListWidget;
m_fileList->setObjectName( QStringLiteral( "guiFileList" ) );
m_fileList->setSelectionMode( QAbstractItemView::ExtendedSelection );
m_fileList->setMinimumHeight( 48 );
@@ -331,7 +337,8 @@ MainWindow::MainWindow( QWidget *parent ) : QMainWindow( parent )
connect( addFiles, &QPushButton::clicked, this, &MainWindow::onAddFiles );
connect( addFolder, &QPushButton::clicked, this, &MainWindow::onAddFolder );
- connect( removeBtn, &QPushButton::clicked, this, &MainWindow::onRemoveSelected );
+ connect(
+ removeBtn, &QPushButton::clicked, this, &MainWindow::onRemoveSelected );
connect( clearBtn, &QPushButton::clicked, this, &MainWindow::onClearFiles );
m_log = new QTextEdit;
@@ -350,8 +357,8 @@ MainWindow::MainWindow( QWidget *parent ) : QMainWindow( parent )
auto *rawOuter = new QVBoxLayout( rawInner );
applySettingsTabPageChrome( rawOuter );
- auto *rawLevels = new QGroupBox( tr( "Levels && exposure" ) );
- QFormLayout *rawLay = nullptr;
+ auto *rawLevels = new QGroupBox( tr( "Levels && exposure" ) );
+ QFormLayout *rawLay = nullptr;
mountFormInGroupBox( rawLevels, &rawLay );
rawLay->setHorizontalSpacing( 12 );
rawLay->setVerticalSpacing( 8 );
@@ -359,8 +366,8 @@ MainWindow::MainWindow( QWidget *parent ) : QMainWindow( parent )
// clips macOS indicators; expanding policy still respects fixed-width spins.
rawLay->setFieldGrowthPolicy( QFormLayout::ExpandingFieldsGrow );
m_autoBright = new QCheckBox;
- m_autoBright->setToolTip(
- tr( "LibRaw automatic brightening; other level options still apply." ) );
+ m_autoBright->setToolTip( tr(
+ "LibRaw automatic brightening; other level options still apply." ) );
m_adjustMaximum = new QDoubleSpinBox;
m_adjustMaximum->setRange( 0.0, 1.0 );
m_adjustMaximum->setDecimals( 4 );
@@ -382,9 +389,9 @@ MainWindow::MainWindow( QWidget *parent ) : QMainWindow( parent )
m_saturationFromMetadata = new QCheckBox( tr( "Take from metadata" ) );
m_saturationFromMetadata->setChecked( true );
- m_saturationFromMetadata->setToolTip(
- tr( "Use the saturation (clip / white) level from RAW metadata; when off, "
- "use the value below." ) );
+ m_saturationFromMetadata->setToolTip( tr(
+ "Use the saturation (clip / white) level from RAW metadata; when off, "
+ "use the value below." ) );
m_saturationLevel = new QSpinBox;
m_saturationLevel->setRange( 1, 2147483647 );
m_saturationLevel->setValue( 16383 );
@@ -407,23 +414,26 @@ MainWindow::MainWindow( QWidget *parent ) : QMainWindow( parent )
saturationVBox->addWidget( m_saturationFromMetadata );
saturationVBox->addWidget( m_saturationLevel );
- rawLay->addRow( tr( "Auto bright:" ), wrapCheckBoxForFormRow( m_autoBright ) );
+ rawLay->addRow(
+ tr( "Auto bright:" ), wrapCheckBoxForFormRow( m_autoBright ) );
rawLay->addRow( tr( "Adjust maximum threshold:" ), m_adjustMaximum );
rawLay->addRow( tr( "Black level:" ), blackLevelBlock );
rawLay->addRow( tr( "Saturation level:" ), saturationBlock );
alignFormLabelTopForField( rawLay, blackLevelBlock );
alignFormLabelTopForField( rawLay, saturationBlock );
- connect( m_blackLevelFromMetadata,
- &QCheckBox::toggled,
- this,
- &MainWindow::updateBlackSaturationUi );
- connect( m_saturationFromMetadata,
- &QCheckBox::toggled,
- this,
- &MainWindow::updateBlackSaturationUi );
+ connect(
+ m_blackLevelFromMetadata,
+ &QCheckBox::toggled,
+ this,
+ &MainWindow::updateBlackSaturationUi );
+ connect(
+ m_saturationFromMetadata,
+ &QCheckBox::toggled,
+ this,
+ &MainWindow::updateBlackSaturationUi );
updateBlackSaturationUi();
- auto *rawChroma = new QGroupBox( tr( "Chromatic aberration && size" ) );
+ auto *rawChroma = new QGroupBox( tr( "Chromatic aberration && size" ) );
QFormLayout *chForm = nullptr;
mountFormInGroupBox( rawChroma, &chForm );
chForm->setHorizontalSpacing( 12 );
@@ -437,7 +447,7 @@ MainWindow::MainWindow( QWidget *parent ) : QMainWindow( parent )
m_chromaB->setDecimals( 4 );
m_chromaR->setValue( 1.0 );
m_chromaB->setValue( 1.0 );
- m_halfSize = new QCheckBox;
+ m_halfSize = new QCheckBox;
m_highlightMode = new QComboBox;
m_highlightMode->setObjectName( QStringLiteral( "guiHighlightMode" ) );
m_highlightMode->addItem( tr( "0 — Clip" ), 0 );
@@ -456,11 +466,11 @@ MainWindow::MainWindow( QWidget *parent ) : QMainWindow( parent )
setFieldMaxWidth( m_chromaB, kStdNumericFieldWidth );
chForm->addRow( tr( "Red channel multiplier:" ), m_chromaR );
chForm->addRow( tr( "Blue channel multiplier:" ), m_chromaB );
- chForm->addRow( tr( "Half-size decode:" ),
- wrapCheckBoxForFormRow( m_halfSize ) );
+ chForm->addRow(
+ tr( "Half-size decode:" ), wrapCheckBoxForFormRow( m_halfSize ) );
chForm->addRow( tr( "Highlight mode:" ), m_highlightMode );
- auto *rawCrop = new QGroupBox( tr( "Crop, orientation && denoise" ) );
+ auto *rawCrop = new QGroupBox( tr( "Crop, orientation && denoise" ) );
QFormLayout *crForm = nullptr;
mountFormInGroupBox( rawCrop, &crForm );
crForm->setHorizontalSpacing( 12 );
@@ -477,8 +487,7 @@ MainWindow::MainWindow( QWidget *parent ) : QMainWindow( parent )
cropRegionForm->setLabelAlignment( Qt::AlignRight | Qt::AlignTop );
cropRegionForm->setContentsMargins( 0, 0, 0, 0 );
cropRegionForm->setHorizontalSpacing( 12 );
- cropRegionForm->setFieldGrowthPolicy(
- QFormLayout::FieldsStayAtSizeHint );
+ cropRegionForm->setFieldGrowthPolicy( QFormLayout::FieldsStayAtSizeHint );
addLabeledSpinRows(
cropRegionForm,
m_cropBox,
@@ -514,20 +523,21 @@ MainWindow::MainWindow( QWidget *parent ) : QMainWindow( parent )
crForm->addRow( tr( "Denoise threshold:" ), m_denoise );
setFieldMaxWidth( m_denoise, kStdNumericFieldWidth );
- auto *rawDemo = new QGroupBox( tr( "Demosaic" ) );
- QFormLayout *dmForm = nullptr;
+ auto *rawDemo = new QGroupBox( tr( "Demosaic" ) );
+ QFormLayout *dmForm = nullptr;
mountFormInGroupBox( rawDemo, &dmForm );
dmForm->setHorizontalSpacing( 12 );
dmForm->setFieldGrowthPolicy( QFormLayout::FieldsStayAtSizeHint );
- m_demosaic = new QComboBox;
+ m_demosaic = new QComboBox;
const QStringList demosaicNames = {
QStringLiteral( "linear" ), QStringLiteral( "VNG" ),
- QStringLiteral( "PPG" ), QStringLiteral( "AHD" ),
- QStringLiteral( "DCB" ), QStringLiteral( "AHD-Mod" ),
- QStringLiteral( "AFD" ), QStringLiteral( "VCD" ),
- QStringLiteral( "Mixed" ), QStringLiteral( "LMMSE" ),
- QStringLiteral( "AMaZE" ), QStringLiteral( "DHT" ),
- QStringLiteral( "AAHD" ), QStringLiteral( "AHD" ) };
+ QStringLiteral( "PPG" ), QStringLiteral( "AHD" ),
+ QStringLiteral( "DCB" ), QStringLiteral( "AHD-Mod" ),
+ QStringLiteral( "AFD" ), QStringLiteral( "VCD" ),
+ QStringLiteral( "Mixed" ), QStringLiteral( "LMMSE" ),
+ QStringLiteral( "AMaZE" ), QStringLiteral( "DHT" ),
+ QStringLiteral( "AAHD" ), QStringLiteral( "AHD" )
+ };
m_demosaic->addItems( demosaicNames );
m_demosaic->setMaximumWidth( kStdNumericFieldWidth * 2 );
dmForm->addRow( tr( "Algorithm:" ), m_demosaic );
@@ -561,7 +571,7 @@ MainWindow::MainWindow( QWidget *parent ) : QMainWindow( parent )
auto *colourOuter = new QVBoxLayout( colourInner );
applySettingsTabPageChrome( colourOuter );
- auto *grpSpectral = new QGroupBox( tr( "Spectral data" ) );
+ auto *grpSpectral = new QGroupBox( tr( "Spectral data" ) );
QFormLayout *spectralForm = nullptr;
mountFormInGroupBoxFullWidth( grpSpectral, &spectralForm );
spectralForm->setHorizontalSpacing( 12 );
@@ -570,10 +580,10 @@ MainWindow::MainWindow( QWidget *parent ) : QMainWindow( parent )
m_dataDir = new QLineEdit;
m_dataDir->setObjectName( QStringLiteral( "guiDataDir" ) );
m_dataDir->setPlaceholderText( tr( "Empty = default search paths" ) );
- m_dataDir->setToolTip(
- tr( "Override directories for camera / illuminant spectral data. "
- "Separate multiple paths with ';' or ':'. Empty uses library defaults "
- "and environment." ) );
+ m_dataDir->setToolTip( tr(
+ "Override directories for camera / illuminant spectral data. "
+ "Separate multiple paths with ';' or ':'. Empty uses library defaults "
+ "and environment." ) );
auto *dataBrowse = new QPushButton( tr( "Browse…" ) );
auto *dataWrap = new QWidget;
dataWrap->setSizePolicy( QSizePolicy::Expanding, QSizePolicy::Preferred );
@@ -583,9 +593,10 @@ MainWindow::MainWindow( QWidget *parent ) : QMainWindow( parent )
dataHBox->addWidget( m_dataDir, 1 );
dataHBox->addWidget( dataBrowse );
spectralForm->addRow( tr( "Data directory:" ), dataWrap );
- connect( dataBrowse, &QPushButton::clicked, this, &MainWindow::onBrowseDataDir );
+ connect(
+ dataBrowse, &QPushButton::clicked, this, &MainWindow::onBrowseDataDir );
- auto *grpWb = new QGroupBox( tr( "White balance" ) );
+ auto *grpWb = new QGroupBox( tr( "White balance" ) );
QFormLayout *wbForm = nullptr;
mountFormInGroupBox( grpWb, &wbForm );
wbForm->setHorizontalSpacing( 12 );
@@ -599,7 +610,7 @@ MainWindow::MainWindow( QWidget *parent ) : QMainWindow( parent )
m_illuminant = new QLineEdit;
m_illuminant->setPlaceholderText( tr( "e.g. D55, 3200K" ) );
m_wbIlluminantWrap = new QWidget;
- auto *illumHBox = new QHBoxLayout( m_wbIlluminantWrap );
+ auto *illumHBox = new QHBoxLayout( m_wbIlluminantWrap );
illumHBox->setContentsMargins( 0, 0, 0, 0 );
illumHBox->addWidget( m_illuminant, 1 );
for ( int i = 0; i < 4; ++i )
@@ -608,7 +619,7 @@ MainWindow::MainWindow( QWidget *parent ) : QMainWindow( parent )
m_wbBox[i]->setRange( -1000000000, 1000000000 );
}
m_wbBoxRegionWrap = new QWidget;
- auto *wbBoxForm = new QFormLayout( m_wbBoxRegionWrap );
+ auto *wbBoxForm = new QFormLayout( m_wbBoxRegionWrap );
polishFormLayout( wbBoxForm );
wbBoxForm->setLabelAlignment( Qt::AlignRight | Qt::AlignTop );
wbBoxForm->setContentsMargins( 0, 0, 0, 0 );
@@ -621,7 +632,7 @@ MainWindow::MainWindow( QWidget *parent ) : QMainWindow( parent )
kStdNumericFieldWidth );
m_wbCustomGainsWrap = new QWidget;
- auto *cwbForm = new QFormLayout( m_wbCustomGainsWrap );
+ auto *cwbForm = new QFormLayout( m_wbCustomGainsWrap );
polishFormLayout( cwbForm );
cwbForm->setLabelAlignment( Qt::AlignRight | Qt::AlignTop );
cwbForm->setContentsMargins( 0, 0, 0, 0 );
@@ -652,13 +663,14 @@ MainWindow::MainWindow( QWidget *parent ) : QMainWindow( parent )
qobject_cast( wbForm->labelForField( m_wbBoxRegionWrap ) );
m_wbCustomGainsLabel =
qobject_cast( wbForm->labelForField( m_wbCustomGainsWrap ) );
- connect( m_wbMethod,
- &QComboBox::currentIndexChanged,
- this,
- &MainWindow::updateWbMethodDependentUi );
+ connect(
+ m_wbMethod,
+ &QComboBox::currentIndexChanged,
+ this,
+ &MainWindow::updateWbMethodDependentUi );
updateWbMethodDependentUi();
- auto *grpMat = new QGroupBox( tr( "Colour matrix && camera" ) );
+ auto *grpMat = new QGroupBox( tr( "Colour matrix && camera" ) );
QFormLayout *matForm = nullptr;
mountFormInGroupBox( grpMat, &matForm );
matForm->setHorizontalSpacing( 12 );
@@ -691,16 +703,17 @@ MainWindow::MainWindow( QWidget *parent ) : QMainWindow( parent )
matForm->addRow( tr( "Custom 3×3 matrix:" ), m_customMatrixWrap );
m_customMatrixLabel =
qobject_cast( matForm->labelForField( m_customMatrixWrap ) );
- connect( m_matrixMethod,
- &QComboBox::currentIndexChanged,
- this,
- &MainWindow::updateMatrixMethodDependentUi );
+ connect(
+ m_matrixMethod,
+ &QComboBox::currentIndexChanged,
+ this,
+ &MainWindow::updateMatrixMethodDependentUi );
updateMatrixMethodDependentUi();
m_customCameraMake = new QLineEdit;
m_customCameraModel = new QLineEdit;
- auto *makeWrap = new QWidget;
- auto *makeHBox = new QHBoxLayout( makeWrap );
+ auto *makeWrap = new QWidget;
+ auto *makeHBox = new QHBoxLayout( makeWrap );
makeHBox->setContentsMargins( 0, 0, 0, 0 );
makeHBox->addWidget( m_customCameraMake, 1 );
auto *modelWrap = new QWidget;
@@ -710,7 +723,7 @@ MainWindow::MainWindow( QWidget *parent ) : QMainWindow( parent )
matForm->addRow( tr( "Override camera make:" ), makeWrap );
matForm->addRow( tr( "Override camera model:" ), modelWrap );
- auto *grpTone = new QGroupBox( tr( "Tone && scale" ) );
+ auto *grpTone = new QGroupBox( tr( "Tone && scale" ) );
QFormLayout *toneForm = nullptr;
mountFormInGroupBox( grpTone, &toneForm );
toneForm->setHorizontalSpacing( 12 );
@@ -748,8 +761,8 @@ MainWindow::MainWindow( QWidget *parent ) : QMainWindow( parent )
auto *lensInner = new QWidget;
auto *lensOuter = new QVBoxLayout( lensInner );
applySettingsTabPageChrome( lensOuter );
- auto *lensFlagsBox = new QGroupBox( tr( "Lens corrections" ) );
- QFormLayout *lensLay = nullptr;
+ auto *lensFlagsBox = new QGroupBox( tr( "Lens corrections" ) );
+ QFormLayout *lensLay = nullptr;
mountFormInGroupBox( lensFlagsBox, &lensLay );
lensLay->setHorizontalSpacing( 12 );
lensLay->setVerticalSpacing( 8 );
@@ -766,14 +779,15 @@ MainWindow::MainWindow( QWidget *parent ) : QMainWindow( parent )
lensCorrVBox->addWidget( m_lensCorrDistortion );
lensCorrVBox->addWidget( m_lensCorrVignetting );
lensLay->addRow( tr( "Correction types:" ), lensCorrStack );
- lensLay->addRow( tr( "Fail if correction unavailable:" ),
- wrapCheckBoxForFormRow( m_requireLens ) );
+ lensLay->addRow(
+ tr( "Fail if correction unavailable:" ),
+ wrapCheckBoxForFormRow( m_requireLens ) );
// Tall field: align label with the first checkbox, not vertical center of stack.
alignFormLabelTopForField( lensLay, lensCorrStack );
alignFormFieldTopForField( lensLay, lensCorrStack );
- auto *lensOverride = new QGroupBox( tr( "Lens metadata" ) );
- QFormLayout *ovLay = nullptr;
+ auto *lensOverride = new QGroupBox( tr( "Lens metadata" ) );
+ QFormLayout *ovLay = nullptr;
mountFormInGroupBox( lensOverride, &ovLay );
ovLay->setHorizontalSpacing( 12 );
ovLay->setVerticalSpacing( 8 );
@@ -785,14 +799,15 @@ MainWindow::MainWindow( QWidget *parent ) : QMainWindow( parent )
tr( "When enabled, use the make, model, aperture, focal length, and "
"focus distance below for lens correction; when off, converter "
"defaults apply (same as leaving CLI overrides unset)." ) );
- ovLay->addRow( tr( "Override:" ),
- wrapCheckBoxForFormRow( m_lensMetadataOverride ) );
- connect( m_lensMetadataOverride,
- &QCheckBox::toggled,
- this,
- &MainWindow::updateLensMetadataOverrideUi );
- m_lensMake = new QLineEdit;
- m_lensModel = new QLineEdit;
+ ovLay->addRow(
+ tr( "Override:" ), wrapCheckBoxForFormRow( m_lensMetadataOverride ) );
+ connect(
+ m_lensMetadataOverride,
+ &QCheckBox::toggled,
+ this,
+ &MainWindow::updateLensMetadataOverrideUi );
+ m_lensMake = new QLineEdit;
+ m_lensModel = new QLineEdit;
m_lensAperture = new QDoubleSpinBox;
m_lensAperture->setRange( 0.0, 1.0e6 );
m_lensAperture->setDecimals( 3 );
@@ -832,16 +847,16 @@ MainWindow::MainWindow( QWidget *parent ) : QMainWindow( parent )
lensOuter->addStretch();
m_settingsTabs->addTab( wrapScroll( lensInner ), tr( "Lens" ) );
#else
- m_lensCorrAberration = nullptr;
- m_lensCorrDistortion = nullptr;
- m_lensCorrVignetting = nullptr;
- m_requireLens = nullptr;
+ m_lensCorrAberration = nullptr;
+ m_lensCorrDistortion = nullptr;
+ m_lensCorrVignetting = nullptr;
+ m_requireLens = nullptr;
m_lensMetadataOverride = nullptr;
- m_lensMake = nullptr;
- m_lensModel = nullptr;
- m_lensAperture = nullptr;
- m_lensFocal = nullptr;
- m_lensFocus = nullptr;
+ m_lensMake = nullptr;
+ m_lensModel = nullptr;
+ m_lensAperture = nullptr;
+ m_lensFocal = nullptr;
+ m_lensFocus = nullptr;
#endif
// --- Output & diagnostics tab (last; paths, write flags, logging) ---
@@ -849,8 +864,8 @@ MainWindow::MainWindow( QWidget *parent ) : QMainWindow( parent )
auto *odVBox = new QVBoxLayout( odInner );
applySettingsTabPageChrome( odVBox );
- auto *pathGroup = new QGroupBox( tr( "Output" ) );
- QFormLayout *pathForm = nullptr;
+ auto *pathGroup = new QGroupBox( tr( "Output" ) );
+ QFormLayout *pathForm = nullptr;
mountFormInGroupBoxFullWidth( pathGroup, &pathForm );
pathForm->setHorizontalSpacing( 12 );
pathForm->setVerticalSpacing( 8 );
@@ -860,19 +875,20 @@ MainWindow::MainWindow( QWidget *parent ) : QMainWindow( parent )
m_outputDir->setObjectName( QStringLiteral( "guiOutputDir" ) );
m_outputDir->setPlaceholderText(
tr( "Empty = same folder as each RAW; or set a subfolder / path" ) );
- m_outputDir->setToolTip(
- tr( "Leave empty to write .exr next to the source file. "
- "If set, output paths are resolved under each input file’s directory." ) );
+ m_outputDir->setToolTip( tr(
+ "Leave empty to write .exr next to the source file. "
+ "If set, output paths are resolved under each input file’s directory." ) );
auto *outBrowse = new QPushButton( tr( "Browse…" ) );
auto *outWrap = new QWidget;
outWrap->setSizePolicy( QSizePolicy::Expanding, QSizePolicy::Preferred );
- auto *outHBox = new QHBoxLayout( outWrap );
+ auto *outHBox = new QHBoxLayout( outWrap );
outHBox->setContentsMargins( 0, 0, 0, 0 );
outHBox->setSpacing( 8 );
outHBox->addWidget( m_outputDir, 1 );
outHBox->addWidget( outBrowse );
pathForm->addRow( tr( "Output directory:" ), outWrap );
- connect( outBrowse, &QPushButton::clicked, this, &MainWindow::onBrowseOutput );
+ connect(
+ outBrowse, &QPushButton::clicked, this, &MainWindow::onBrowseOutput );
m_overwrite = new QCheckBox;
m_createDirs = new QCheckBox;
@@ -881,13 +897,15 @@ MainWindow::MainWindow( QWidget *parent ) : QMainWindow( parent )
m_createDirs->setToolTip(
tr( "When an output directory is set, create missing parent folders "
"if they do not exist." ) );
- pathForm->addRow( tr( "Overwrite existing files:" ),
- wrapCheckBoxForFormRow( m_overwrite ) );
- pathForm->addRow( tr( "Create missing directories:" ),
- wrapCheckBoxForFormRow( m_createDirs ) );
-
- auto *logGroup = new QGroupBox( tr( "Logging && cache" ) );
- QFormLayout *diagLay = nullptr;
+ pathForm->addRow(
+ tr( "Overwrite existing files:" ),
+ wrapCheckBoxForFormRow( m_overwrite ) );
+ pathForm->addRow(
+ tr( "Create missing directories:" ),
+ wrapCheckBoxForFormRow( m_createDirs ) );
+
+ auto *logGroup = new QGroupBox( tr( "Logging && cache" ) );
+ QFormLayout *diagLay = nullptr;
mountFormInGroupBox( logGroup, &diagLay );
diagLay->setHorizontalSpacing( 12 );
diagLay->setVerticalSpacing( 8 );
@@ -895,7 +913,7 @@ MainWindow::MainWindow( QWidget *parent ) : QMainWindow( parent )
m_useTiming = new QCheckBox;
m_disableCache = new QCheckBox;
m_disableExiftool = new QCheckBox;
- m_verbosity = new QComboBox;
+ m_verbosity = new QComboBox;
m_verbosity->setObjectName( QStringLiteral( "guiVerbosity" ) );
m_verbosity->addItem( tr( "Quiet" ), 0 );
m_verbosity->addItem( tr( "Progress" ), 1 );
@@ -904,22 +922,25 @@ MainWindow::MainWindow( QWidget *parent ) : QMainWindow( parent )
m_verbosity->addItem( tr( "Solver trace" ), 4 );
m_verbosity->setCurrentIndex( 0 );
m_verbosity->setMaximumWidth( kStdNumericFieldWidth * 2 );
- m_verbosity->setToolTip(
- tr( "How much is printed to the log and terminal. "
- "Progress: per-step messages. Detailed: adds configuration summary. "
- "Solver report: Ceres summary and IDT matrix. "
- "Solver trace: also Ceres minimizer progress." ) );
- diagLay->addRow( tr( "Log timing:" ), wrapCheckBoxForFormRow( m_useTiming ) );
- diagLay->addRow( tr( "Disable cache:" ),
- wrapCheckBoxForFormRow( m_disableCache ) );
- diagLay->addRow( tr( "Disable exiftool:" ),
- wrapCheckBoxForFormRow( m_disableExiftool ) );
+ m_verbosity->setToolTip( tr(
+ "How much is printed to the log and terminal. "
+ "Progress: per-step messages. Detailed: adds configuration summary. "
+ "Solver report: Ceres summary and IDT matrix. "
+ "Solver trace: also Ceres minimizer progress." ) );
+ diagLay->addRow(
+ tr( "Log timing:" ), wrapCheckBoxForFormRow( m_useTiming ) );
+ diagLay->addRow(
+ tr( "Disable cache:" ), wrapCheckBoxForFormRow( m_disableCache ) );
+ diagLay->addRow(
+ tr( "Disable exiftool:" ),
+ wrapCheckBoxForFormRow( m_disableExiftool ) );
diagLay->addRow( tr( "Verbosity:" ), m_verbosity );
odVBox->addWidget( wrapSettingsSectionTail( pathGroup ) );
odVBox->addWidget( wrapSettingsSectionTail( logGroup ) );
odVBox->addStretch();
- m_settingsTabs->addTab( wrapScroll( odInner ), tr( "Output && diagnostics" ) );
+ m_settingsTabs->addTab(
+ wrapScroll( odInner ), tr( "Output && diagnostics" ) );
auto *runRow = new QWidget;
auto *runLay = new QHBoxLayout( runRow );
@@ -942,8 +963,10 @@ MainWindow::MainWindow( QWidget *parent ) : QMainWindow( parent )
runLay->addWidget( m_convertButton );
runLay->addWidget( m_cancelButton );
runLay->addWidget( m_progress, 1 );
- connect( m_convertButton, &QPushButton::clicked, this, &MainWindow::onConvert );
- connect( m_cancelButton, &QPushButton::clicked, this, &MainWindow::onCancel );
+ connect(
+ m_convertButton, &QPushButton::clicked, this, &MainWindow::onConvert );
+ connect(
+ m_cancelButton, &QPushButton::clicked, this, &MainWindow::onCancel );
auto *bottom = new QWidget;
auto *bottomLay = new QVBoxLayout( bottom );
@@ -984,37 +1007,19 @@ rta::util::ImageConverter::Settings MainWindow::buildSettingsFromUi() const
switch ( m_wbMethod->currentIndex() )
{
- case 0:
- s.WB_method = S::WBMethod::Metadata;
- break;
- case 1:
- s.WB_method = S::WBMethod::Illuminant;
- break;
- case 2:
- s.WB_method = S::WBMethod::Box;
- break;
- default:
- s.WB_method = S::WBMethod::Custom;
- break;
+ case 0: s.WB_method = S::WBMethod::Metadata; break;
+ case 1: s.WB_method = S::WBMethod::Illuminant; break;
+ case 2: s.WB_method = S::WBMethod::Box; break;
+ default: s.WB_method = S::WBMethod::Custom; break;
}
switch ( m_matrixMethod->currentIndex() )
{
- case 0:
- s.matrix_method = S::MatrixMethod::Auto;
- break;
- case 1:
- s.matrix_method = S::MatrixMethod::Spectral;
- break;
- case 2:
- s.matrix_method = S::MatrixMethod::Metadata;
- break;
- case 3:
- s.matrix_method = S::MatrixMethod::Adobe;
- break;
- default:
- s.matrix_method = S::MatrixMethod::Custom;
- break;
+ case 0: s.matrix_method = S::MatrixMethod::Auto; break;
+ case 1: s.matrix_method = S::MatrixMethod::Spectral; break;
+ case 2: s.matrix_method = S::MatrixMethod::Metadata; break;
+ case 3: s.matrix_method = S::MatrixMethod::Adobe; break;
+ default: s.matrix_method = S::MatrixMethod::Custom; break;
}
s.illuminant = m_illuminant->text().toStdString();
@@ -1045,25 +1050,20 @@ rta::util::ImageConverter::Settings MainWindow::buildSettingsFromUi() const
s.headroom = static_cast( m_headroom->value() );
s.scale = static_cast( m_scale->value() );
- s.auto_bright = m_autoBright->isChecked();
- s.adjust_maximum_threshold =
- static_cast( m_adjustMaximum->value() );
- s.black_level =
- ( m_blackLevelFromMetadata != nullptr &&
- m_blackLevelFromMetadata->isChecked() )
- ? -1
- : m_blackLevel->value();
- s.saturation_level =
- ( m_saturationFromMetadata != nullptr &&
- m_saturationFromMetadata->isChecked() )
- ? 0
- : m_saturationLevel->value();
- s.chromatic_aberration[0] =
- static_cast( m_chromaR->value() );
- s.chromatic_aberration[1] =
- static_cast( m_chromaB->value() );
- s.half_size = m_halfSize->isChecked();
- s.highlight_mode = m_highlightMode->currentData().toInt();
+ s.auto_bright = m_autoBright->isChecked();
+ s.adjust_maximum_threshold = static_cast( m_adjustMaximum->value() );
+ s.black_level = ( m_blackLevelFromMetadata != nullptr &&
+ m_blackLevelFromMetadata->isChecked() )
+ ? -1
+ : m_blackLevel->value();
+ s.saturation_level = ( m_saturationFromMetadata != nullptr &&
+ m_saturationFromMetadata->isChecked() )
+ ? 0
+ : m_saturationLevel->value();
+ s.chromatic_aberration[0] = static_cast( m_chromaR->value() );
+ s.chromatic_aberration[1] = static_cast( m_chromaB->value() );
+ s.half_size = m_halfSize->isChecked();
+ s.highlight_mode = m_highlightMode->currentData().toInt();
for ( int i = 0; i < 4; ++i )
{
s.crop_box[i] = m_cropBox[i]->value();
@@ -1071,21 +1071,14 @@ rta::util::ImageConverter::Settings MainWindow::buildSettingsFromUi() const
switch ( m_cropMode->currentIndex() )
{
- case 0:
- s.crop_mode = S::CropMode::Off;
- break;
- case 1:
- s.crop_mode = S::CropMode::Soft;
- break;
- default:
- s.crop_mode = S::CropMode::Hard;
- break;
+ case 0: s.crop_mode = S::CropMode::Off; break;
+ case 1: s.crop_mode = S::CropMode::Soft; break;
+ default: s.crop_mode = S::CropMode::Hard; break;
}
- s.flip = m_flip->currentData().toInt();
- s.denoise_threshold = static_cast( m_denoise->value() );
- s.demosaic_algorithm =
- m_demosaic->currentText().toStdString();
+ s.flip = m_flip->currentData().toInt();
+ s.denoise_threshold = static_cast( m_denoise->value() );
+ s.demosaic_algorithm = m_demosaic->currentText().toStdString();
s.overwrite = m_overwrite->isChecked();
s.create_dirs = m_createDirs->isChecked();
@@ -1111,15 +1104,13 @@ rta::util::ImageConverter::Settings MainWindow::buildSettingsFromUi() const
s.lens_correction_types |= S::LensCorrectionType::Vignetting;
}
s.require_lens_correction = m_requireLens->isChecked();
- if ( m_lensMetadataOverride != nullptr
- && m_lensMetadataOverride->isChecked() )
+ if ( m_lensMetadataOverride != nullptr &&
+ m_lensMetadataOverride->isChecked() )
{
s.custom_lens_make = m_lensMake->text().toStdString();
s.custom_lens_model = m_lensModel->text().toStdString();
- s.custom_aperture =
- static_cast( m_lensAperture->value() );
- s.custom_focal_length =
- static_cast( m_lensFocal->value() );
+ s.custom_aperture = static_cast( m_lensAperture->value() );
+ s.custom_focal_length = static_cast( m_lensFocal->value() );
s.custom_focus_distance =
static_cast( m_lensFocus->value() );
}
@@ -1178,7 +1169,7 @@ void MainWindow::updateWbMethodDependentUi()
{
return;
}
- const int idx = m_wbMethod->currentIndex();
+ const int idx = m_wbMethod->currentIndex();
const bool showIlluminant = ( idx == 1 );
const bool showBoxRegion = ( idx == 2 );
const bool showCustomGains = ( idx == 3 );
@@ -1244,9 +1235,8 @@ void MainWindow::onAddFolder()
return;
}
const std::vector paths = { dir.toStdString() };
- const auto batches =
- rta::util::collect_image_files( paths );
- const QStringList flat = flattenBatches( batches );
+ const auto batches = rta::util::collect_image_files( paths );
+ const QStringList flat = flattenBatches( batches );
for ( const QString &f: flat )
{
m_fileList->addItem( f );
@@ -1305,23 +1295,27 @@ void MainWindow::onConvert()
appendLog( tr( "Starting batch (%1 files)…" ).arg( paths.size() ) );
auto *thread = new ConversionThread( this );
- m_worker = thread;
- connect( thread,
- &ConversionThread::fileStarted,
- this,
- &MainWindow::onConversionFileStarted );
- connect( thread,
- &ConversionThread::fileFinished,
- this,
- &MainWindow::onConversionFileFinished );
- connect( thread,
- &ConversionThread::progress,
- this,
- &MainWindow::onConversionProgress );
- connect( thread,
- &ConversionThread::batchFinished,
- this,
- &MainWindow::onBatchFinished );
+ m_worker = thread;
+ connect(
+ thread,
+ &ConversionThread::fileStarted,
+ this,
+ &MainWindow::onConversionFileStarted );
+ connect(
+ thread,
+ &ConversionThread::fileFinished,
+ this,
+ &MainWindow::onConversionFileFinished );
+ connect(
+ thread,
+ &ConversionThread::progress,
+ this,
+ &MainWindow::onConversionProgress );
+ connect(
+ thread,
+ &ConversionThread::batchFinished,
+ this,
+ &MainWindow::onBatchFinished );
connect( thread, &QThread::finished, thread, &QObject::deleteLater );
thread->setJob( settings, paths );
@@ -1380,7 +1374,7 @@ void MainWindow::onAbout()
namespace
{
-constexpr auto kPrefsRootQLS = "rawtoaces_gui";
+constexpr auto kPrefsRootQLS = "rawtoaces_gui";
constexpr int kPrefsFormatVersion = 1;
void setComboBoxIndexClamped( QComboBox *comboBox, int index )
@@ -1389,8 +1383,7 @@ void setComboBoxIndexClamped( QComboBox *comboBox, int index )
{
return;
}
- comboBox->setCurrentIndex(
- qBound( 0, index, comboBox->count() - 1 ) );
+ comboBox->setCurrentIndex( qBound( 0, index, comboBox->count() - 1 ) );
}
void setComboBoxCurrentByIntData( QComboBox *comboBox, int value )
@@ -1426,61 +1419,60 @@ void MainWindow::savePreferences() const
{
QSettings settings;
settings.beginGroup( kPrefsRootQLS );
- settings.setValue( QStringLiteral( "formatVersion" ),
- kPrefsFormatVersion );
+ settings.setValue( QStringLiteral( "formatVersion" ), kPrefsFormatVersion );
settings.beginGroup( QStringLiteral( "window" ) );
settings.setValue( QStringLiteral( "geometry" ), saveGeometry() );
if ( m_mainSplitter != nullptr )
{
- settings.setValue( QStringLiteral( "splitter" ),
- m_mainSplitter->saveState() );
+ settings.setValue(
+ QStringLiteral( "splitter" ), m_mainSplitter->saveState() );
}
if ( m_settingsTabs != nullptr )
{
- settings.setValue( QStringLiteral( "settingsTab" ),
- m_settingsTabs->currentIndex() );
+ settings.setValue(
+ QStringLiteral( "settingsTab" ), m_settingsTabs->currentIndex() );
}
settings.endGroup();
settings.beginGroup( QStringLiteral( "paths" ) );
if ( m_outputDir != nullptr )
{
- settings.setValue( QStringLiteral( "outputDir" ),
- m_outputDir->text() );
+ settings.setValue( QStringLiteral( "outputDir" ), m_outputDir->text() );
}
if ( m_dataDir != nullptr )
{
- settings.setValue( QStringLiteral( "spectralDataOverride" ),
- m_dataDir->text() );
+ settings.setValue(
+ QStringLiteral( "spectralDataOverride" ), m_dataDir->text() );
}
settings.endGroup();
settings.beginGroup( QStringLiteral( "colour" ) );
if ( m_wbMethod != nullptr )
{
- settings.setValue( QStringLiteral( "wbMethodIndex" ),
- m_wbMethod->currentIndex() );
+ settings.setValue(
+ QStringLiteral( "wbMethodIndex" ), m_wbMethod->currentIndex() );
}
if ( m_illuminant != nullptr )
{
- settings.setValue( QStringLiteral( "illuminant" ),
- m_illuminant->text() );
+ settings.setValue(
+ QStringLiteral( "illuminant" ), m_illuminant->text() );
}
for ( int i = 0; i < 4 && m_wbBox[i] != nullptr; ++i )
{
- settings.setValue( QStringLiteral( "wbBox%1" ).arg( i ),
- m_wbBox[i]->value() );
+ settings.setValue(
+ QStringLiteral( "wbBox%1" ).arg( i ), m_wbBox[i]->value() );
}
for ( int i = 0; i < 4 && m_customWb[i] != nullptr; ++i )
{
- settings.setValue( QStringLiteral( "customWb%1" ).arg( i ),
- m_customWb[i]->value() );
+ settings.setValue(
+ QStringLiteral( "customWb%1" ).arg( i ), m_customWb[i]->value() );
}
if ( m_matrixMethod != nullptr )
{
- settings.setValue( QStringLiteral( "matrixMethodIndex" ),
- m_matrixMethod->currentIndex() );
+ settings.setValue(
+ QStringLiteral( "matrixMethodIndex" ),
+ m_matrixMethod->currentIndex() );
}
for ( int r = 0; r < 3; ++r )
{
@@ -1496,18 +1488,18 @@ void MainWindow::savePreferences() const
}
if ( m_customCameraMake != nullptr )
{
- settings.setValue( QStringLiteral( "customCameraMake" ),
- m_customCameraMake->text() );
+ settings.setValue(
+ QStringLiteral( "customCameraMake" ), m_customCameraMake->text() );
}
if ( m_customCameraModel != nullptr )
{
- settings.setValue( QStringLiteral( "customCameraModel" ),
- m_customCameraModel->text() );
+ settings.setValue(
+ QStringLiteral( "customCameraModel" ),
+ m_customCameraModel->text() );
}
if ( m_headroom != nullptr )
{
- settings.setValue( QStringLiteral( "headroom" ),
- m_headroom->value() );
+ settings.setValue( QStringLiteral( "headroom" ), m_headroom->value() );
}
if ( m_scale != nullptr )
{
@@ -1518,33 +1510,35 @@ void MainWindow::savePreferences() const
settings.beginGroup( QStringLiteral( "raw" ) );
if ( m_autoBright != nullptr )
{
- settings.setValue( QStringLiteral( "autoBright" ),
- m_autoBright->isChecked() );
+ settings.setValue(
+ QStringLiteral( "autoBright" ), m_autoBright->isChecked() );
}
if ( m_adjustMaximum != nullptr )
{
- settings.setValue( QStringLiteral( "adjustMaximum" ),
- m_adjustMaximum->value() );
+ settings.setValue(
+ QStringLiteral( "adjustMaximum" ), m_adjustMaximum->value() );
}
if ( m_blackLevelFromMetadata != nullptr )
{
- settings.setValue( QStringLiteral( "blackLevelFromMetadata" ),
- m_blackLevelFromMetadata->isChecked() );
+ settings.setValue(
+ QStringLiteral( "blackLevelFromMetadata" ),
+ m_blackLevelFromMetadata->isChecked() );
}
if ( m_blackLevel != nullptr )
{
- settings.setValue( QStringLiteral( "blackLevel" ),
- m_blackLevel->value() );
+ settings.setValue(
+ QStringLiteral( "blackLevel" ), m_blackLevel->value() );
}
if ( m_saturationFromMetadata != nullptr )
{
- settings.setValue( QStringLiteral( "saturationFromMetadata" ),
- m_saturationFromMetadata->isChecked() );
+ settings.setValue(
+ QStringLiteral( "saturationFromMetadata" ),
+ m_saturationFromMetadata->isChecked() );
}
if ( m_saturationLevel != nullptr )
{
- settings.setValue( QStringLiteral( "saturationLevel" ),
- m_saturationLevel->value() );
+ settings.setValue(
+ QStringLiteral( "saturationLevel" ), m_saturationLevel->value() );
}
if ( m_chromaR != nullptr )
{
@@ -1556,28 +1550,29 @@ void MainWindow::savePreferences() const
}
if ( m_halfSize != nullptr )
{
- settings.setValue( QStringLiteral( "halfSize" ),
- m_halfSize->isChecked() );
+ settings.setValue(
+ QStringLiteral( "halfSize" ), m_halfSize->isChecked() );
}
if ( m_highlightMode != nullptr )
{
- settings.setValue( QStringLiteral( "highlightMode" ),
- m_highlightMode->currentData().toInt() );
+ settings.setValue(
+ QStringLiteral( "highlightMode" ),
+ m_highlightMode->currentData().toInt() );
}
for ( int i = 0; i < 4 && m_cropBox[i] != nullptr; ++i )
{
- settings.setValue( QStringLiteral( "cropBox%1" ).arg( i ),
- m_cropBox[i]->value() );
+ settings.setValue(
+ QStringLiteral( "cropBox%1" ).arg( i ), m_cropBox[i]->value() );
}
if ( m_cropMode != nullptr )
{
- settings.setValue( QStringLiteral( "cropModeIndex" ),
- m_cropMode->currentIndex() );
+ settings.setValue(
+ QStringLiteral( "cropModeIndex" ), m_cropMode->currentIndex() );
}
if ( m_flip != nullptr )
{
- settings.setValue( QStringLiteral( "flip" ),
- m_flip->currentData().toInt() );
+ settings.setValue(
+ QStringLiteral( "flip" ), m_flip->currentData().toInt() );
}
if ( m_denoise != nullptr )
{
@@ -1585,39 +1580,40 @@ void MainWindow::savePreferences() const
}
if ( m_demosaic != nullptr )
{
- settings.setValue( QStringLiteral( "demosaicAlgorithm" ),
- m_demosaic->currentText() );
+ settings.setValue(
+ QStringLiteral( "demosaicAlgorithm" ), m_demosaic->currentText() );
}
settings.endGroup();
settings.beginGroup( QStringLiteral( "output" ) );
if ( m_overwrite != nullptr )
{
- settings.setValue( QStringLiteral( "overwrite" ),
- m_overwrite->isChecked() );
+ settings.setValue(
+ QStringLiteral( "overwrite" ), m_overwrite->isChecked() );
}
if ( m_createDirs != nullptr )
{
- settings.setValue( QStringLiteral( "createDirs" ),
- m_createDirs->isChecked() );
+ settings.setValue(
+ QStringLiteral( "createDirs" ), m_createDirs->isChecked() );
}
settings.endGroup();
settings.beginGroup( QStringLiteral( "diagnostics" ) );
if ( m_useTiming != nullptr )
{
- settings.setValue( QStringLiteral( "useTiming" ),
- m_useTiming->isChecked() );
+ settings.setValue(
+ QStringLiteral( "useTiming" ), m_useTiming->isChecked() );
}
if ( m_disableCache != nullptr )
{
- settings.setValue( QStringLiteral( "disableCache" ),
- m_disableCache->isChecked() );
+ settings.setValue(
+ QStringLiteral( "disableCache" ), m_disableCache->isChecked() );
}
if ( m_disableExiftool != nullptr )
{
- settings.setValue( QStringLiteral( "disableExiftool" ),
- m_disableExiftool->isChecked() );
+ settings.setValue(
+ QStringLiteral( "disableExiftool" ),
+ m_disableExiftool->isChecked() );
}
if ( m_verbosity != nullptr )
{
@@ -1631,26 +1627,28 @@ void MainWindow::savePreferences() const
if ( m_lensCorrAberration != nullptr )
{
settings.beginGroup( QStringLiteral( "lens" ) );
- settings.setValue( QStringLiteral( "corrAberration" ),
- m_lensCorrAberration->isChecked() );
- settings.setValue( QStringLiteral( "corrDistortion" ),
- m_lensCorrDistortion->isChecked() );
- settings.setValue( QStringLiteral( "corrVignetting" ),
- m_lensCorrVignetting->isChecked() );
- settings.setValue( QStringLiteral( "requireLens" ),
- m_requireLens->isChecked() );
- settings.setValue( QStringLiteral( "lensMetadataOverride" ),
- m_lensMetadataOverride->isChecked() );
- settings.setValue( QStringLiteral( "lensMake" ),
- m_lensMake->text() );
- settings.setValue( QStringLiteral( "lensModel" ),
- m_lensModel->text() );
- settings.setValue( QStringLiteral( "lensAperture" ),
- m_lensAperture->value() );
- settings.setValue( QStringLiteral( "lensFocal" ),
- m_lensFocal->value() );
- settings.setValue( QStringLiteral( "lensFocus" ),
- m_lensFocus->value() );
+ settings.setValue(
+ QStringLiteral( "corrAberration" ),
+ m_lensCorrAberration->isChecked() );
+ settings.setValue(
+ QStringLiteral( "corrDistortion" ),
+ m_lensCorrDistortion->isChecked() );
+ settings.setValue(
+ QStringLiteral( "corrVignetting" ),
+ m_lensCorrVignetting->isChecked() );
+ settings.setValue(
+ QStringLiteral( "requireLens" ), m_requireLens->isChecked() );
+ settings.setValue(
+ QStringLiteral( "lensMetadataOverride" ),
+ m_lensMetadataOverride->isChecked() );
+ settings.setValue( QStringLiteral( "lensMake" ), m_lensMake->text() );
+ settings.setValue( QStringLiteral( "lensModel" ), m_lensModel->text() );
+ settings.setValue(
+ QStringLiteral( "lensAperture" ), m_lensAperture->value() );
+ settings.setValue(
+ QStringLiteral( "lensFocal" ), m_lensFocal->value() );
+ settings.setValue(
+ QStringLiteral( "lensFocus" ), m_lensFocus->value() );
settings.endGroup();
}
#endif
@@ -1716,15 +1714,17 @@ void MainWindow::loadPreferences()
{
m_wbBox[i]->setValue(
settings
- .value( QStringLiteral( "wbBox%1" ).arg( i ), m_wbBox[i]->value() )
+ .value(
+ QStringLiteral( "wbBox%1" ).arg( i ), m_wbBox[i]->value() )
.toInt() );
}
for ( int i = 0; i < 4 && m_customWb[i] != nullptr; ++i )
{
m_customWb[i]->setValue(
settings
- .value( QStringLiteral( "customWb%1" ).arg( i ),
- m_customWb[i]->value() )
+ .value(
+ QStringLiteral( "customWb%1" ).arg( i ),
+ m_customWb[i]->value() )
.toDouble() );
}
setComboBoxIndexClamped(
@@ -1738,10 +1738,11 @@ void MainWindow::loadPreferences()
{
m_customMat[r][c]->setValue(
settings
- .value( QStringLiteral( "customMatrix_%1_%2" )
- .arg( r )
- .arg( c ),
- m_customMat[r][c]->value() )
+ .value(
+ QStringLiteral( "customMatrix_%1_%2" )
+ .arg( r )
+ .arg( c ),
+ m_customMat[r][c]->value() )
.toDouble() );
}
}
@@ -1749,8 +1750,7 @@ void MainWindow::loadPreferences()
if ( m_customCameraMake != nullptr )
{
m_customCameraMake->setText(
- settings.value( QStringLiteral( "customCameraMake" ) )
- .toString() );
+ settings.value( QStringLiteral( "customCameraMake" ) ).toString() );
}
if ( m_customCameraModel != nullptr )
{
@@ -1776,24 +1776,26 @@ void MainWindow::loadPreferences()
if ( m_autoBright != nullptr )
{
m_autoBright->setChecked(
- settings.value( QStringLiteral( "autoBright" ),
- m_autoBright->isChecked() )
+ settings
+ .value(
+ QStringLiteral( "autoBright" ), m_autoBright->isChecked() )
.toBool() );
}
if ( m_adjustMaximum != nullptr )
{
- m_adjustMaximum->setValue(
- settings
- .value( QStringLiteral( "adjustMaximum" ),
- m_adjustMaximum->value() )
- .toDouble() );
+ m_adjustMaximum->setValue( settings
+ .value(
+ QStringLiteral( "adjustMaximum" ),
+ m_adjustMaximum->value() )
+ .toDouble() );
}
if ( m_blackLevelFromMetadata != nullptr )
{
m_blackLevelFromMetadata->setChecked(
settings
- .value( QStringLiteral( "blackLevelFromMetadata" ),
- m_blackLevelFromMetadata->isChecked() )
+ .value(
+ QStringLiteral( "blackLevelFromMetadata" ),
+ m_blackLevelFromMetadata->isChecked() )
.toBool() );
}
if ( m_blackLevel != nullptr )
@@ -1807,16 +1809,18 @@ void MainWindow::loadPreferences()
{
m_saturationFromMetadata->setChecked(
settings
- .value( QStringLiteral( "saturationFromMetadata" ),
- m_saturationFromMetadata->isChecked() )
+ .value(
+ QStringLiteral( "saturationFromMetadata" ),
+ m_saturationFromMetadata->isChecked() )
.toBool() );
}
if ( m_saturationLevel != nullptr )
{
m_saturationLevel->setValue(
settings
- .value( QStringLiteral( "saturationLevel" ),
- m_saturationLevel->value() )
+ .value(
+ QStringLiteral( "saturationLevel" ),
+ m_saturationLevel->value() )
.toInt() );
}
if ( m_chromaR != nullptr )
@@ -1834,25 +1838,26 @@ void MainWindow::loadPreferences()
if ( m_halfSize != nullptr )
{
m_halfSize->setChecked(
- settings.value( QStringLiteral( "halfSize" ),
- m_halfSize->isChecked() )
+ settings
+ .value( QStringLiteral( "halfSize" ), m_halfSize->isChecked() )
.toBool() );
}
if ( m_highlightMode != nullptr )
{
const int v = settings
- .value( QStringLiteral( "highlightMode" ),
- m_highlightMode->currentData().toInt() )
+ .value(
+ QStringLiteral( "highlightMode" ),
+ m_highlightMode->currentData().toInt() )
.toInt();
setComboBoxCurrentByIntData( m_highlightMode, std::clamp( v, 0, 9 ) );
}
for ( int i = 0; i < 4 && m_cropBox[i] != nullptr; ++i )
{
- m_cropBox[i]->setValue(
- settings
- .value( QStringLiteral( "cropBox%1" ).arg( i ),
- m_cropBox[i]->value() )
- .toInt() );
+ m_cropBox[i]->setValue( settings
+ .value(
+ QStringLiteral( "cropBox%1" ).arg( i ),
+ m_cropBox[i]->value() )
+ .toInt() );
}
setComboBoxIndexClamped(
m_cropMode,
@@ -1861,8 +1866,8 @@ void MainWindow::loadPreferences()
{
const int v =
settings
- .value( QStringLiteral( "flip" ),
- m_flip->currentData().toInt() )
+ .value(
+ QStringLiteral( "flip" ), m_flip->currentData().toInt() )
.toInt();
setComboBoxCurrentByIntData( m_flip, std::clamp( v, 0, 8 ) );
}
@@ -1875,8 +1880,7 @@ void MainWindow::loadPreferences()
if ( m_demosaic != nullptr )
{
const QString algo =
- settings.value( QStringLiteral( "demosaicAlgorithm" ) )
- .toString();
+ settings.value( QStringLiteral( "demosaicAlgorithm" ) ).toString();
if ( !algo.isEmpty() )
{
const int demosaicIx = m_demosaic->findText( algo );
@@ -1892,15 +1896,17 @@ void MainWindow::loadPreferences()
if ( m_overwrite != nullptr )
{
m_overwrite->setChecked(
- settings.value( QStringLiteral( "overwrite" ),
- m_overwrite->isChecked() )
+ settings
+ .value(
+ QStringLiteral( "overwrite" ), m_overwrite->isChecked() )
.toBool() );
}
if ( m_createDirs != nullptr )
{
m_createDirs->setChecked(
- settings.value( QStringLiteral( "createDirs" ),
- m_createDirs->isChecked() )
+ settings
+ .value(
+ QStringLiteral( "createDirs" ), m_createDirs->isChecked() )
.toBool() );
}
settings.endGroup();
@@ -1909,22 +1915,26 @@ void MainWindow::loadPreferences()
if ( m_useTiming != nullptr )
{
m_useTiming->setChecked(
- settings.value( QStringLiteral( "useTiming" ),
- m_useTiming->isChecked() )
+ settings
+ .value(
+ QStringLiteral( "useTiming" ), m_useTiming->isChecked() )
.toBool() );
}
if ( m_disableCache != nullptr )
{
- m_disableCache->setChecked(
- settings.value( QStringLiteral( "disableCache" ),
- m_disableCache->isChecked() )
- .toBool() );
+ m_disableCache->setChecked( settings
+ .value(
+ QStringLiteral( "disableCache" ),
+ m_disableCache->isChecked() )
+ .toBool() );
}
if ( m_disableExiftool != nullptr )
{
m_disableExiftool->setChecked(
- settings.value( QStringLiteral( "disableExiftool" ),
- m_disableExiftool->isChecked() )
+ settings
+ .value(
+ QStringLiteral( "disableExiftool" ),
+ m_disableExiftool->isChecked() )
.toBool() );
}
if ( m_verbosity != nullptr )
@@ -1940,24 +1950,30 @@ void MainWindow::loadPreferences()
{
settings.beginGroup( QStringLiteral( "lens" ) );
m_lensCorrAberration->setChecked(
- settings.value( QStringLiteral( "corrAberration" ),
- m_lensCorrAberration->isChecked() )
+ settings
+ .value(
+ QStringLiteral( "corrAberration" ),
+ m_lensCorrAberration->isChecked() )
.toBool() );
m_lensCorrDistortion->setChecked(
- settings.value( QStringLiteral( "corrDistortion" ),
- m_lensCorrDistortion->isChecked() )
+ settings
+ .value(
+ QStringLiteral( "corrDistortion" ),
+ m_lensCorrDistortion->isChecked() )
.toBool() );
m_lensCorrVignetting->setChecked(
- settings.value( QStringLiteral( "corrVignetting" ),
- m_lensCorrVignetting->isChecked() )
- .toBool() );
- m_requireLens->setChecked(
- settings.value( QStringLiteral( "requireLens" ),
- m_requireLens->isChecked() )
+ settings
+ .value(
+ QStringLiteral( "corrVignetting" ),
+ m_lensCorrVignetting->isChecked() )
.toBool() );
+ m_requireLens->setChecked( settings
+ .value(
+ QStringLiteral( "requireLens" ),
+ m_requireLens->isChecked() )
+ .toBool() );
m_lensMetadataOverride->setChecked(
- settings
- .value( QStringLiteral( "lensMetadataOverride" ), false )
+ settings.value( QStringLiteral( "lensMetadataOverride" ), false )
.toBool() );
m_lensMake->setText(
settings.value( QStringLiteral( "lensMake" ) ).toString() );
@@ -1965,8 +1981,8 @@ void MainWindow::loadPreferences()
settings.value( QStringLiteral( "lensModel" ) ).toString() );
m_lensAperture->setValue(
settings
- .value( QStringLiteral( "lensAperture" ),
- m_lensAperture->value() )
+ .value(
+ QStringLiteral( "lensAperture" ), m_lensAperture->value() )
.toDouble() );
m_lensFocal->setValue(
settings
diff --git a/src/rawtoaces_gui/main_window.h b/src/rawtoaces_gui/main_window.h
index 24c2b1c2..b3d86f6e 100644
--- a/src/rawtoaces_gui/main_window.h
+++ b/src/rawtoaces_gui/main_window.h
@@ -54,78 +54,78 @@ private slots:
private:
rta::util::ImageConverter::Settings buildSettingsFromUi() const;
- void appendLog( const QString &line );
- void setUiBusy( bool busy );
- void loadPreferences();
- void savePreferences() const;
-
- QListWidget *m_fileList = nullptr;
- QLineEdit *m_outputDir = nullptr;
- QLineEdit *m_dataDir = nullptr;
- QPushButton *m_convertButton = nullptr;
- QPushButton *m_cancelButton = nullptr;
- QProgressBar *m_progress = nullptr;
- QTextEdit *m_log = nullptr;
-
- QSplitter *m_mainSplitter = nullptr;
- QTabWidget *m_settingsTabs = nullptr;
-
- QComboBox *m_wbMethod = nullptr;
- QComboBox *m_matrixMethod = nullptr;
- QLineEdit *m_illuminant = nullptr;
- QSpinBox *m_wbBox[4]{};
+ void appendLog( const QString &line );
+ void setUiBusy( bool busy );
+ void loadPreferences();
+ void savePreferences() const;
+
+ QListWidget *m_fileList = nullptr;
+ QLineEdit *m_outputDir = nullptr;
+ QLineEdit *m_dataDir = nullptr;
+ QPushButton *m_convertButton = nullptr;
+ QPushButton *m_cancelButton = nullptr;
+ QProgressBar *m_progress = nullptr;
+ QTextEdit *m_log = nullptr;
+
+ QSplitter *m_mainSplitter = nullptr;
+ QTabWidget *m_settingsTabs = nullptr;
+
+ QComboBox *m_wbMethod = nullptr;
+ QComboBox *m_matrixMethod = nullptr;
+ QLineEdit *m_illuminant = nullptr;
+ QSpinBox *m_wbBox[4]{};
QDoubleSpinBox *m_customWb[4]{};
QDoubleSpinBox *m_customMat[3][3]{};
- QLineEdit *m_customCameraMake = nullptr;
- QLineEdit *m_customCameraModel = nullptr;
- QDoubleSpinBox *m_headroom = nullptr;
- QDoubleSpinBox *m_scale = nullptr;
+ QLineEdit *m_customCameraMake = nullptr;
+ QLineEdit *m_customCameraModel = nullptr;
+ QDoubleSpinBox *m_headroom = nullptr;
+ QDoubleSpinBox *m_scale = nullptr;
- QCheckBox *m_autoBright = nullptr;
- QDoubleSpinBox *m_adjustMaximum = nullptr;
+ QCheckBox *m_autoBright = nullptr;
+ QDoubleSpinBox *m_adjustMaximum = nullptr;
QCheckBox *m_blackLevelFromMetadata = nullptr;
- QSpinBox *m_blackLevel = nullptr;
+ QSpinBox *m_blackLevel = nullptr;
QCheckBox *m_saturationFromMetadata = nullptr;
- QSpinBox *m_saturationLevel = nullptr;
- QDoubleSpinBox *m_chromaR = nullptr;
- QDoubleSpinBox *m_chromaB = nullptr;
- QCheckBox *m_halfSize = nullptr;
- QComboBox *m_highlightMode = nullptr;
+ QSpinBox *m_saturationLevel = nullptr;
+ QDoubleSpinBox *m_chromaR = nullptr;
+ QDoubleSpinBox *m_chromaB = nullptr;
+ QCheckBox *m_halfSize = nullptr;
+ QComboBox *m_highlightMode = nullptr;
QSpinBox *m_cropBox[4]{};
QComboBox *m_cropMode = nullptr;
- QComboBox *m_flip = nullptr;
- QDoubleSpinBox *m_denoise = nullptr;
+ QComboBox *m_flip = nullptr;
+ QDoubleSpinBox *m_denoise = nullptr;
QComboBox *m_demosaic = nullptr;
- QCheckBox *m_overwrite = nullptr;
+ QCheckBox *m_overwrite = nullptr;
QCheckBox *m_createDirs = nullptr;
- QCheckBox *m_lensCorrAberration = nullptr;
- QCheckBox *m_lensCorrDistortion = nullptr;
- QCheckBox *m_lensCorrVignetting = nullptr;
- QCheckBox *m_requireLens = nullptr;
- QCheckBox *m_lensMetadataOverride = nullptr;
- QLineEdit *m_lensMake = nullptr;
- QLineEdit *m_lensModel = nullptr;
- QDoubleSpinBox *m_lensAperture = nullptr;
- QDoubleSpinBox *m_lensFocal = nullptr;
- QDoubleSpinBox *m_lensFocus = nullptr;
-
- QCheckBox *m_useTiming = nullptr;
- QCheckBox *m_disableCache = nullptr;
+ QCheckBox *m_lensCorrAberration = nullptr;
+ QCheckBox *m_lensCorrDistortion = nullptr;
+ QCheckBox *m_lensCorrVignetting = nullptr;
+ QCheckBox *m_requireLens = nullptr;
+ QCheckBox *m_lensMetadataOverride = nullptr;
+ QLineEdit *m_lensMake = nullptr;
+ QLineEdit *m_lensModel = nullptr;
+ QDoubleSpinBox *m_lensAperture = nullptr;
+ QDoubleSpinBox *m_lensFocal = nullptr;
+ QDoubleSpinBox *m_lensFocus = nullptr;
+
+ QCheckBox *m_useTiming = nullptr;
+ QCheckBox *m_disableCache = nullptr;
QCheckBox *m_disableExiftool = nullptr;
- QComboBox *m_verbosity = nullptr;
+ QComboBox *m_verbosity = nullptr;
QPointer m_worker;
/// Shown only when matrix method is Custom (matches `ImageConverter` usage).
- QWidget *m_customMatrixWrap = nullptr;
- QLabel *m_customMatrixLabel = nullptr;
-
- QWidget *m_wbIlluminantWrap = nullptr;
- QLabel *m_wbIlluminantLabel = nullptr;
- QWidget *m_wbBoxRegionWrap = nullptr;
- QLabel *m_wbBoxRegionLabel = nullptr;
- QWidget *m_wbCustomGainsWrap = nullptr;
- QLabel *m_wbCustomGainsLabel = nullptr;
+ QWidget *m_customMatrixWrap = nullptr;
+ QLabel *m_customMatrixLabel = nullptr;
+
+ QWidget *m_wbIlluminantWrap = nullptr;
+ QLabel *m_wbIlluminantLabel = nullptr;
+ QWidget *m_wbBoxRegionWrap = nullptr;
+ QLabel *m_wbBoxRegionLabel = nullptr;
+ QWidget *m_wbCustomGainsWrap = nullptr;
+ QLabel *m_wbCustomGainsLabel = nullptr;
};
From b60a26c7adbdcffe5cd3c657edb90e9e8684f2b7 Mon Sep 17 00:00:00 2001
From: Aleksandr Motsjonov
Date: Sun, 22 Mar 2026 13:05:26 +1100
Subject: [PATCH 3/9] fixing issues
Signed-off-by: Aleksandr Motsjonov
---
.github/workflows/gui.yml | 1 +
README.md | 2 +-
build_scripts/install_deps_ubuntu.bash | 3 +-
src/rawtoaces_gui/CMakeLists.txt | 2 +-
src/rawtoaces_gui/conversion_thread.cpp | 6 ++++
src/rawtoaces_gui/main.cpp | 12 +++-----
src/rawtoaces_gui/main_window.cpp | 39 +++++++++++++++++++++----
7 files changed, 48 insertions(+), 17 deletions(-)
diff --git a/.github/workflows/gui.yml b/.github/workflows/gui.yml
index 82e8e7e2..ac10d8d9 100644
--- a/.github/workflows/gui.yml
+++ b/.github/workflows/gui.yml
@@ -25,6 +25,7 @@ jobs:
exiftool \
liblensfun-dev \
liblensfun-data-v1 \
+ libglib2.0-dev \
qt6-base-dev \
qt6-base-dev-tools
diff --git a/README.md b/README.md
index dfa87374..55c90a51 100644
--- a/README.md
+++ b/README.md
@@ -47,7 +47,7 @@ To build `rawtoaces` you would need to satisfy these dependencies:
| Library | Min Version| Purpose | Link to installation instruction |
| ------- | -----------| -------- | -------------------------------- |
| `cmake` | `3.16` | | [CMake download](https://cmake.org/download/)|
-| Qt 6 (optional) | `6.5` | Widgets module for `rawtoaces_gui` | [Qt documentation](https://doc.qt.io/qt-6/) |
+| Qt 6 (optional) | `6.4` | Widgets module for `rawtoaces_gui` | [Qt documentation](https://doc.qt.io/qt-6/) |
| `ceres` | `1.12.0` | Ceres Solver is an open source library for solving Non-linear Least Squares problems with bounds constraints and unconstrained optimization problems. It processes non-linear regression for rawtoaces. | [Ceres Solver installation](http://ceres-solver.org/installation.html)|
| `OpenImageIO` | `3.0` | OpenImageIO is an open source library providing vast functionality for image processing. rawtoaces relies on OpenImageIO for reading raw files, saving AcesContainer files, and also all pixel operations. | [OpenImageIO installation](https://github.com/AcademySoftwareFoundation/OpenImageIO/blob/main/INSTALL.md) |
| `nlohmann-json` | `3.6` | nlohmann-json is a simple header-only library for parsing JSON files. | [nlohmann-json integration](https://github.com/nlohmann/json#integration) |
diff --git a/build_scripts/install_deps_ubuntu.bash b/build_scripts/install_deps_ubuntu.bash
index 3c44c414..fa5c4b6a 100644
--- a/build_scripts/install_deps_ubuntu.bash
+++ b/build_scripts/install_deps_ubuntu.bash
@@ -11,7 +11,8 @@ time sudo apt-get -q -f install -y \
openimageio-tools libopenimageio-dev \
exiftool \
liblensfun-dev \
- liblensfun-data-v1
+ liblensfun-data-v1 \
+ libglib2.0-dev
# Nanobind in apt is still v1.9, we need at least v2.2.
pip3 install pytest nanobind
diff --git a/src/rawtoaces_gui/CMakeLists.txt b/src/rawtoaces_gui/CMakeLists.txt
index 9474c78f..9ad43699 100644
--- a/src/rawtoaces_gui/CMakeLists.txt
+++ b/src/rawtoaces_gui/CMakeLists.txt
@@ -1,6 +1,6 @@
cmake_minimum_required(VERSION 3.16)
-find_package(Qt6 6.5 REQUIRED COMPONENTS Widgets)
+find_package(Qt6 6.4 REQUIRED COMPONENTS Widgets)
set(CMAKE_AUTOMOC ON)
diff --git a/src/rawtoaces_gui/conversion_thread.cpp b/src/rawtoaces_gui/conversion_thread.cpp
index 58a35fc1..4dfcac38 100644
--- a/src/rawtoaces_gui/conversion_thread.cpp
+++ b/src/rawtoaces_gui/conversion_thread.cpp
@@ -22,6 +22,12 @@ void ConversionThread::requestCancel()
void ConversionThread::run()
{
const int total = static_cast( m_paths.size() );
+ if ( total <= 0 )
+ {
+ emit progress( 0, 0 );
+ emit batchFinished();
+ return;
+ }
for ( int i = 0; i < total; ++i )
{
if ( m_cancel.load() )
diff --git a/src/rawtoaces_gui/main.cpp b/src/rawtoaces_gui/main.cpp
index c48353fb..9a8b89f6 100644
--- a/src/rawtoaces_gui/main.cpp
+++ b/src/rawtoaces_gui/main.cpp
@@ -5,18 +5,14 @@
#include
-#ifndef WIN32
-# include
-#else
-# include
-#endif
+#include
int main( int argc, char *argv[] )
{
-#ifndef WIN32
- setenv( "TZ", "UTC", 1 );
-#else
+#if defined( _WIN32 )
_putenv( const_cast( "TZ=UTC" ) );
+#else
+ setenv( "TZ", "UTC", 1 );
#endif
QApplication application( argc, argv );
diff --git a/src/rawtoaces_gui/main_window.cpp b/src/rawtoaces_gui/main_window.cpp
index e0779900..84727c4f 100644
--- a/src/rawtoaces_gui/main_window.cpp
+++ b/src/rawtoaces_gui/main_window.cpp
@@ -28,7 +28,6 @@
#include
#include
#include
-#include
#include
#include
#include
@@ -1365,11 +1364,13 @@ void MainWindow::onBatchFinished()
void MainWindow::onAbout()
{
- QMessageBox::about(
- this,
- tr( "About rawtoaces" ),
- tr( "rawtoaces GUI — ACES container output from camera RAW.\n"
- "Settings match the same options as the rawtoaces image converter." ) );
+ QString body = tr(
+ "rawtoaces GUI — ACES container output from camera RAW.\n"
+ "Settings match the same options as the rawtoaces image converter." );
+#ifdef VERSION
+ body.prepend( tr( "Version %1\n\n" ).arg( QStringLiteral( VERSION ) ) );
+#endif
+ QMessageBox::about( this, tr( "About rawtoaces" ), body );
}
namespace
@@ -1411,6 +1412,32 @@ void setVerbosityComboFromLevel( QComboBox *comboBox, int level )
void MainWindow::closeEvent( QCloseEvent *event )
{
+ if ( m_worker != nullptr && m_worker->isRunning() )
+ {
+ const auto reply = QMessageBox::question(
+ this,
+ tr( "rawtoaces" ),
+ tr( "A conversion is still running. Cancel it and close?" ),
+ QMessageBox::Yes | QMessageBox::No,
+ QMessageBox::No );
+ if ( reply != QMessageBox::Yes )
+ {
+ event->ignore();
+ return;
+ }
+ m_worker->requestCancel();
+ constexpr int kStopWaitMs = 300000;
+ if ( !m_worker->wait( kStopWaitMs ) )
+ {
+ QMessageBox::warning(
+ this,
+ tr( "rawtoaces" ),
+ tr( "The conversion could not be stopped before the timeout. "
+ "Try again after the current file finishes." ) );
+ event->ignore();
+ return;
+ }
+ }
savePreferences();
QMainWindow::closeEvent( event );
}
From a530a0568061651fa8f9cfd605f05b397eb3cc19 Mon Sep 17 00:00:00 2001
From: Aleksandr Motsjonov
Date: Sun, 22 Mar 2026 13:30:24 +1100
Subject: [PATCH 4/9] Appple .app file
Signed-off-by: Aleksandr Motsjonov
---
README.md | 1 +
src/rawtoaces_gui/CMakeLists.txt | 32 +++++++++++++++++-
.../macos/rawtoaces-icon-1024.png | Bin 0 -> 18917 bytes
.../macos/rawtoaces-icon-source.png | Bin 0 -> 9989 bytes
src/rawtoaces_gui/macos/rawtoaces_gui.icns | Bin 0 -> 149157 bytes
src/rawtoaces_gui/main.cpp | 17 ++++++++++
6 files changed, 49 insertions(+), 1 deletion(-)
create mode 100644 src/rawtoaces_gui/macos/rawtoaces-icon-1024.png
create mode 100644 src/rawtoaces_gui/macos/rawtoaces-icon-source.png
create mode 100644 src/rawtoaces_gui/macos/rawtoaces_gui.icns
diff --git a/README.md b/README.md
index 55c90a51..407c95df 100644
--- a/README.md
+++ b/README.md
@@ -128,6 +128,7 @@ $ cmake -S . -B build -DRTA_BUILD_GUI=ON -DCMAKE_PREFIX_PATH="$(brew --prefix qt
$ cmake --build build
```
+After building, start the GUI from `build/src/rawtoaces_gui/`: use the **`rawtoaces_gui`** program on Windows and Linux, or **`rawtoaces_gui.app`** on macOS. It appears in the menu bar and about box as **rawtoaces**.
#### Docker
diff --git a/src/rawtoaces_gui/CMakeLists.txt b/src/rawtoaces_gui/CMakeLists.txt
index 9ad43699..60d331d1 100644
--- a/src/rawtoaces_gui/CMakeLists.txt
+++ b/src/rawtoaces_gui/CMakeLists.txt
@@ -32,4 +32,34 @@ if(WIN32)
set_target_properties(rawtoaces_gui PROPERTIES WIN32_EXECUTABLE TRUE)
endif()
-install(TARGETS rawtoaces_gui DESTINATION ${INSTALL_BIN_DIR})
+# macOS: build a real .app so Finder/Dock get bundle metadata; menu bar title
+# still uses setApplicationDisplayName in main.cpp.
+if(APPLE)
+ set(RTA_GUI_MACOS_ICON "${CMAKE_CURRENT_SOURCE_DIR}/macos/rawtoaces_gui.icns")
+ set_target_properties(rawtoaces_gui PROPERTIES
+ MACOSX_BUNDLE TRUE
+ MACOSX_BUNDLE_BUNDLE_NAME "rawtoaces"
+ MACOSX_BUNDLE_GUI_IDENTIFIER "org.aswf.rawtoaces.gui"
+ MACOSX_BUNDLE_INFO_SHORT_VERSION_STRING "${RAWTOACES_VERSION}"
+ MACOSX_BUNDLE_BUNDLE_VERSION "${RAWTOACES_VERSION}"
+ )
+ if(EXISTS "${RTA_GUI_MACOS_ICON}")
+ target_sources(rawtoaces_gui PRIVATE "${RTA_GUI_MACOS_ICON}")
+ set_source_files_properties("${RTA_GUI_MACOS_ICON}" PROPERTIES
+ MACOSX_PACKAGE_LOCATION Resources
+ )
+ # Base name must match Resources/*.icns; same as CFBundleExecutable avoids edge cases.
+ set_target_properties(rawtoaces_gui PROPERTIES
+ MACOSX_BUNDLE_ICON_FILE "rawtoaces_gui"
+ )
+ endif()
+endif()
+
+if(APPLE)
+ install(TARGETS rawtoaces_gui
+ BUNDLE DESTINATION ${INSTALL_BIN_DIR}
+ RUNTIME DESTINATION ${INSTALL_BIN_DIR}
+ )
+else()
+ install(TARGETS rawtoaces_gui DESTINATION ${INSTALL_BIN_DIR})
+endif()
diff --git a/src/rawtoaces_gui/macos/rawtoaces-icon-1024.png b/src/rawtoaces_gui/macos/rawtoaces-icon-1024.png
new file mode 100644
index 0000000000000000000000000000000000000000..070e110e1e3e59af0a6ab956d0e88fac1a3524a7
GIT binary patch
literal 18917
zcmeHucUV)|+V55z8Ap-921s*otPD*-kQN<5R7AuAD1;~xDKQiw^bph$5D;cW0g<90
zy+m55At-H>E+s%HLKH9wp(l`#z@I(nGndREC}2MXh5Q0RJTMe81wl9s
z2x8oTAiV?#lJ-cdvNQl+Y;-U)HHJj+|Ci*fSTG{)eb(GWoFTSrz5Iqe(*gkmooPI4
zeDaFl;Ph}{Lb4-XeRZ@%J#o|L4WD(7n{BHL|IzB?#V)H2a~BK54K8jT9DZEZ*T(70
zyEM`_Tt#-JsrI>I%W1A<{oMJBQe`{KY%Je|hi|&O|A!yme?I@C>5;F2NTvXNyDz`{
zN6f=~|JxJXl~v7-vEV+uh|RxdnKG0A{7^t(pvpRMZRedu8Lai-LkN32c!y4`hYz4L
zC*XHz)3@;Zo^|jqp&$3a?;#uD?SXy`f#0ER-^1^JPx^Z!{?>@UP2+DL@wc-C;o$G6
z@pt(A{{RvsUGV|9sIEvAi6671=@hQS6q{{m(uJ|o(;Fl6&ouoRiwWi9vhZJB+(;UD
zy$cW5E%UW(P0)XX>LKUqo+gS-Td?}KS=^!78CFkBs5d6M<8xSt#P-2p?1Xf(p8L&K
z+oB^Aswi1AYm`iER(p75ORBw>NnRKaZ8)WbZmrx*?|BRT@GeU%WE~9MbnnF#*6xWj
zV&1o@;@wXucW_)h|9r%w@p52=xUN0?3%~EymP^5%OcLBTb9m2~JZ(HCr6dyz1G~;w
z{w>x`x$$JR_S&f@j1Goa6b_!`T%WVB3m!jaMPV&zqS^P0Th>hh+zUa`4!te*(s}BE
z$e*s^g`Z@VX2qKhtb;Ue4$W7MMttZzsX3c*X*i}za8Ow|SDV_17zy+paE$Svp^R&L
zPKW(QN7QGu8m=lgxr@bPk<#Y^sx(nF~bJH`WBDn%}
z|AB{|b1kVlL#R{3&(dU!3Ki6q)3W`Q%3S4G^u8BvF}ChCVT@zP&$ibg5QaF)rE^Uy*^XesyAH;eFVe1RTP>A3;eVRQrJRDQY{VK)B
zDh4jn_C_3xNH5n3HsslqNv7}Zy&Dy9x+e4LFxF05>P6p(f{xvSesk?jbkFCUZhB=r
zMH_5TnbEl9c^R;8=gJ&ug~eu2cA^0;z<|4C-S~~WpDmDxmCteLz*De$A&a4ktEqu2
zw&_kEN)s*0q4VDutz5EZpkJ%Bc|Qi1sO34E4K?(;n^bD@%g$1^;!oK0XTp|d^U
zZQ~pRBWm>(+*^d_owrdJkTLXCD(ae#EQG!f=3Qztzy=KLgSK~q+Z&&k#4E=Y>JeF&
zpr$SBpgNf&PJ^WhhSLX?EEQH&u0Bh=%;jRIoujhUuXlPEk|<4-UvM8jrxdoYe12~@
z_j_nyL#am{nZE5*?7ig89Zd#eF!>o-s@R?3~%
zFzTb#q+~B-gc?Ok@e~cNX>HhWpg#^a!v
zgueLK>6%8%Ib57)U-M*(s6*PJg&b3c7hr_rI_cb@eaM2ZncSemygsABU~{U-z7