diff --git a/crates/ui-egui/src/windows.rs b/crates/ui-egui/src/windows.rs index 81fa5d584..6afcf1b1e 100644 --- a/crates/ui-egui/src/windows.rs +++ b/crates/ui-egui/src/windows.rs @@ -343,8 +343,9 @@ pub(crate) fn confirm_close(app: &mut FigApp, ctx: &Context) { return; } app.ui.close_after_save_as = None; + // Saved: go on with the other files, then close. if app.session.docs.iter().find(|d| d.uid == uid).is_none_or(|d| !d.is_dirty()) { - request_close(app, ctx); + save_then_close(app, ctx); } return; } @@ -374,6 +375,10 @@ pub(crate) fn confirm_close(app: &mut FigApp, ctx: &Context) { answer = Some(CloseAnswer::Save); } }); + // Save is the default: Enter answers it when no button has the keyboard. + if answer.is_none() && ui.memory(|m| m.focused().is_none()) && ui.input(|i| i.key_pressed(egui::Key::Enter)) { + answer = Some(CloseAnswer::Save); + } }); if !open { answer = Some(CloseAnswer::Cancel); diff --git a/crates/ui-egui/src/windows_tests.rs b/crates/ui-egui/src/windows_tests.rs index c5f93a29c..a4e29212b 100644 --- a/crates/ui-egui/src/windows_tests.rs +++ b/crates/ui-egui/src/windows_tests.rs @@ -171,16 +171,18 @@ fn a_request_for_a_window_on_screen_wakes_it_while_the_main_window_is_hidden() { assert!(woken.lock().unwrap().contains(&other), "the other window is asked to draw: {:?}", woken.lock().unwrap()); } -/// Answers Save as with `path` (`None`: cancelled) and counts the dialogs asked for. +/// Answers each Save as with the next of `paths` (none left: cancelled) and counts the dialogs +/// asked for. struct SaveAs { - path: Option, + paths: Vec, asked: std::rc::Rc>, } impl crate::Services for SaveAs { fn pick_save(&mut self, _title: &str, _name: &str, _exts: &[&str]) -> crate::DialogFuture { self.asked.set(self.asked.get() + 1); - Box::pin(std::future::ready(self.path.take())) + let next = if self.paths.is_empty() { None } else { Some(self.paths.remove(0)) }; + Box::pin(std::future::ready(next)) } } @@ -193,7 +195,7 @@ fn temp_dir(tag: &str) -> std::path::PathBuf { /// The main window with one file that has unsaved changes, saved at `path` first when given, and /// the window asked to close (the question shows). -fn asking(path: Option<&std::path::Path>, save_as: Option) -> (egui::Context, FigApp, std::rc::Rc>) { +fn asking(path: Option<&std::path::Path>, save_as: Vec) -> (egui::Context, FigApp, std::rc::Rc>) { let mut s = Session::new(); if let Some(p) = path { s.execute("file.saveAs", &json!({"path": p.to_string_lossy()})).unwrap(); @@ -201,7 +203,7 @@ fn asking(path: Option<&std::path::Path>, save_as: Option) - s.execute("shape.rectangle", &json!({"x": 0, "y": 0, "width": 10, "height": 10})).unwrap(); let asked = std::rc::Rc::new(std::cell::Cell::new(0)); let ctx = egui::Context::default(); - let mut app = FigApp::new(s, Box::new(SaveAs { path: save_as, asked: asked.clone() })); + let mut app = FigApp::new(s, Box::new(SaveAs { paths: save_as, asked: asked.clone() })); frame(&ctx, &mut app, false); frame(&ctx, &mut app, true); assert!(app.ui.confirm_close, "the question shows"); @@ -224,7 +226,7 @@ fn settle(ctx: &egui::Context, app: &mut FigApp) { #[test] fn the_unsaved_changes_question_offers_save_dont_save_and_cancel() { // As the other Craft apps ask it (#1170): Don't Save, Cancel, and Save as the default. - let (ctx, mut app, _) = asking(None, None); + let (ctx, mut app, _) = asking(None, vec![]); ctx.enable_accesskit(); frame(&ctx, &mut app, false); let out = ctx.run_ui(RawInput::default(), |ui| app.ui(ui)); @@ -241,7 +243,7 @@ fn the_unsaved_changes_question_offers_save_dont_save_and_cancel() { fn save_saves_a_file_with_a_path_and_then_closes() { let d = temp_dir("path"); let path = d.join("a.figcraft"); - let (ctx, mut app, asked) = asking(Some(&path), None); + let (ctx, mut app, asked) = asking(Some(&path), vec![]); assert_eq!(rects_in(&path), 0); crate::windows::answer_close(&mut app, &ctx, crate::windows::CloseAnswer::Save); settle(&ctx, &mut app); @@ -255,7 +257,7 @@ fn save_saves_a_file_with_a_path_and_then_closes() { fn save_asks_where_for_an_untitled_file_and_closes_once_saved() { let d = temp_dir("untitled"); let path = d.join("new.figcraft"); - let (ctx, mut app, asked) = asking(None, Some(path.clone())); + let (ctx, mut app, asked) = asking(None, vec![path.clone()]); crate::windows::answer_close(&mut app, &ctx, crate::windows::CloseAnswer::Save); settle(&ctx, &mut app); assert_eq!(asked.get(), 1, "Save as asks where"); @@ -267,7 +269,7 @@ fn save_asks_where_for_an_untitled_file_and_closes_once_saved() { #[test] fn a_cancelled_save_as_or_a_failed_save_keeps_the_window_open() { // Save as cancelled: nothing is written, the file keeps its changes and the window stays. - let (ctx, mut app, asked) = asking(None, None); + let (ctx, mut app, asked) = asking(None, vec![]); crate::windows::answer_close(&mut app, &ctx, crate::windows::CloseAnswer::Save); settle(&ctx, &mut app); assert_eq!(asked.get(), 1); @@ -278,7 +280,7 @@ fn a_cancelled_save_as_or_a_failed_save_keeps_the_window_open() { // The save fails (its folder is gone): the error shows and the window stays. let d = temp_dir("gone"); let path = d.join("a.figcraft"); - let (ctx, mut app, _) = asking(Some(&path), None); + let (ctx, mut app, _) = asking(Some(&path), vec![]); std::fs::remove_dir_all(&d).unwrap(); crate::windows::answer_close(&mut app, &ctx, crate::windows::CloseAnswer::Save); settle(&ctx, &mut app); @@ -290,7 +292,7 @@ fn a_cancelled_save_as_or_a_failed_save_keeps_the_window_open() { fn dont_save_closes_and_cancel_keeps_the_window() { let d = temp_dir("answers"); let path = d.join("a.figcraft"); - let (ctx, mut app, _) = asking(Some(&path), None); + let (ctx, mut app, _) = asking(Some(&path), vec![]); crate::windows::answer_close(&mut app, &ctx, crate::windows::CloseAnswer::Cancel); settle(&ctx, &mut app); assert!(!app.ui.quit && !app.ui.confirm_close); @@ -301,3 +303,43 @@ fn dont_save_closes_and_cancel_keeps_the_window() { assert_eq!(rects_in(&path), 0, "without saving"); std::fs::remove_dir_all(&d).ok(); } + +#[test] +fn one_save_saves_every_untitled_file_of_the_window_then_closes() { + // Two new files with changes: Save asks where for each in turn, then the window closes, + // without asking the question again in between (#1173 review). + let d = temp_dir("two"); + let (a, b) = (d.join("a.figcraft"), d.join("b.figcraft")); + let (ctx, mut app, asked) = asking(None, vec![a.clone(), b.clone()]); + app.session.execute("file.new", &json!({"name": "Second"})).unwrap(); + app.session.execute("shape.rectangle", &json!({"x": 0, "y": 0, "width": 10, "height": 10})).unwrap(); + crate::windows::answer_close(&mut app, &ctx, crate::windows::CloseAnswer::Save); + for _ in 0..8 { + frame(&ctx, &mut app, false); + assert!(!app.ui.confirm_close, "not asked again"); + } + assert_eq!(asked.get(), 2); + assert_eq!((rects_in(&a), rects_in(&b)), (1, 1), "both saved"); + assert!(app.ui.quit); + std::fs::remove_dir_all(&d).ok(); +} + +#[test] +fn enter_answers_save() { + let d = temp_dir("enter"); + let path = d.join("a.figcraft"); + let (ctx, mut app, _) = asking(Some(&path), vec![]); + frame(&ctx, &mut app, false); + let enter = |pressed| egui::Event::Key { key: egui::Key::Enter, physical_key: None, pressed, repeat: false, modifiers: egui::Modifiers::NONE }; + let input = RawInput { events: vec![enter(true), enter(false)], ..Default::default() }; + ctx.run_ui(input, |ui| { + crate::windows::on_close_request(&mut app, ui.ctx()); + app.ui(ui); + }) + .textures_delta + .clear(); + settle(&ctx, &mut app); + assert_eq!(rects_in(&path), 1, "Enter saved"); + assert!(app.ui.quit); + std::fs::remove_dir_all(&d).ok(); +}