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:
@@ -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
|
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
|
between while it plays. The finished preview and the saved video are
|
||||||
unchanged.
|
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
|
## [0.7.0] - 2026-09-26
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
} // namespace effector
|
||||||
|
|||||||
@@ -54,4 +54,9 @@ Project projectFromJson(const std::string& text, const std::string& directory =
|
|||||||
void saveProject(const std::string& path, const Project& project);
|
void saveProject(const std::string& path, const Project& project);
|
||||||
Project loadProject(const std::string& path);
|
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
|
} // namespace effector
|
||||||
|
|||||||
+24
-2
@@ -2476,6 +2476,7 @@ void EffectorWindow::applyProject(const Project& project, const std::string& pat
|
|||||||
seedEntry_.set_text(std::to_string(seed_));
|
seedEntry_.set_text(std::to_string(seed_));
|
||||||
|
|
||||||
projectPath_ = path;
|
projectPath_ = path;
|
||||||
|
lastExport_.clear();
|
||||||
updateTitle();
|
updateTitle();
|
||||||
clearError();
|
clearError();
|
||||||
suspendRender_ = false;
|
suspendRender_ = false;
|
||||||
@@ -2801,8 +2802,27 @@ void EffectorWindow::onSave() {
|
|||||||
|
|
||||||
auto dialog = Gtk::FileDialog::create();
|
auto dialog = Gtk::FileDialog::create();
|
||||||
dialog->set_title(video ? "Save video" : "Save image");
|
dialog->set_title(video ? "Save video" : "Save image");
|
||||||
dialog->set_initial_name("effector-" + chainSlug(stages_, video) + "-" + std::to_string(seed_) +
|
// The project's own folder when it has one, else where the last save went.
|
||||||
(video ? ".mp4" : ".png"));
|
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) {
|
dialog->save(*this, [this, dialog](const Glib::RefPtr<Gio::AsyncResult>& result) {
|
||||||
onSaveChosen(result, dialog);
|
onSaveChosen(result, dialog);
|
||||||
});
|
});
|
||||||
@@ -2838,6 +2858,7 @@ void EffectorWindow::onSaveChosen(const Glib::RefPtr<Gio::AsyncResult>& result,
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
clearError();
|
clearError();
|
||||||
|
lastExport_ = path.string();
|
||||||
status_.set_text("Saved " + path.string());
|
status_.set_text("Saved " + path.string());
|
||||||
status_.set_tooltip_text("Saved " + path.string());
|
status_.set_tooltip_text("Saved " + path.string());
|
||||||
}
|
}
|
||||||
@@ -2956,6 +2977,7 @@ void EffectorWindow::onExportFinished() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
clearError();
|
clearError();
|
||||||
|
lastExport_ = exportPath_;
|
||||||
status_.set_text("Saved " + exportPath_);
|
status_.set_text("Saved " + exportPath_);
|
||||||
status_.set_tooltip_text("Saved " + exportPath_);
|
status_.set_tooltip_text("Saved " + exportPath_);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -206,6 +206,9 @@ private:
|
|||||||
Image result_;
|
Image result_;
|
||||||
std::uint64_t seed_ = 0;
|
std::uint64_t seed_ = 0;
|
||||||
std::string projectPath_; // empty until the project has been saved once
|
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_;
|
Glib::RefPtr<Gio::Menu> recentMenu_;
|
||||||
sigc::connection recentChanged_; // the recent list outlives the window
|
sigc::connection recentChanged_; // the recent list outlives the window
|
||||||
bool suspendRender_ = true;
|
bool suspendRender_ = true;
|
||||||
|
|||||||
@@ -1281,6 +1281,21 @@ int main() {
|
|||||||
{
|
{
|
||||||
const auto dir = std::filesystem::temp_directory_path() / "effector_test_project";
|
const auto dir = std::filesystem::temp_directory_path() / "effector_test_project";
|
||||||
std::filesystem::create_directories(dir);
|
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";
|
const auto source = dir / "source.png";
|
||||||
gradient(64, 48, 7).save(source.string());
|
gradient(64, 48, 7).save(source.string());
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user