M12: a portfolio's initial document opens only while the portfolio is still in front; doc comments for the portfolio code and tests (#1652)

This commit is contained in:
Eric Kaden committed 2026-10-29 03:44:27 -04:00
1 parent 751720732a
commit d9127da2e0
6 files changed
+45 -5

No files matched your search

@@ -4,6 +4,7 @@
use pdfcraft_automation::{Automation, Content};
use serde_json::{Value, json};
/// Call tool `name` and return its JSON result.
fn tool(a: &mut Automation, name: &str, args: Value) -> Value {
match a.call(name, &args).unwrap().remove(0) {
Content::Json(value) => value,
@@ -34,6 +35,7 @@ trailer << /Root 1 0 R >>
.into_bytes()
}
/// `doc_info` names a portfolio's initial document and view, and reports none for a plain file.
#[test]
fn doc_info_names_the_document_a_portfolio_presents_first() {
let dir = tempfile::tempdir().unwrap();
+3
View File
@@ -161,6 +161,8 @@ impl<'a> Inspector<'a> {
me
}
/// Read the document's catalog and pages into `info`: metadata, outline, fields, layers,
/// fonts, attachments, a portfolio and where it opens.
pub(super) fn fill(&self, info: &mut DocInfo) {
// lopdf drops /Encrypt from the trailer once it has decrypted the file.
info.encrypted = self.doc.encrypted();
@@ -289,6 +291,7 @@ impl<'a> Inspector<'a> {
Portfolio { initial, view }
}
/// The object a reference points at, or `o` itself for a direct object or a broken reference.
pub(super) fn resolve(&self, o: &'a Object) -> &'a Object {
self.doc.dereference(o).map(|(_, o)| o).unwrap_or(o)
}
@@ -26,10 +26,13 @@ trailer << /Root 1 0 R >>
.into_bytes()
}
/// The portfolio [`portfolio`] with `collection` reads as.
fn read(collection: Option<&str>) -> Option<Portfolio> {
inspect(Arc::new(portfolio(collection)), None).expect("opens").portfolio
}
/// `/D` picks the attachment presented first, matched as the keys decode, and `/View` how the
/// list first shows.
#[test]
fn a_portfolio_presents_the_attachment_its_d_names() {
// `/D` names `b.pdf` in the keys' own UTF-16, then as a literal string: both decode as the
+8 -4
View File
@@ -247,6 +247,8 @@ impl PdfCraftApp {
self.finish_open(name, path, bytes.len(), prepared, activate).map(|_| ())
}
/// Admit a prepared document: its tab, the panels and view it opens with, a portfolio's file
/// list and initial document, the recent-files entry and what its scripts said on opening.
fn finish_open(
&mut self,
name: &str,
@@ -316,9 +318,9 @@ impl PdfCraftApp {
if opening_mode != Mode::Read && self.lists_files(id) {
self.show_portfolio_files(id, right_before);
}
if let Some(index) = portfolio_initial {
self.portfolio_initial = Some((id, index));
}
// A document opening in front of a portfolio that hasn't presented its initial
// document yet (files opened together) keeps the front.
self.portfolio_initial = portfolio_initial.map(|index| (id, index));
}
if activate
&& self.left_open
@@ -428,7 +430,9 @@ 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() {
if let Some((doc, index)) = self.portfolio_initial.take()
&& self.active_ids().is_some_and(|(_, id)| id == doc)
{
self.attachment_action(doc, index, true);
// A portfolio inside the portfolio waits until the reader opens it, so a nested
// file can't open tab after tab on its own.
+2 -1
View File
@@ -269,7 +269,8 @@ pub struct PdfCraftApp {
/// showing its comments or bookmarks (#1590). Remembered across restarts.
pub right_panel_closed: bool,
/// A portfolio that just opened and the attachment it presents first (`/Collection /D`),
/// opened by [`Self::present_portfolios`] on the next frame.
/// opened by [`Self::present_portfolios`] on the next frame while the portfolio is still the
/// active tab.
portfolio_initial: Option<(DocId, usize)>,
/// The document whose tab [`Self::present_portfolios`] last saw active.
portfolio_seen: Option<DocId>,
+27
View File
@@ -78,6 +78,8 @@ fn select(h: &mut Harness<'static, PdfCraftApp>, index: usize) {
h.run_steps(2);
}
/// The initial document opens in front, and the portfolio's tab lists its files whatever the
/// reader's panel preference, giving the panel back when left.
#[test]
fn a_portfolio_opens_its_initial_document_and_shows_its_files_with_the_panel_closed() {
let mut h = opened(portfolio("/Collection << /D (form.pdf) /View /T >>", &[("intro.pdf", PAGE), ("form.pdf", PAGE)]), true);
@@ -106,6 +108,7 @@ fn a_portfolio_opens_its_initial_document_and_shows_its_files_with_the_panel_clo
assert_eq!(h.state().right, Some(RightPanel::Bookmarks));
}
/// The initial document opens on its own panels, not on the portfolio's file list.
#[test]
fn the_initial_document_opens_without_the_portfolios_file_list() {
// The portfolio asks for the Attachments panel too (`/PageMode /UseAttachments`), and the
@@ -118,6 +121,28 @@ fn the_initial_document_opens_without_the_portfolios_file_list() {
assert_eq!(h.state().right, Some(RightPanel::Attachments));
}
/// A file opened together with a portfolio, after it, stays in front.
#[test]
fn a_file_opened_after_the_portfolio_keeps_the_front() {
// Two files opened at once, the portfolio first: the second stays in front, and the
// portfolio's initial document waits for the reader, whose list shows on the portfolio's tab.
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.right_panel_closed = true;
app.open_bytes("portfolio.pdf", None, pdf.clone()).unwrap();
app.open_bytes("other.pdf", None, PAGE.to_vec()).unwrap();
app
});
h.run_steps(4);
assert_eq!(tabs(&h), (vec!["portfolio.pdf".into(), "other.pdf".into()], Some("other.pdf".into())));
assert_eq!(h.state().right, None);
select(&mut h, 0);
assert_eq!(h.state().right, Some(RightPanel::Attachments));
}
/// `/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() {
let mut h = opened(portfolio("/Collection << /D (form.pdf) /View /H >>", &[("intro.pdf", PAGE), ("form.pdf", PAGE)]), true);
@@ -126,6 +151,7 @@ fn a_portfolio_that_keeps_its_list_hidden_still_presents_its_initial_document()
assert_eq!(h.state().right, None, "/View /H: the list shows when the reader asks for it");
}
/// Without a PDF to present, the portfolio stays in front with its files listed.
#[test]
fn a_portfolio_without_an_initial_document_shows_its_files_over_the_cover() {
// Without `/D` the cover is the initial document; a `/D` naming a file that isn't a PDF
@@ -137,6 +163,7 @@ fn a_portfolio_without_an_initial_document_shows_its_files_over_the_cover() {
}
}
/// A portfolio nested in the initial document doesn't open a chain of tabs.
#[test]
fn a_portfolio_inside_the_initial_document_waits_for_the_reader() {
// The initial document is itself a portfolio presenting a third file: opening the outer