Merge pull request #7224 from dotCooCoo/fix/close-modified-background-tab-keeps-front

Documents: closing a modified background tab keeps the document in front
This commit is contained in:
BFlat
2026-11-14 02:39:19 +07:00
committed by GitHub
3 changed files with 137 additions and 6 deletions
+22
View File
@@ -862,6 +862,28 @@ fn save_changes_answers_for_its_own_document() {
assert_eq!((d.kind.as_str(), &d.fields["uid"]), (crate::unsaved::KIND, &json!(uids[2])));
}
/// Closing a modified background tab whose Save opens a Save dialog: once the dialog answers,
/// the document closes and the document that was in front before the question is in front again.
#[test]
fn save_changes_answered_by_a_save_dialog_keeps_the_front_document() {
let (mut app, shown, written) = with_dialogs(false);
// A clean and in front, B modified and untitled, C clean.
app.run("file.new", json!({"width": 100, "height": 80})).unwrap();
app.run("file.new", json!({"width": 100, "height": 80})).unwrap();
app.run("shape.rectangle", json!({"x": 0, "y": 0, "width": 10, "height": 10})).unwrap();
app.run("file.new", json!({"width": 100, "height": 80})).unwrap();
app.session.set_active(0);
let uids: Vec<u64> = app.session.documents().iter().map(|d| d.uid).collect();
app.run("file.close", json!({"index": 1})).unwrap();
assert!(crate::dialogs::confirm(&mut app).is_err());
assert!(matches!(shown.requests().as_slice(), [PickRequest::Save(_)]));
shown.answer(&["/art/b.vectorcraft"]);
poll(&mut app);
assert_eq!(*written.borrow(), ["/art/b.vectorcraft"], "{}", app.ui.status);
assert_eq!(app.session.documents().iter().map(|d| d.uid).collect::<Vec<_>>(), [uids[0], uids[2]]);
assert_eq!(app.session.active().map(|d| d.uid), Some(uids[0]), "A is in front again");
}
/// A dialog asked for while another is open is refused, and the status bar says so rather than
/// "cancelled".
#[test]
+114 -6
View File
@@ -14,9 +14,15 @@ use crate::{VectorcraftApp, io};
/// Dialog kind. Fields: `index` (document), `uid` (the same document, which `index` may no longer
/// point at once a file dialog answers), `name` (its title), `then` (`close`, `closeAll` or `quit`:
/// what continues after this document) and `discard` (Don't Save).
/// what continues after this document) and `discard` (Don't Save). With `then` = `close`, the
/// protected field [`FRONT`] holds the uid of the document that was in front before the question.
pub const KIND: &str = "saveChanges";
/// Field of a [`KIND`] dialog asked by File → Close or a tab's ×: the uid of the document in front
/// before the question showed the asked one. Once the asked document closes, that document is in
/// front again while it is still open.
const FRONT: &str = "__front";
/// Nothing closes while a file dialog's sheet is on the window (macOS): what asked for it runs
/// again when it answers. (Elsewhere the dialog is a window of its own, and closing goes on.)
pub(crate) fn refuse_while_picking(app: &VectorcraftApp) -> Result<(), String> {
@@ -37,7 +43,7 @@ pub fn close(app: &mut VectorcraftApp, i: usize) -> Result<Value, String> {
refuse_while_picking(app)?;
finish_pending(app);
let dirty = app.session.documents().get(i).ok_or("no such document")?.is_dirty();
if dirty { ask(app, i, "close") } else { close_now(app, i) }
if dirty { ask(app, i, "close") } else { close_now(app, i, None) }
}
/// File → Close All (`then` = `closeAll`) or Quit (`quit`): ask about the next modified document,
@@ -68,18 +74,28 @@ pub fn any_dirty(app: &VectorcraftApp) -> bool {
app.session.documents().iter().any(|d| d.is_dirty())
}
/// Show document `i` and ask whether to save it.
/// Show document `i` and ask whether to save it. For a single close (`then` = `close`), the
/// dialog also remembers the document in front before it ([`FRONT`]).
fn ask(app: &mut VectorcraftApp, i: usize, then: &str) -> Result<Value, String> {
let front = app.session.active().map(|d| d.uid);
app.session.set_active(i);
let doc = &app.session.documents()[i];
let (name, uid) = (doc.title(), doc.uid);
app.ui.dialog = Some(Dialog::new(KIND, json!({ "index": i, "uid": uid, "name": name, "then": then })));
let mut fields = json!({ "index": i, "uid": uid, "name": name, "then": then });
if let Some(front) = front.filter(|f| then == "close" && *f != uid) {
fields[FRONT] = json!(front);
}
app.ui.dialog = Some(Dialog::new(KIND, fields));
Ok(json!({ "pending": KIND }))
}
/// Close document `i` through the engine command; [`VectorcraftApp::sync_views`] drops its view.
fn close_now(app: &mut VectorcraftApp, i: usize) -> Result<Value, String> {
/// When `front` names a document that is still open, that document is in front afterwards.
fn close_now(app: &mut VectorcraftApp, i: usize, front: Option<u64>) -> Result<Value, String> {
let r = app.session.execute("file.close", &json!({ "index": i })).map_err(|e| e.to_string())?;
if let Some(j) = front.and_then(|uid| app.session.documents().iter().position(|d| d.uid == uid)) {
app.session.set_active(j);
}
app.sync_views();
if app.session.tool_id() == "artboard"
&& let Some(n) = app.session.active().map(|d| d.doc.artboards.len())
@@ -117,7 +133,7 @@ pub fn confirm(app: &mut VectorcraftApp) -> Result<Value, String> {
}
}
}
close_now(app, i)?;
close_now(app, i, d.fields.get(FRONT).and_then(Value::as_u64))?;
match d.str("then").as_str() {
"close" => Ok(Value::Null),
then => close_all(app, then),
@@ -195,6 +211,98 @@ mod tests {
assert_eq!((docs(&app), app.views.len()), (1, 1));
}
/// Three documents, A clean, B modified and C clean, with A in front: their uids.
fn front_and_modified_background_tab(app: &mut VectorcraftApp) -> [u64; 3] {
new_doc(app, false);
new_doc(app, true);
new_doc(app, false);
app.session.set_active(0);
let uids: Vec<u64> = app.session.documents().iter().map(|d| d.uid).collect();
[uids[0], uids[1], uids[2]]
}
fn front_uid(app: &VectorcraftApp) -> Option<u64> {
app.session.active().map(|d| d.uid)
}
#[test]
fn closing_a_modified_background_tab_keeps_the_front_document_after_dont_save() {
let (mut app, written) = app();
let [a, _, c] = front_and_modified_background_tab(&mut app);
assert_eq!(app.run("file.close", json!({ "index": 1 })).unwrap(), json!({ "pending": KIND }));
// The question shows the document it asks about.
assert_eq!((pending(&app), app.session.active_index()), (Some(1), Some(1)));
app.ui.dialog.as_mut().unwrap().fields.insert("discard".into(), json!(true));
confirm(&mut app).unwrap();
let uids: Vec<u64> = app.session.documents().iter().map(|d| d.uid).collect();
assert_eq!((uids, front_uid(&app), app.views.len()), (vec![a, c], Some(a), 2));
assert!(written.lock().unwrap().is_empty());
}
#[test]
fn closing_a_modified_background_tab_keeps_the_front_document_after_save() {
let (mut app, written) = app();
let [a, _, c] = front_and_modified_background_tab(&mut app);
close(&mut app, 1).unwrap();
confirm(&mut app).unwrap();
assert_eq!(*written.lock().unwrap(), vec!["out.vectorcraft".to_string()]);
let uids: Vec<u64> = app.session.documents().iter().map(|d| d.uid).collect();
assert_eq!((uids, front_uid(&app)), (vec![a, c], Some(a)));
}
#[test]
fn cancel_keeps_the_asked_background_tab_in_front() {
let (mut app, _) = app();
let [_, b, _] = front_and_modified_background_tab(&mut app);
close(&mut app, 1).unwrap();
app.ui.dialog = None;
assert_eq!((docs(&app), front_uid(&app)), (3, Some(b)));
// A later close of the tab in front brings its neighbor to the front.
close(&mut app, 1).unwrap();
app.ui.dialog.as_mut().unwrap().fields.insert("discard".into(), json!(true));
confirm(&mut app).unwrap();
assert_eq!((docs(&app), app.session.active_index()), (2, Some(1)));
}
#[test]
fn closing_the_modified_front_document_brings_its_neighbor_to_the_front() {
let (mut app, _) = app();
new_doc(&mut app, false);
new_doc(&mut app, true);
new_doc(&mut app, false);
let c = app.session.documents()[2].uid;
app.session.set_active(1);
close(&mut app, 1).unwrap();
assert!(app.ui.dialog.as_ref().is_some_and(|d| !d.fields.contains_key(FRONT)));
app.ui.dialog.as_mut().unwrap().fields.insert("discard".into(), json!(true));
confirm(&mut app).unwrap();
assert_eq!((docs(&app), front_uid(&app)), (2, Some(c)));
}
/// The document in front before the question is remembered by its uid: when it closed while
/// the question was open, the closed document's neighbor comes to the front as before.
#[test]
fn a_front_document_closed_meanwhile_is_not_brought_back() {
let (mut app, _) = app();
let [a, _, c] = front_and_modified_background_tab(&mut app);
close(&mut app, 1).unwrap();
let d = app.ui.dialog.take().unwrap();
assert_eq!(d.fields.get(FRONT), Some(&json!(a)));
close(&mut app, 0).unwrap();
app.ui.dialog = Some(d);
app.ui.dialog.as_mut().unwrap().fields.insert("discard".into(), json!(true));
confirm(&mut app).unwrap();
assert_eq!((docs(&app), front_uid(&app)), (1, Some(c)));
}
#[test]
fn close_all_does_not_remember_a_front_document() {
let (mut app, _) = app();
front_and_modified_background_tab(&mut app);
app.run("file.closeAll", json!({})).unwrap();
assert!(app.ui.dialog.as_ref().is_some_and(|d| d.kind == KIND && !d.fields.contains_key(FRONT)));
}
#[test]
fn a_cancelled_save_as_keeps_the_document_open() {
let (mut app, _) = app();
+1
View File
@@ -927,6 +927,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
- The canvas context menu lists Object › Path › Smooth… while the selection holds a path, in a group or a compound path too. It opens the Smooth slider, as the Object menu does (`right_click_on_a_path_opens_the_smooth_slider`, `smooth_follows_the_selections_paths`).
- Native macOS save sheets offer a localised Format menu (#866), built from the request's existing format filters. It selects the suggested filename's format and updates the suffix when changed, including legacy `.drawcraft` names. `apps/vectorcraft/src/mac_save_format.rs` has filename regressions; an actual native SVG save, selection of PDF and cancellation without changing the saved document were verified.
- Save / Don't Save / Cancel before closing or quitting with unsaved documents (tabs, Close, Close All, Quit, window close).
- Closing a modified tab that is not in front shows it while asking to save; after Save or Don't Save the document that was in front before the question is in front again, and Cancel keeps the asked document in front. Tests: `closing_a_modified_background_tab_keeps_the_front_document_after_dont_save`, `closing_a_modified_background_tab_keeps_the_front_document_after_save`, `cancel_keeps_the_asked_background_tab_in_front` and `save_changes_answered_by_a_save_dialog_keeps_the_front_document`.
- Four brightness themes, persistent preferences.
- **Snapping quick access (#6164):** Control bar › Snapping includes Snap to Glyph (`view.snapToGlyph`) and Alignment Guides (`prefs.set`, `alignmentGuides`). The headless pointer/accessibility test `the_control_bar_snapping_menu_shares_glyph_and_alignment_settings` covers both commands, independent state, checked states after reopening, and all eight menu rows within a 1200 × 400-point window. Floating-panel parity remains unverified; scores and estimates unchanged.
- **Pixel Snapping Options:** Control bar › Snapping › Pixel Snapping Options… sets whether View › Snap to Pixel snaps drawing, moving and scaling, each on by default (`view.snapToPixel.options`, the `pixelSnap` preference group; [the help page](https://helpx.adobe.com/illustrator/desktop/measure-and-align/grids-and-guides/draw-pixel-perfect-art.html)). Tests: `pixel_snapping_options_read_set_and_reset`, `pixel_snapping_options_choose_the_edits_snap_to_pixel_rounds`, `handle_drag_follows_the_while_scaling_option`, `move_follows_the_while_moving_option`, `an_anchor_drag_follows_the_while_moving_option`, `new_ruler_guide_follows_the_while_moving_option` and `the_control_bar_snapping_menu_opens_pixel_snapping_options`. Scores and estimates unchanged.