Merge pull request #4263 from nrq6993-a11y/stream/vc-feat-4122-open-last-folder

#4122: File → Open starts in the last opened file's folder
This commit is contained in:
BFlat
2026-10-26 20:43:26 +07:00
committed by GitHub
4 changed files with 49 additions and 4 deletions
+1 -1
View File
@@ -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,
};
+23 -1
View File
@@ -185,6 +185,14 @@ fn parent_folder(path: &str) -> Option<String> {
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<String> {
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<Option<(Strin
.as_object()
.into_iter()
.flatten()
.filter(|(k, v)| !(v.is_null() || matches!(k.as_str(), "path" | "format" | "options") || (svg && k.as_str() == "svg")))
.filter(|(k, v)| !(v.is_null() || matches!(k.as_str(), "path" | "format" | "options") || svg && k.as_str() == "svg"))
.map(|(k, v)| (k.clone(), v.clone()))
.collect();
if svg {
@@ -1020,6 +1028,20 @@ mod tests_folders {
assert_eq!(super::xdg_documents("XDG_MUSIC_DIR=\"$HOME/Musica\"", home), None);
}
/// #4122: File → Open starts in the folder of the last opened file.
#[test]
fn dialog_folder_is_the_nearest_existing_parent() {
let base = std::env::temp_dir().join(format!("vc-open-folder-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&base);
std::fs::create_dir_all(&base).unwrap();
let file = base.join("poster.svg");
assert_eq!(super::dialog_folder(&file.to_string_lossy()).map(PathBuf::from), Some(base.clone()));
let missing = base.join("gone").join("card.ai");
assert_eq!(super::dialog_folder(&missing.to_string_lossy()).map(PathBuf::from), Some(base.clone()));
assert!(super::dialog_folder("poster.svg").is_none());
let _ = std::fs::remove_dir_all(&base);
}
/// #703: the Templates folder a dialog starts in exists: made when missing, else the nearest
/// folder above it.
#[test]
+8 -1
View File
@@ -189,13 +189,20 @@ pub fn install_module(app: &mut VectorcraftApp, module: Value) -> Result<Value,
Ok(r)
}
/// Where File → Open… starts: the folder of the most recently opened file, else the active
/// document's (#4122).
fn open_dialog_folder(app: &VectorcraftApp) -> Option<String> {
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(|_| ())
}
+17 -1
View File
@@ -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.