From 4dac9d04fc99ccf22b92f898be9ed091fdf64bc9 Mon Sep 17 00:00:00 2001 From: Jonas Neubauer <108294796+jonassneu@users.noreply.github.com> Date: Sun, 1 Nov 2026 20:44:57 +0100 Subject: [PATCH] G2: Save a Copy starts from " copy" (#2788) --- crates/ui-egui/src/menus/frontend.rs | 17 +++++++++-------- crates/ui-egui/src/menus/tests.rs | 7 ++++--- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/crates/ui-egui/src/menus/frontend.rs b/crates/ui-egui/src/menus/frontend.rs index 2327bbd3b..bc7b4dc04 100644 --- a/crates/ui-egui/src/menus/frontend.rs +++ b/crates/ui-egui/src/menus/frontend.rs @@ -553,17 +553,18 @@ pub(super) fn file_dialog(app: &mut EffectcraftApp, id: &str, params: &Value) -> } // Save As, Save a Copy and Save a Copy As XML start from the open project's file, in its // folder, as After Effects does (#2788); an untitled project starts from Untitled Project. - let project_as = |ext: &str| -> std::borrow::Cow<'static, str> { - match app.session.path.as_deref() { - Some(p) => std::path::Path::new(p).with_extension(ext).to_string_lossy().into_owned().into(), - None => format!("Untitled Project.{ext}").into(), - } + // Save a Copy adds ` copy` (`suffix`): it never writes over the open project's file. + let project_as = |suffix: &str, ext: &str| -> std::borrow::Cow<'static, str> { + let Some(path) = app.session.path.as_deref().map(std::path::Path::new) else { return format!("Untitled Project{suffix}.{ext}").into() }; + let stem = path.file_stem().map(|s| s.to_string_lossy().into_owned()).unwrap_or_else(|| "Untitled Project".into()); + path.with_file_name(format!("{stem}{suffix}.{ext}")).to_string_lossy().into_owned().into() }; let (key, ask) = match id { "file.import" | "file.importMultiple" => ("paths", Ask::Import), "file.open" => ("path", Ask::OpenProject), - "file.save" if app.session.path.is_none() => ("path", Ask::Save(project_as("ecproj"))), - "file.saveAs" | "file.saveCopy" => ("path", Ask::Save(project_as("ecproj"))), + "file.save" if app.session.path.is_none() => ("path", Ask::Save(project_as("", "ecproj"))), + "file.saveAs" => ("path", Ask::Save(project_as("", "ecproj"))), + "file.saveCopy" => ("path", Ask::Save(project_as(" copy", "ecproj"))), "comp.saveFrameAs" => ("path", Ask::Save("Frame.png".into())), "comp.saveFrameAsPsd" => ("path", Ask::Save("Frame.psd".into())), "comp.saveFrameAsExr" => ("path", Ask::Save("Frame.exr".into())), @@ -584,7 +585,7 @@ pub(super) fn file_dialog(app: &mut EffectcraftApp, id: &str, params: &Value) -> "file.replaceFootage" => ("path", Ask::Import), "file.relinkFootage" => ("folder", Ask::Folder), "file.collectFiles" => ("folder", Ask::Save("Collected Files".into())), - "file.saveCopyAsXml" => ("path", Ask::Save(project_as("ecprojx"))), + "file.saveCopyAsXml" => ("path", Ask::Save(project_as("", "ecprojx"))), "keys.rpfCameraImport" => ("path", Ask::Open(&["json", "csv", "txt"])), "essential.exportTemplate" => ("path", Ask::Save("Template.ectemplate".into())), "essential.importTemplate" => ("path", Ask::Open(&["ectemplate"])), diff --git a/crates/ui-egui/src/menus/tests.rs b/crates/ui-egui/src/menus/tests.rs index 1d7aa861f..7f9241d78 100644 --- a/crates/ui-egui/src/menus/tests.rs +++ b/crates/ui-egui/src/menus/tests.rs @@ -299,8 +299,8 @@ fn save_dialogs_offer_the_file_extension() { assert_eq!(*asked.borrow(), ["project Frame.png"]); } -/// #2788: Save As, Save a Copy and Save a Copy As XML start from the open project's name (in its -/// folder), not Untitled Project. +/// #2788: Save As, Save a Copy (` copy`) and Save a Copy As XML start from the open +/// project's name (in its folder), not Untitled Project. #[test] fn save_as_dialogs_start_from_the_open_project() { use std::cell::RefCell; @@ -326,7 +326,8 @@ fn save_as_dialogs_start_from_the_open_project() { invoke(&mut app, &ctx, id, json!({})).unwrap(); } let xml = dir.join("Intro.ecprojx").to_string_lossy().to_string(); - assert_eq!(*asked.borrow(), ["project Intro.ecproj".to_string(), "project Intro.ecproj".into(), format!("file {xml} [ecprojx]")]); + // Save a Copy names another file: it would refuse to write over the open project. + assert_eq!(*asked.borrow(), ["project Intro.ecproj".to_string(), "project Intro copy.ecproj".into(), format!("file {xml} [ecprojx]")]); // The non-blocking dialog gets the whole path, so it opens in the project's folder. let pending: Arc>> = Arc::default(); let queue = pending.clone();