diff --git a/src/app/update/dialog.rs b/src/app/update/dialog.rs index edede88c6..784815f8f 100644 --- a/src/app/update/dialog.rs +++ b/src/app/update/dialog.rs @@ -153,7 +153,7 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven async move { let exts: Vec<&str> = extensions.iter().map(|s| s.as_str()).collect(); - let path = rfd::AsyncFileDialog::new() + let path = crate::sys::file_dialog() .set_title(title) .add_filter(filter_name, &exts) .add_filter("All Files", &["*"]) diff --git a/src/app/update/file.rs b/src/app/update/file.rs index 33cac8659..36f3d4305 100644 --- a/src/app/update/file.rs +++ b/src/app/update/file.rs @@ -845,7 +845,7 @@ pub(super) fn on_open_file(&mut self) -> Task { let close = self.close_save_dialog_window(); let pick = Task::perform( async move { - let mut dlg = rfd::AsyncFileDialog::new() + let mut dlg = crate::sys::file_dialog() .set_title("Save Drawing As") .set_file_name(default_name) .add_filter(filter_label, &[filter_ext]); @@ -2204,7 +2204,7 @@ pub(super) fn on_open_file(&mut self) -> Task { .unwrap_or("export.ctb".into()); Task::perform( async move { - rfd::AsyncFileDialog::new() + crate::sys::file_dialog() .set_title("Save Plot Style Table") .set_file_name(&default_name) .add_filter("Plot Style Files", &["ctb", "stb", "CTB", "STB"]) diff --git a/src/app/update/mod.rs b/src/app/update/mod.rs index 574944003..e2ab273ba 100644 --- a/src/app/update/mod.rs +++ b/src/app/update/mod.rs @@ -408,7 +408,7 @@ impl OpenCADStudio { Message::XAttachPick => Task::perform( async { - let handle = rfd::AsyncFileDialog::new() + let handle = crate::sys::file_dialog() .set_title("Select External Reference File") .add_filter("CAD Files", &["dwg", "dxf", "bak", "DWG", "DXF", "BAK"]) .add_filter("DWG Files", &["dwg", "DWG"]) @@ -446,7 +446,7 @@ impl OpenCADStudio { let name = block_name.clone(); Task::perform( async move { - let path = rfd::AsyncFileDialog::new() + let path = crate::sys::file_dialog() .set_title("Save Block As") .set_file_name("block.dwg") .add_filter("DWG Files", &["dwg"]) @@ -469,7 +469,7 @@ impl OpenCADStudio { let csv_clone = csv.clone(); Task::perform( async move { - let path = rfd::AsyncFileDialog::new() + let path = crate::sys::file_dialog() .set_title("Save Data Extraction") .set_file_name("extraction.csv") .add_filter("CSV", &["csv"]) @@ -512,7 +512,7 @@ impl OpenCADStudio { } Task::perform( async { - rfd::AsyncFileDialog::new() + crate::sys::file_dialog() .set_title("Export STL") .set_file_name("export.stl") .add_filter("STL Files", &["stl"]) @@ -539,7 +539,7 @@ impl OpenCADStudio { } Task::perform( async { - rfd::AsyncFileDialog::new() + crate::sys::file_dialog() .set_title("Export STEP AP203") .set_file_name("export.step") .add_filter("STEP Files", &["step", "stp"]) @@ -559,7 +559,7 @@ impl OpenCADStudio { // ── OBJ import ──────────────────────────────────────────────── Message::ObjImport => Task::perform( async { - rfd::AsyncFileDialog::new() + crate::sys::file_dialog() .set_title("Import OBJ Mesh") .add_filter("Wavefront OBJ", &["obj", "OBJ"]) .add_filter("All Files", &["*"]) diff --git a/src/config.rs b/src/config.rs index d7cd7487b..52eb3306a 100644 --- a/src/config.rs +++ b/src/config.rs @@ -28,3 +28,45 @@ pub fn config_dir() -> Option { p.push("OpenCADStudio"); Some(p) } + +// ── Last file-dialog directory ─────────────────────────────────────────────── + +use std::path::Path; +use std::sync::{Mutex, OnceLock}; + +fn last_dir_store() -> &'static Mutex> { + static STORE: OnceLock>> = OnceLock::new(); + STORE.get_or_init(|| { + // Seed from the persisted value; discard it if the folder is gone. + let loaded = config_dir() + .map(|d| d.join("last_dir.txt")) + .and_then(|f| std::fs::read_to_string(f).ok()) + .map(|s| PathBuf::from(s.trim())) + .filter(|p| p.is_dir()); + Mutex::new(loaded) + }) +} + +/// The directory the last file dialog picked or saved into, if it still +/// exists — used to seed the next dialog so pickers reopen where the user +/// left off. Persisted across runs. +pub fn last_dialog_dir() -> Option { + last_dir_store().lock().ok()?.clone().filter(|p| p.is_dir()) +} + +/// Record the directory of a path a file dialog just returned. +pub fn remember_dialog_dir(file_path: &Path) { + let Some(dir) = file_path.parent().filter(|d| d.is_dir()) else { + return; + }; + if let Ok(mut store) = last_dir_store().lock() { + if store.as_deref() == Some(dir) { + return; // unchanged — skip the disk write + } + *store = Some(dir.to_path_buf()); + } + if let Some(cfg) = config_dir() { + let _ = std::fs::create_dir_all(&cfg); + let _ = std::fs::write(cfg.join("last_dir.txt"), dir.display().to_string()); + } +} diff --git a/src/io/mod.rs b/src/io/mod.rs index 4a3342328..10375bb80 100644 --- a/src/io/mod.rs +++ b/src/io/mod.rs @@ -40,7 +40,7 @@ const PHASE_FINALIZING: u8 = 3; /// the parser thread starts. #[cfg(not(target_arch = "wasm32"))] pub async fn pick_open_path() -> Option<(PathBuf, u64)> { - let handle = rfd::AsyncFileDialog::new() + let handle = crate::sys::file_dialog() .set_title("Open CAD file") .add_filter("CAD Files", &["dwg", "dxf", "bak", "sv$", "DWG", "DXF", "BAK"]) .add_filter("DWG Files", &["dwg", "DWG"]) @@ -103,7 +103,7 @@ pub async fn open_path_with_phase( #[cfg(target_arch = "wasm32")] pub async fn pick_and_load_web( ) -> Result<(String, PathBuf, CadDocument, DerivedCaches), String> { - let handle = rfd::AsyncFileDialog::new() + let handle = crate::sys::file_dialog() .set_title("Open CAD file") .add_filter("CAD Files", &["dwg", "dxf", "DWG", "DXF"]) .add_filter("All Files", &["*"]) @@ -364,7 +364,7 @@ pub fn write_backup(path: &std::path::Path) { /// Show a file-open dialog and load the selected CTB or STB file. pub async fn pick_plot_style() -> Option { - let handle = rfd::AsyncFileDialog::new() + let handle = crate::sys::file_dialog() .set_title("Load Plot Style Table") .add_filter("Plot Style Tables", &["ctb", "stb", "CTB", "STB"]) .add_filter("CTB Files", &["ctb", "CTB"]) @@ -380,7 +380,7 @@ pub async fn pick_plot_style() -> Option { /// Show a file-open dialog for raster images and decode the selected file. /// Returns `(path, pixel_width, pixel_height)` or an error string. pub async fn pick_image_file() -> Result<(PathBuf, u32, u32), String> { - let handle = rfd::AsyncFileDialog::new() + let handle = crate::sys::file_dialog() .set_title("Select Image File") .add_filter("Images", &["png", "jpg", "jpeg", "bmp", "tiff", "tif"]) .add_filter("PNG", &["png"]) diff --git a/src/io/pdf_export.rs b/src/io/pdf_export.rs index a77e1e084..52a3bad4f 100644 --- a/src/io/pdf_export.rs +++ b/src/io/pdf_export.rs @@ -100,7 +100,7 @@ pub fn export_pdf( /// Show a PDF save-file dialog and return the chosen path (or None if cancelled). #[cfg(not(target_arch = "wasm32"))] pub async fn pick_pdf_path_owned(stem: String) -> Option { - rfd::AsyncFileDialog::new() + crate::sys::file_dialog() .set_title("Export as PDF") .set_file_name(&format!("{stem}.pdf")) .add_filter("PDF Files", &["pdf"]) diff --git a/src/sys.rs b/src/sys.rs index dbe759801..da4f9d72c 100644 --- a/src/sys.rs +++ b/src/sys.rs @@ -39,7 +39,11 @@ pub async fn read_clipboard_text() -> Option { /// (a follow-up). #[cfg(not(target_arch = "wasm32"))] pub fn handle_path(h: &rfd::FileHandle) -> std::path::PathBuf { - h.path().to_path_buf() + let p = h.path().to_path_buf(); + // Every dialog result funnels through here — remember its folder so the + // next dialog opens where the user left off. + crate::config::remember_dialog_dir(&p); + p } #[cfg(target_arch = "wasm32")] @@ -47,6 +51,23 @@ pub fn handle_path(h: &rfd::FileHandle) -> std::path::PathBuf { std::path::PathBuf::from(h.file_name()) } +/// New async file dialog seeded with the last directory a dialog was used in. +/// All pickers should start from this instead of `rfd::AsyncFileDialog::new()`. +#[cfg(not(target_arch = "wasm32"))] +pub fn file_dialog() -> rfd::AsyncFileDialog { + let dlg = rfd::AsyncFileDialog::new(); + match crate::config::last_dialog_dir() { + Some(dir) => dlg.set_directory(dir), + None => dlg, + } +} + +/// Web: no filesystem paths, nothing to remember. +#[cfg(target_arch = "wasm32")] +pub fn file_dialog() -> rfd::AsyncFileDialog { + rfd::AsyncFileDialog::new() +} + /// Trigger a browser download of `bytes` as `name`. Builds a Blob, points a /// hidden `` at it and clicks it programmatically — because this /// runs inside the Save button's click (a user gesture), the file downloads