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:
+165
-1
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user