mirror of
https://github.com/storytold/wordcraft.git
synced 2026-11-03 00:40:33 -06:00
Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
@@ -51,7 +51,10 @@ impl Launcher {
|
||||
let dialog = rfd::AsyncFileDialog::new().set_parent(parent);
|
||||
let picking: Picking = match req {
|
||||
FileDialogRequest::Open { purpose } => Box::pin(open_filters(dialog, &purpose).pick_file()),
|
||||
FileDialogRequest::Save { name } => Box::pin(dialog.set_file_name(name).save_file()),
|
||||
FileDialogRequest::Save { name } => {
|
||||
let exts = save_exts(&name);
|
||||
Box::pin(dialog.set_file_name(name).add_filter("Documents", &exts).save_file())
|
||||
}
|
||||
};
|
||||
let ctx = self.ctx.clone();
|
||||
let waiting = std::thread::Builder::new().name("file dialog".into()).spawn(move || {
|
||||
@@ -82,3 +85,43 @@ fn open_filters(d: rfd::AsyncFileDialog, purpose: &str) -> rfd::AsyncFileDialog
|
||||
.add_filter("All files", &["*"])
|
||||
}
|
||||
}
|
||||
|
||||
/// The file types Save As offers: every format the engine saves, with the suggested name's own
|
||||
/// format first. rfd makes a filter's first extension the dialog's default, which the Windows,
|
||||
/// macOS and Linux panels append to a file name the user typed without one; with no filter at
|
||||
/// all the name came back bare and the save failed, since the format is chosen by extension
|
||||
/// (#689).
|
||||
fn save_exts(name: &str) -> Vec<String> {
|
||||
let suggested = std::path::Path::new(name)
|
||||
.extension()
|
||||
.and_then(|e| e.to_str())
|
||||
.map(|e| e.to_ascii_lowercase())
|
||||
.filter(|e| wordcraft_engine::io::SAVE_EXTS.contains(&e.as_str()))
|
||||
.unwrap_or_else(|| "docx".to_string());
|
||||
let mut exts: Vec<String> = Vec::with_capacity(wordcraft_engine::io::SAVE_EXTS.len());
|
||||
exts.push(suggested.clone());
|
||||
exts.extend(wordcraft_engine::io::SAVE_EXTS.iter().filter(|e| **e != suggested).map(|e| (*e).to_string()));
|
||||
exts
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::save_exts;
|
||||
|
||||
/// #689: the Save dialog's default extension is the suggested name's own format, so a file
|
||||
/// name the user types without one is saved in the document's format instead of coming back
|
||||
/// bare and failing.
|
||||
#[test]
|
||||
fn save_dialog_defaults_to_the_suggested_format() {
|
||||
assert_eq!(save_exts("novel.docx")[0], "docx");
|
||||
assert_eq!(save_exts("report.odt")[0], "odt");
|
||||
assert_eq!(save_exts("annual report.pdf")[0], "pdf");
|
||||
// A suggested name always has an extension; anything unreadable falls back to Word's.
|
||||
assert_eq!(save_exts("no extension")[0], "docx");
|
||||
assert_eq!(save_exts("mystery.rtf2")[0], "docx");
|
||||
// Every savable format is still offered, once.
|
||||
let exts = save_exts("novel.docx");
|
||||
assert!(wordcraft_engine::io::SAVE_EXTS.iter().all(|e| exts.iter().any(|x| x == e)));
|
||||
assert_eq!(exts.len(), wordcraft_engine::io::SAVE_EXTS.len());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -321,6 +321,35 @@ mod tests {
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
/// #689: Save As honours the filename typed in the dialog, not just the folder: a document
|
||||
/// opened as `a/one.docx` and saved as `b/two.docx` is written there and takes that path.
|
||||
#[test]
|
||||
fn save_as_with_a_new_filename_saves_under_it() {
|
||||
let dir = scratch("rename");
|
||||
let opened = dir.join("one.docx");
|
||||
let picked = dir.join("sub").join("two.docx");
|
||||
std::fs::create_dir_all(dir.join("sub")).unwrap();
|
||||
wordcraft_engine::Session::new(wordcraft_doc::Document::from_text("original"))
|
||||
.run("file.save", &json!({"path": opened.to_string_lossy()}))
|
||||
.unwrap();
|
||||
let (mut a, shown) = async_app();
|
||||
a.run("file.open", json!({"path": opened.to_string_lossy()})).unwrap();
|
||||
assert_eq!(a.session.path.as_deref(), Some(opened.as_path()));
|
||||
a.save_as_dialog();
|
||||
answer(&shown, Some(&picked.to_string_lossy()));
|
||||
let r = a.poll_file_dialog().unwrap().unwrap();
|
||||
assert_eq!(r, json!({"saved": true}), "the save-as succeeded");
|
||||
assert_eq!(a.session.path.as_deref(), Some(picked.as_path()), "the picked filename is the document's path");
|
||||
assert!(picked.is_file(), "the file is written under the picked name");
|
||||
assert!(saved_text(&picked).contains("original"));
|
||||
// F12 (file.saveAs without a path) lands in the same dialog, suggesting the new name.
|
||||
let r = a.run("file.saveAs", json!({})).unwrap();
|
||||
assert_eq!(r, json!({"saved": false}));
|
||||
assert!(a.file_dialog_open(), "F12's saveAs request opens the dialog");
|
||||
assert_eq!(shown.borrow()[0].0, FileDialogRequest::Save { name: "two.docx".into() });
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
/// While a dialog is open, more Save As / Open / Export / Picture clicks don't open a second one.
|
||||
#[test]
|
||||
fn a_second_dialog_while_one_is_open_is_ignored() {
|
||||
|
||||
Reference in New Issue
Block a user