forked from 1b-agent/opencadstudio-upstream
feat(dialogs): reopen file pickers in the last-used directory
Every rfd dialog result funnels through sys::handle_path, which now records the picked file's folder (persisted to the config dir), and all pickers are built via sys::file_dialog(), seeded with that folder. Save-As keeps the current tab's own directory as the first choice and falls back to the remembered one. Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
1 parent
e8411c1b6d
commit
e78ad7884d
7 files changed
+78
-15
No files matched your search
@@ -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", &["*"])
|
||||
|
||||
@@ -845,7 +845,7 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||
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<Message> {
|
||||
.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"])
|
||||
|
||||
@@ -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", &["*"])
|
||||
|
||||
@@ -28,3 +28,45 @@ pub fn config_dir() -> Option<PathBuf> {
|
||||
p.push("OpenCADStudio");
|
||||
Some(p)
|
||||
}
|
||||
|
||||
// ── Last file-dialog directory ───────────────────────────────────────────────
|
||||
|
||||
use std::path::Path;
|
||||
use std::sync::{Mutex, OnceLock};
|
||||
|
||||
fn last_dir_store() -> &'static Mutex<Option<PathBuf>> {
|
||||
static STORE: OnceLock<Mutex<Option<PathBuf>>> = 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<PathBuf> {
|
||||
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());
|
||||
}
|
||||
}
|
||||
+4
-4
@@ -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<plot_style::PlotStyleTable> {
|
||||
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<plot_style::PlotStyleTable> {
|
||||
/// 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"])
|
||||
|
||||
@@ -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<std::path::PathBuf> {
|
||||
rfd::AsyncFileDialog::new()
|
||||
crate::sys::file_dialog()
|
||||
.set_title("Export as PDF")
|
||||
.set_file_name(&format!("{stem}.pdf"))
|
||||
.add_filter("PDF Files", &["pdf"])
|
||||
|
||||
+22
-1
@@ -39,7 +39,11 @@ pub async fn read_clipboard_text() -> Option<String> {
|
||||
/// (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 `<a download>` at it and clicks it programmatically — because this
|
||||
/// runs inside the Save button's click (a user gesture), the file downloads
|
||||
|
||||
Reference in new issue
Block a user