diff --git a/crates/engine/src/cmd/fileio/mod.rs b/crates/engine/src/cmd/fileio/mod.rs index 2fb4cec00..4b05cb8ab 100644 --- a/crates/engine/src/cmd/fileio/mod.rs +++ b/crates/engine/src/cmd/fileio/mod.rs @@ -74,7 +74,7 @@ pub use native::with_compression_pref; pub use pdfimport::{LoadOptions, page_document}; pub(crate) use save::job_for; pub use save::{ - SAVE_FORMATS, SaveJob, SaveMode, SavePlan, StreamWrite, TEMPLATE_FORMATS, export_folder, reads_option as save_reads_option, + SAVE_FORMATS, SaveJob, SaveMode, SavePlan, StreamWrite, TEMPLATE_FORMATS, dialog_folder, export_folder, reads_option as save_reads_option, remembered_save_options, save_filters, save_format, save_job, save_plan, save_with, stamp_save_dates, template_filters, templates_dialog_folder, templates_folder, }; diff --git a/crates/engine/src/cmd/fileio/save.rs b/crates/engine/src/cmd/fileio/save.rs index 1d70e99f2..486777a24 100644 --- a/crates/engine/src/cmd/fileio/save.rs +++ b/crates/engine/src/cmd/fileio/save.rs @@ -185,6 +185,14 @@ fn parent_folder(path: &str) -> Option { std::path::Path::new(path).parent().map(|p| p.to_string_lossy().to_string()).filter(|p| !p.is_empty()) } +/// A file dialog's starting folder for a path on disk (#4122): the path's parent when it is a +/// folder, else the nearest ancestor directory that exists. +pub fn dialog_folder(path: &str) -> Option { + let folder = parent_folder(path)?; + let path = std::path::PathBuf::from(folder); + path.ancestors().find(|a| a.is_dir()).map(|a| a.to_string_lossy().into_owned()) +} + /// A save format by id or extension, or why it isn't one. fn save_format_of(f: &str) -> std::result::Result<&'static Format, String> { match format(f) { @@ -300,7 +308,7 @@ fn disagreeing_option(p: &Value, svg: bool) -> std::result::Result Result Option { + app.ui.recent_files.iter().find_map(|p| fileio::dialog_folder(p)).or_else(|| suggested(app, "").1) +} + /// File → Open… pub fn open_dialog(app: &mut VectorcraftApp) -> Result<(), String> { if let Some(f) = app.services.open_async.as_mut() { f(); return Ok(()); } - let path = pick_open(app, &FilePick { filters: FilePick::filters(fileio::open_filters()), ..Default::default() })?; + let path = + pick_open(app, &FilePick { filters: FilePick::filters(fileio::open_filters()), folder: open_dialog_folder(app), ..Default::default() })?; open_reporting(app, &path).map(|_| ()) } diff --git a/crates/ui-egui/src/tests_picks.rs b/crates/ui-egui/src/tests_picks.rs index ece5aa444..686fddd18 100644 --- a/crates/ui-egui/src/tests_picks.rs +++ b/crates/ui-egui/src/tests_picks.rs @@ -154,12 +154,28 @@ fn saved_document(app: &mut VectorcraftApp, tag: &str) -> (std::path::PathBuf, S // ------------------------------------------------------------------------------------- the tests +#[test] +fn file_open_starts_in_the_last_opened_files_folder() { + let (mut app, shown, _) = app(); + let dir = std::env::temp_dir().join(format!("vc-open-dialog-{}", std::process::id())); + std::fs::create_dir_all(&dir).unwrap(); + let path = dir.join("poster.svg").to_string_lossy().into_owned(); + crate::io::note_recent(&mut app, &path); + assert!(app.run("file.open", json!({})).is_err()); + poll(&mut app); + let folder = dir.to_string_lossy().into_owned(); + assert!(matches!(shown.requests().as_slice(), [PickRequest::Open(p)] if p.folder == Some(folder))); + shown.answer(&[]); + poll(&mut app); + let _ = std::fs::remove_dir_all(&dir); +} + #[test] fn file_open_opens_what_its_dialog_picks_when_it_answers() { let (mut app, shown, _) = app(); // The dialog shows; File › Open gives up for now, without a "cancelled". assert!(app.run("file.open", json!({})).is_err()); - assert!(matches!(shown.requests().as_slice(), [PickRequest::Open(_)])); + assert!(matches!(shown.requests().as_slice(), [PickRequest::Open(p)] if p.folder.is_none())); poll(&mut app); assert!(app.session.documents().is_empty() && app.ui.status.is_empty(), "{}", app.ui.status); // Another dialog waits for this one.