Load source images in the background with a progress window

Decoding a batch of photographs took seconds on the main thread, so the
window froze with no sign of what was happening. loadImages now takes an
optional LoadProgress (per-file callback plus a cancel flag) and hands files
out from a shared cursor, so one large file cannot stall the queue behind it.

The GUI decodes off the main thread and shows a progress window with the
count, the current file and a Cancel button; it only appears if the batch is
still running after 250 ms. Headless mode keeps a "reading n/total" line on
stderr when it is attached to a terminal.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
hans
2026-09-18 14:39:07 +02:00
co-authored by Claude Opus 5
parent 925a719da0
commit 7e98357070
7 changed files with 317 additions and 15 deletions
+165 -1
View File
@@ -38,6 +38,8 @@ EffectorWindow::EffectorWindow(const Options& initial) {
seed_ = Rng::randomSeed();
renderDone_.connect(sigc::mem_fun(*this, &EffectorWindow::onRenderFinished));
loadProgress_.connect(sigc::mem_fun(*this, &EffectorWindow::onLoadProgress));
loadFinished_.connect(sigc::mem_fun(*this, &EffectorWindow::onLoadFinished));
buildUi();
applyInitial(initial);
@@ -48,6 +50,9 @@ EffectorWindow::EffectorWindow(const Options& initial) {
EffectorWindow::~EffectorWindow() {
debounce_.disconnect();
loadWindowDelay_.disconnect();
loadCancel_.store(true, std::memory_order_relaxed);
if (loader_.joinable()) loader_.join();
if (worker_.joinable()) worker_.join();
}
@@ -62,6 +67,7 @@ void EffectorWindow::buildUi() {
addButton->set_tooltip_text("Add images");
addButton->signal_clicked().connect(sigc::mem_fun(*this, &EffectorWindow::onAddImages));
header->pack_start(*addButton);
addButton_ = addButton;
std::vector<Glib::ustring> titles;
for (const auto& effect : effects_) titles.push_back(effect->title());
@@ -330,8 +336,94 @@ void EffectorWindow::mutateSources(Mutate&& mutate) {
}
void EffectorWindow::addPaths(const std::vector<std::string>& paths) {
if (paths.empty()) return;
if (loading_) { // a second selection arrived mid-batch; take it afterwards
loadQueue_.insert(loadQueue_.end(), paths.begin(), paths.end());
return;
}
loading_ = true;
loadCancel_.store(false, std::memory_order_relaxed);
loadDone_.store(0, std::memory_order_relaxed);
loadTotal_ = paths.size();
{
std::lock_guard<std::mutex> lock(loadMutex_);
loadCurrent_.clear();
}
if (addButton_ != nullptr) addButton_->set_sensitive(false);
// A couple of small files decode faster than a window can be drawn, so the
// progress window only appears if the batch is still going after a moment.
loadWindowDelay_.disconnect();
loadWindowDelay_ = Glib::signal_timeout().connect(
[this] {
showLoadWindow();
return false;
},
250);
loader_ = std::thread([this, paths] {
LoadProgress progress;
progress.cancel = &loadCancel_;
progress.onFile = [this](std::size_t done, std::size_t, const std::string& path) {
{
std::lock_guard<std::mutex> lock(loadMutex_);
loadCurrent_ = path;
}
loadDone_.store(done, std::memory_order_relaxed);
loadProgress_.emit();
};
std::vector<LoadFailure> failures;
auto images = loadImages(paths, failures, &progress);
{
std::lock_guard<std::mutex> lock(loadMutex_);
loadedImages_ = std::move(images);
loadFailures_ = std::move(failures);
}
loadFinished_.emit();
});
}
void EffectorWindow::onLoadProgress() {
const std::size_t done = loadDone_.load(std::memory_order_relaxed);
std::string current;
{
std::lock_guard<std::mutex> lock(loadMutex_);
current = loadCurrent_;
}
if (loadBar_ != nullptr && loadTotal_ > 0) {
loadBar_->set_fraction(static_cast<double>(done) / static_cast<double>(loadTotal_));
loadBar_->set_text(std::to_string(done) + " of " + std::to_string(loadTotal_));
}
if (loadFileLabel_ != nullptr && !current.empty()) {
loadFileLabel_->set_text(std::filesystem::path(current).filename().string());
}
}
void EffectorWindow::onLoadFinished() {
if (loader_.joinable()) loader_.join();
loading_ = false;
loadWindowDelay_.disconnect();
closeLoadWindow();
if (addButton_ != nullptr) addButton_->set_sensitive(true);
std::vector<Image> images;
std::vector<LoadFailure> failures;
auto images = loadImages(paths, failures);
{
std::lock_guard<std::mutex> lock(loadMutex_);
images = std::move(loadedImages_);
failures = std::move(loadFailures_);
loadedImages_.clear();
loadFailures_.clear();
}
if (loadCancel_.load(std::memory_order_relaxed)) {
// A cancelled batch is incomplete, so none of it is taken.
loadQueue_.clear();
showError("Loading cancelled.");
return;
}
for (const auto& image : images) appendSourceRow(image.path());
mutateSources([&images](SourceList& list) {
@@ -349,6 +441,78 @@ void EffectorWindow::addPaths(const std::vector<std::string>& paths) {
clearError();
}
requestRender();
if (!loadQueue_.empty()) {
auto queued = std::move(loadQueue_);
loadQueue_.clear();
addPaths(queued);
}
}
void EffectorWindow::showLoadWindow() {
if (!loading_ || loadWindow_) return;
loadWindow_ = std::make_unique<Gtk::Window>();
loadWindow_->set_title("Reading images");
loadWindow_->set_transient_for(*this);
loadWindow_->set_modal(true);
loadWindow_->set_resizable(false);
loadWindow_->set_default_size(420, -1);
auto* box = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::VERTICAL, 12);
box->set_margin(18);
auto* heading = Gtk::make_managed<Gtk::Label>("Reading source images…");
heading->set_xalign(0.0F);
heading->add_css_class("heading");
box->append(*heading);
auto* bar = Gtk::make_managed<Gtk::ProgressBar>();
bar->set_show_text(true);
bar->set_text("0 of " + std::to_string(loadTotal_));
box->append(*bar);
loadBar_ = bar;
auto* file = Gtk::make_managed<Gtk::Label>("");
file->set_xalign(0.0F);
file->set_ellipsize(Pango::EllipsizeMode::MIDDLE);
file->add_css_class("dim-label");
box->append(*file);
loadFileLabel_ = file;
auto* cancel = Gtk::make_managed<Gtk::Button>("Cancel");
cancel->set_halign(Gtk::Align::END);
cancel->signal_clicked().connect(sigc::mem_fun(*this, &EffectorWindow::cancelLoad));
box->append(*cancel);
// Closing the window means the same thing as pressing Cancel, but the
// window stays up until the decoding threads have actually stopped.
loadWindow_->signal_close_request().connect(
[this] {
cancelLoad();
return true;
},
false);
loadWindow_->set_child(*box);
onLoadProgress(); // show whatever finished while the window was pending
loadWindow_->present();
}
void EffectorWindow::closeLoadWindow() {
loadBar_ = nullptr;
loadFileLabel_ = nullptr;
if (!loadWindow_) return;
loadWindow_->set_hide_on_close(false);
loadWindow_->hide();
loadWindow_.reset();
}
void EffectorWindow::cancelLoad() {
if (!loading_) return;
loadCancel_.store(true, std::memory_order_relaxed);
if (loadBar_ != nullptr) loadBar_->set_text("Cancelling…");
if (loadFileLabel_ != nullptr) loadFileLabel_->set_text("");
}
void EffectorWindow::appendSourceRow(const std::string& path) {
+28 -1
View File
@@ -2,6 +2,7 @@
#include <gtkmm.h>
#include <atomic>
#include <cstdint>
#include <memory>
#include <mutex>
@@ -34,7 +35,12 @@ private:
void onAddImages();
void onImagesChosen(const Glib::RefPtr<Gio::AsyncResult>& result,
const Glib::RefPtr<Gtk::FileDialog>& dialog);
void addPaths(const std::vector<std::string>& paths);
void addPaths(const std::vector<std::string>& paths); // decodes in the background
void onLoadProgress(); // dispatcher: one file finished decoding
void onLoadFinished(); // dispatcher: the batch is done or was cancelled
void showLoadWindow();
void closeLoadWindow();
void cancelLoad();
void appendSourceRow(const std::string& path);
void removeSource(Gtk::ListBoxRow* row);
@@ -65,6 +71,22 @@ private:
std::uint64_t seed_ = 0;
bool suspendRender_ = true;
// Source loading. Decoding a batch of large photographs takes seconds, so
// it runs off the main thread and reports into a small progress window.
std::thread loader_;
Glib::Dispatcher loadProgress_;
Glib::Dispatcher loadFinished_;
std::atomic<bool> loadCancel_{false};
std::atomic<std::size_t> loadDone_{0};
std::size_t loadTotal_ = 0;
std::mutex loadMutex_;
std::string loadCurrent_;
std::vector<Image> loadedImages_;
std::vector<LoadFailure> loadFailures_;
std::vector<std::string> loadQueue_; // chosen while another batch was loading
bool loading_ = false;
sigc::connection loadWindowDelay_;
// Render job
Glib::Dispatcher renderDone_;
std::thread worker_;
@@ -78,6 +100,11 @@ private:
// Widgets
Gtk::Label banner_;
Gtk::Button* addButton_ = nullptr;
// Built fresh with the progress window, and null whenever it is closed.
std::unique_ptr<Gtk::Window> loadWindow_;
Gtk::ProgressBar* loadBar_ = nullptr;
Gtk::Label* loadFileLabel_ = nullptr;
Gtk::ListBox sourceList_;
Gtk::SpinButton widthSpin_;
Gtk::SpinButton heightSpin_;