Open Save in the project folder and offer the last name with a number counted on

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Si
2026-09-26 20:24:10 +02:00
co-authored by Claude Opus 5.5
parent ac8b62208c
commit 8537a3cf0a
6 changed files with 68 additions and 2 deletions
+3
View File
@@ -22,6 +22,9 @@ commit that made it. The interface shows this file under Changelog in its menu.
plays that draft, each frame held for three, then fills in the frames
between while it plays. The finished preview and the saved video are
unchanged.
- Save opens in the project's folder, and after the first save it offers the
last saved file's name with a number counted on (`city.png`, then
`city-1.png`, `city-2.png`), skipping names already taken.
## [0.7.0] - 2026-09-26
+18
View File
@@ -672,4 +672,22 @@ Project loadProject(const std::string& path) {
}
}
std::string nextExportPath(const std::string& previous) {
const std::filesystem::path path(previous);
std::string stem = path.stem().string();
long count = 0;
const auto dash = stem.rfind('-');
if (dash != std::string::npos && dash + 1 < stem.size() && stem.size() - dash <= 10 &&
stem.find_first_not_of("0123456789", dash + 1) == std::string::npos) {
count = std::strtol(stem.c_str() + dash + 1, nullptr, 10);
stem.resize(dash);
}
std::filesystem::path next;
std::error_code error;
do {
next = path.parent_path() / (stem + "-" + std::to_string(++count) + path.extension().string());
} while (std::filesystem::exists(next, error));
return next.string();
}
} // namespace effector
+5
View File
@@ -54,4 +54,9 @@ Project projectFromJson(const std::string& text, const std::string& directory =
void saveProject(const std::string& path, const Project& project);
Project loadProject(const std::string& path);
// The name to offer for the next export after `previous`: its stem with the
// trailing `-N` counted on (`city.png` → `city-1.png`, `city-4.png` →
// `city-5.png`), skipping any that already exist in its folder.
std::string nextExportPath(const std::string& previous);
} // namespace effector
+24 -2
View File
@@ -2476,6 +2476,7 @@ void EffectorWindow::applyProject(const Project& project, const std::string& pat
seedEntry_.set_text(std::to_string(seed_));
projectPath_ = path;
lastExport_.clear();
updateTitle();
clearError();
suspendRender_ = false;
@@ -2801,8 +2802,27 @@ void EffectorWindow::onSave() {
auto dialog = Gtk::FileDialog::create();
dialog->set_title(video ? "Save video" : "Save image");
dialog->set_initial_name("effector-" + chainSlug(stages_, video) + "-" + std::to_string(seed_) +
(video ? ".mp4" : ".png"));
// The project's own folder when it has one, else where the last save went.
std::filesystem::path folder;
if (!projectPath_.empty()) {
folder = std::filesystem::path(projectPath_).parent_path();
} else if (!lastExport_.empty()) {
folder = std::filesystem::path(lastExport_).parent_path();
}
if (!folder.empty()) dialog->set_initial_folder(Gio::File::create_for_path(folder.string()));
// The last save's name counted on, so saving again never offers to
// overwrite it; its extension only when it is the same kind of file.
std::string name;
if (lastExport_.empty()) {
name = "effector-" + chainSlug(stages_, video) + "-" + std::to_string(seed_) +
(video ? ".mp4" : ".png");
} else {
std::filesystem::path last(lastExport_);
if (isVideoPath(last.string()) != video) last.replace_extension(video ? ".mp4" : ".png");
if (!folder.empty()) last = folder / last.filename();
name = std::filesystem::path(nextExportPath(last.string())).filename().string();
}
dialog->set_initial_name(name);
dialog->save(*this, [this, dialog](const Glib::RefPtr<Gio::AsyncResult>& result) {
onSaveChosen(result, dialog);
});
@@ -2838,6 +2858,7 @@ void EffectorWindow::onSaveChosen(const Glib::RefPtr<Gio::AsyncResult>& result,
return;
}
clearError();
lastExport_ = path.string();
status_.set_text("Saved " + path.string());
status_.set_tooltip_text("Saved " + path.string());
}
@@ -2956,6 +2977,7 @@ void EffectorWindow::onExportFinished() {
return;
}
clearError();
lastExport_ = exportPath_;
status_.set_text("Saved " + exportPath_);
status_.set_tooltip_text("Saved " + exportPath_);
}
+3
View File
@@ -206,6 +206,9 @@ private:
Image result_;
std::uint64_t seed_ = 0;
std::string projectPath_; // empty until the project has been saved once
// The last image or video saved from this project, which the next Save
// offers again with its number counted on; empty until one is saved.
std::string lastExport_;
Glib::RefPtr<Gio::Menu> recentMenu_;
sigc::connection recentChanged_; // the recent list outlives the window
bool suspendRender_ = true;
+15
View File
@@ -1281,6 +1281,21 @@ int main() {
{
const auto dir = std::filesystem::temp_directory_path() / "effector_test_project";
std::filesystem::create_directories(dir);
{
const auto base = (dir / "city.png").string();
std::filesystem::remove(dir / "city-1.png");
std::filesystem::remove(dir / "city-2.png");
check(nextExportPath(base) == (dir / "city-1.png").string(),
"the next export after a plain name counts from 1");
check(nextExportPath((dir / "city-1.png").string()) == (dir / "city-2.png").string(),
"the next export counts on from the last number");
std::ofstream((dir / "city-1.png").string()) << "x";
check(nextExportPath(base) == (dir / "city-2.png").string(),
"the next export skips a name already on disk");
std::filesystem::remove(dir / "city-1.png");
check(nextExportPath((dir / "a-b.mp4").string()) == (dir / "a-b-1.mp4").string(),
"a dash before letters is not a count");
}
const auto source = dir / "source.png";
gradient(64, 48, 7).save(source.string());