From a9c3d842737880da56ca3a9a188be1ceb4ff7192 Mon Sep 17 00:00:00 2001 From: Eric Kaden <608341+EricKaden@users.noreply.github.com> Date: Thu, 29 Oct 2026 04:08:31 -0400 Subject: [PATCH] M12: a portfolio's initial document waits for a prompt, picker or front-bound open in progress, so a password asked for a file opened with it is kept (#1652) --- crates/ui-egui/src/app/files.rs | 11 +++++++- crates/ui-egui/tests/it/portfolio.rs | 41 ++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/crates/ui-egui/src/app/files.rs b/crates/ui-egui/src/app/files.rs index 90dbf7025c..1da4a191d5 100644 --- a/crates/ui-egui/src/app/files.rs +++ b/crates/ui-egui/src/app/files.rs @@ -430,7 +430,16 @@ impl PdfCraftApp { /// Leaving the tab gives back the panel the list replaced, unless the reader changed panels /// meanwhile. Called every frame. pub(crate) fn present_portfolios(&mut self) { - if let Some((doc, index)) = self.portfolio_initial.take() + // A prompt or picker over the window belongs to the document it was raised for (the + // password of a file opened together with the portfolio, say): the initial document + // waits for its answer, and opening it then would close the prompt. A file still + // opening in the background comes to the front when it arrives, so it is waited for too. + #[cfg(not(target_arch = "wasm32"))] + let busy = self.modal_open() || self.pickers.pending() || self.pending_opens.iter().any(|p| p.activate); + #[cfg(target_arch = "wasm32")] + let busy = self.modal_open(); + if !busy + && let Some((doc, index)) = self.portfolio_initial.take() && self.active_ids().is_some_and(|(_, id)| id == doc) { self.attachment_action(doc, index, true); diff --git a/crates/ui-egui/tests/it/portfolio.rs b/crates/ui-egui/tests/it/portfolio.rs index d8dd2726b3..2a7c91a710 100644 --- a/crates/ui-egui/tests/it/portfolio.rs +++ b/crates/ui-egui/tests/it/portfolio.rs @@ -22,6 +22,21 @@ const NOTED: &[u8] = b"%PDF-1.7 trailer << /Root 1 0 R >> %%EOF"; +/// [`PAGE`] encrypted, opening with the password `pw`. +fn locked() -> Vec { + let mut doc = pdfcraft_cos::Document::open(std::sync::Arc::new(PAGE.to_vec())).unwrap(); + doc.set_encryption(&pdfcraft_cos::NewEncryption { + algorithm: pdfcraft_cos::Algorithm::Aes256, + user_password: "pw", + owner_password: "owner", + permissions: -1, + encrypt_metadata: true, + seed: [4; 32], + }) + .unwrap(); + pdfcraft_cos::write_full(&doc, &Default::default()).unwrap() +} + /// A one-page portfolio of `files` (name, bytes), with `catalog` (its `/Collection` and any other /// entries) in its catalog. fn portfolio(catalog: &str, files: &[(&str, &[u8])]) -> Vec { @@ -142,6 +157,32 @@ fn a_file_opened_after_the_portfolio_keeps_the_front() { assert_eq!(h.state().right, Some(RightPanel::Attachments)); } +/// A password asked for a file opened together with the portfolio stays asked: the initial +/// document waits for the answer, then gives way to that file, or opens once the prompt is +/// cancelled. +#[test] +fn a_password_asked_for_a_file_opened_with_the_portfolio_stays_asked() { + for answer in [Some("pw".to_string()), None] { + let pdf = portfolio("/Collection << /D (form.pdf) >>", &[("form.pdf", PAGE)]); + let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(move |_cc| { + let mut app = PdfCraftApp::new(); + app.set_option("language", "en").unwrap(); + app.open_bytes("portfolio.pdf", None, pdf.clone()).unwrap(); + app.open_bytes("locked.pdf", None, locked()).unwrap(); + app + }); + h.run_steps(4); + assert!(h.state().password_prompt.is_some(), "the prompt for locked.pdf is still up"); + assert_eq!(tabs(&h).0, ["portfolio.pdf"]); + let opened = if answer.is_some() { "locked.pdf" } else { "form.pdf" }; + // The answered file opens in the background worker. + h.state_mut().submit_password(answer); + crate::support::wait_until(&mut h, "a second tab", |app| app.views.len() == 2); + h.run_steps(4); + assert_eq!(tabs(&h), (vec!["portfolio.pdf".into(), opened.into()], Some(opened.into()))); + } +} + /// `/View /H` presents the initial document but leaves the file list to the reader. #[test] fn a_portfolio_that_keeps_its_list_hidden_still_presents_its_initial_document() {