diff --git a/crates/ui-egui/src/app/files.rs b/crates/ui-egui/src/app/files.rs index ba5b23105..4cd29d214 100644 --- a/crates/ui-egui/src/app/files.rs +++ b/crates/ui-egui/src/app/files.rs @@ -52,18 +52,21 @@ impl DesignApp { self.place_file(if append { json!({"path": path, "append": true}) } else { json!({"path": path}) }) } - /// Place an ordered batch of files, as File › Place with several chosen. The batch stops when - /// a file opens a dialog (a multi-page PDF's Import Options, an alert), as the inbox's does; - /// every file that loads the cursor adds to what the earlier ones loaded, in the order the - /// files were chosen (images in a row load together, before the file after them). A file that - /// can't be placed gets an alert and stops the batch; its error is returned, so automation - /// doesn't carry on as if the batch had loaded. + /// Place an ordered batch of files, as File › Place with several chosen. A multi-page PDF's + /// Import Options pause the batch: the files after it wait in the dialog and carry on when it + /// is confirmed, and Cancel ends the batch. Any other dialog (an alert, one already open) stops + /// it, as the inbox's does. Every file that loads the cursor adds to what the earlier ones + /// loaded, in the order the files were chosen (images in a row load together, before the file + /// after them). A file that can't be placed gets an alert and stops the batch; its error is + /// returned, so automation doesn't carry on as if the batch had loaded. pub(crate) fn place_paths(&mut self, paths: Vec) -> Result<(), String> { let mut images = Vec::new(); // Images already in the place cursor stay: the batch queues after them. let mut loaded = self.session.loaded.is_some(); - // `None` at the end loads the images left. - for path in paths.into_iter().map(Some).chain([None]) { + let mut paths = paths.into_iter(); + loop { + // `None` at the end loads the images left. + let path = paths.next(); if let Some(p) = &path && is_cursor_image(p) { @@ -71,7 +74,7 @@ impl DesignApp { continue; } if path.is_none() && images.is_empty() { - break; + return Ok(()); } if self.ui.dialog.is_some() { self.status(crate::i18n::tr(&self.ui.language, "Place cancelled while a dialog is open")); @@ -81,12 +84,14 @@ impl DesignApp { let r = self.load_place_cursor(std::mem::take(&mut images), loaded)?; loaded |= r.get("loaded").and_then(Value::as_bool) == Some(true); } - if let Some(p) = path { - let r = self.place_path(&p, loaded)?; - loaded |= r.get("loaded").and_then(Value::as_bool) == Some(true); + let Some(p) = path else { return Ok(()) }; + let r = self.place_path(&p, loaded)?; + loaded |= r.get("loaded").and_then(Value::as_bool) == Some(true); + if let Some(d) = self.ui.dialog.as_mut().filter(|d| d.id == "pdfImport") { + d.fields.insert(dialogs::PDF_IMPORT_REST.into(), json!(paths.collect::>())); + return Ok(()); } } - Ok(()) } /// `place.load` of `files` (`{path}` or `{name, base64}` each), queued after the images @@ -996,6 +1001,57 @@ pub(crate) mod browser_import_tests { assert_eq!(loaded_names(&app), ["a.png", "b.png", "three.pdf"]); } + /// The files after a multi-page PDF in a native batch wait for its Import Options, through a + /// second such PDF too, and keep the order chosen. + #[test] + fn native_batch_continues_after_a_multi_page_pdf_is_confirmed() { + let dir = tempfile::tempdir().unwrap(); + let path = |n: &str| dir.path().join(n).to_string_lossy().to_string(); + for name in ["a.svg", "b.svg", "c.svg", "d.svg"] { + std::fs::write(path(name), image()).unwrap(); + } + std::fs::write(path("three.pdf"), pdf_pages(3)).unwrap(); + std::fs::write(path("two.pdf"), pdf_pages(2)).unwrap(); + let mut app = app(); + app.services.read = Some(Box::new(|p| std::fs::read(p).map_err(|e| e.to_string()))); + let batch = ["a.svg", "three.pdf", "b.svg", "c.svg", "two.pdf", "d.svg"].map(path).to_vec(); + app.place_paths(batch).unwrap(); + assert_eq!(loaded_names(&app), ["a.svg"], "waiting for the first PDF's options"); + dialogs::confirm(&mut app).unwrap(); + assert_eq!(app.ui.dialog.as_ref().map(|d| d.id.as_str()), Some("pdfImport"), "the second PDF asks in its turn"); + assert_eq!(loaded_names(&app), ["a.svg", "three.pdf", "b.svg", "c.svg"]); + dialogs::confirm(&mut app).unwrap(); + assert!(app.ui.dialog.is_none()); + assert_eq!(loaded_names(&app), ["a.svg", "three.pdf", "b.svg", "c.svg", "two.pdf", "d.svg"]); + } + + /// A refused page number keeps the rest of the batch waiting; Cancel ends the batch, and what + /// loaded before the PDF stays. + #[test] + fn native_batch_waits_through_a_refused_page_and_ends_at_cancel() { + let dir = tempfile::tempdir().unwrap(); + let path = |n: &str| dir.path().join(n).to_string_lossy().to_string(); + std::fs::write(path("a.svg"), image()).unwrap(); + std::fs::write(path("b.svg"), image()).unwrap(); + std::fs::write(path("three.pdf"), pdf_pages(3)).unwrap(); + let mut app = app(); + app.services.read = Some(Box::new(|p| std::fs::read(p).map_err(|e| e.to_string()))); + let batch = ["a.svg", "three.pdf", "b.svg"].map(path).to_vec(); + app.place_paths(batch.clone()).unwrap(); + app.ui.dialog.as_mut().unwrap().fields.insert("page".into(), json!("4-5")); + assert!(dialogs::confirm(&mut app).is_err()); + app.ui.dialog.as_mut().unwrap().fields.insert("page".into(), json!("2")); + dialogs::confirm(&mut app).unwrap(); + assert_eq!(loaded_names(&app), ["a.svg", "three.pdf", "b.svg"]); + + let mut app = self::app(); + app.services.read = Some(Box::new(|p| std::fs::read(p).map_err(|e| e.to_string()))); + app.place_paths(batch).unwrap(); + dialogs::cancel(&mut app); + assert!(app.ui.dialog.is_none()); + assert_eq!(loaded_names(&app), ["a.svg"]); + } + /// A single multi-page PDF still replaces what the cursor holds, as any single Place does. #[test] fn a_single_multi_page_pdf_replaces_the_cursor_when_confirmed() { diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index 4633620f3..b104a0ae1 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -1562,6 +1562,9 @@ fn text_frame_params(app: &DesignApp, d: &Dialog) -> Result, Ok(p) } +/// In a native batch's `pdfImport` dialog: the paths chosen after its PDF, placed once it is confirmed. +pub(crate) const PDF_IMPORT_REST: &str = "rest"; + /// File › Document Setup, filled from the document. /// The active document's ruler unit (picas without a document). fn doc_unit(app: &DesignApp) -> Unit { @@ -4711,9 +4714,18 @@ fn confirm_part3(app: &mut DesignApp, d: Dialog) -> Result { if let Some(target) = d.fields.get("target").and_then(Value::as_u64) { app.activate_import_target(&crate::ImportRequest { purpose: "place".into(), target: Some(target) })?; } - app.place_file( + let placed = app.place_file( json!({"path": d.s("path"), "pdfPage": page, "pdfCrop": if crop.is_empty() { "crop".to_string() } else { crop }, "append": d.b("append")}), - ) + )?; + // The files chosen after this PDF, which waited for its options. + let rest: Vec = d + .fields + .get(PDF_IMPORT_REST) + .and_then(Value::as_array) + .map(|paths| paths.iter().filter_map(Value::as_str).map(str::to_string).collect()) + .unwrap_or_default(); + app.place_paths(rest)?; + Ok(placed) } "print" => { let pages = if d.s("range") == "range" { json!(d.s("pages")) } else { Value::Null }; diff --git a/docs/multi-image-placement.md b/docs/multi-image-placement.md index 478ad6755..e64a26fbc 100644 --- a/docs/multi-image-placement.md +++ b/docs/multi-image-placement.md @@ -9,7 +9,7 @@ Choose **File > Place** (Cmd+D on macOS) and select several images, or drop a se - Press Escape or select another tool to cancel the remaining placement. - Switching to, opening or closing a document also cancels it, and goes back to the Selection tool. -A single selected file retains the normal Place behavior, including filling a selected frame and PDF import options. A multi-page PDF in a batch stops the batch at its import options; once confirmed, it queues after the images loaded before it. The place cursor holds up to 1000 images, counting those already in it when a batch adds to them. Rows and columns each range from 1 to 100. A batch containing an unreadable image fails without replacing the existing queue or changing the document. An invalid grid area also leaves the document and queue unchanged. +A single selected file retains the normal Place behavior, including filling a selected frame and PDF import options. A multi-page PDF in a batch pauses the batch at its import options; once confirmed, it queues after the images loaded before it and the rest of the batch follows. Cancelling them ends the batch, and the images already loaded stay. The place cursor holds up to 1000 images, counting those already in it when a batch adds to them. Rows and columns each range from 1 to 100. A batch containing an unreadable image fails without replacing the existing queue or changing the document. An invalid grid area also leaves the document and queue unchanged. ## Dropping files