diff --git a/crates/automation/src/a11y.rs b/crates/automation/src/a11y.rs index 2124549731..a7c05e14e9 100644 --- a/crates/automation/src/a11y.rs +++ b/crates/automation/src/a11y.rs @@ -656,6 +656,7 @@ fn request_json(r: &pdfcraft_engine::js::Request) -> Value { R::Submit(u) => json!({ "submit": u }), R::Focus(f) => json!({ "focus": f }), R::Beep => json!({ "beep": true }), + R::Highlight(on) => json!({ "highlight": on }), R::SaveAs => json!({ "save_as": true }), } } diff --git a/crates/engine/src/js.rs b/crates/engine/src/js.rs index b797bc8d83..84a3dc6b4d 100644 --- a/crates/engine/src/js.rs +++ b/crates/engine/src/js.rs @@ -111,8 +111,15 @@ pub struct JsRunner { } impl JsRunner { - pub fn new(cos: &pdfcraft_cos::Document, file_name: &str) -> JsRunner { - let doc = pdfcraft_js::DocInfo { file_name: file_name.to_string(), num_pages: pdfcraft_model::pages(cos).len(), page: 0, info: info(cos) }; + /// `highlight`: whether the viewer highlights form fields (`app.runtimeHighlight`). + pub fn new(cos: &pdfcraft_cos::Document, file_name: &str, highlight: bool) -> JsRunner { + let doc = pdfcraft_js::DocInfo { + file_name: file_name.to_string(), + num_pages: pdfcraft_model::pages(cos).len(), + page: 0, + info: info(cos), + highlight, + }; JsRunner { doc, doc_scripts: pdfcraft_forms::document_scripts(cos), output: JsOutput::default(), cos: cos.clone() } } } @@ -175,6 +182,13 @@ impl Session { !self.js_off } + /// Whether the viewer highlights form fields (Highlight fields), which forms' scripts read as + /// `app.runtimeHighlight`. A script that changes it asks for [`Request::Highlight`]; the + /// viewer then calls this with the new setting. + pub fn set_field_highlight(&mut self, on: bool) { + self.field_highlight = on; + } + /// What scripts printed or asked for since the last call (alerts, console, print, …). pub fn take_js_output(&mut self, id: DocId) -> JsOutput { self.docs.iter_mut().find(|d| d.id == id).map(|d| std::mem::take(&mut d.js_output)).unwrap_or_default() @@ -205,7 +219,7 @@ impl Session { ..Default::default() }); } - let runner = JsRunner::new(&cos, &doc.name); + let runner = JsRunner::new(&cos, &doc.name, self.field_highlight); let fields = pdfcraft_forms::fields(&cos); let states: Vec<_> = fields.iter().map(|f| field_state(&cos, f)).collect(); let event = match target { diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index dfe11b25c6..dbd35e55df 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -2252,6 +2252,8 @@ struct EditCtx { xfa_removed: Vec, /// Pages in the document, for `xfa.layout.pageCount()`. pages: usize, + /// Whether the viewer highlights form fields, for `app.runtimeHighlight`. + highlight: bool, date: Option, /// Today in local time, for date tokens. today: (i64, u32, u32), @@ -2281,6 +2283,7 @@ impl EditCtx { xfa_ran_away: false, xfa_removed: Vec::new(), pages: 0, + highlight: false, date: now.map(pdfcraft_cos::pdf_date), today: (1970, 1, 1), seed, @@ -2438,7 +2441,8 @@ fn run_edit(doc: &mut pdfcraft_cos::Document, edit: &Edit, cx: &mut EditCtx) -> }, Edit::XfaEvent { som, activity } => { let tpl = cx.xfa.clone().ok_or_else(|| EditError::Invalid("this is not a laid-out XFA form, or JavaScript is off".into()))?; - let mut run = xfa::XfaRun { page_count: cx.pages, out: &mut cx.xfa_out, datasets: cx.xfa_datasets, ran_away: false }; + let mut run = + xfa::XfaRun { page_count: cx.pages, out: &mut cx.xfa_out, datasets: cx.xfa_datasets, ran_away: false, highlight: cx.highlight }; let ran = xfa::on_event(doc, &tpl, som, activity, &mut run); cx.xfa_datasets = run.datasets; cx.xfa_ran_away |= run.ran_away; @@ -2935,6 +2939,9 @@ pub struct Session { date_format: Option, /// Preferences ▸ Date format ▸ Language, when not following the interface language. date_language: Option, + /// Highlight fields, as the viewer shows it: what forms' scripts read as + /// `app.runtimeHighlight` (off by default, as in a session without a viewer). + field_highlight: bool, } /// Immutable session state needed while a document is prepared away from its owning thread. @@ -2943,6 +2950,7 @@ pub struct OpenContext { now: Option, js_off: bool, trust: Arc, + highlight: bool, } /// A fully prepared document that has not yet been registered in a session. @@ -3177,7 +3185,7 @@ fn image_content_type(bytes: &[u8]) -> Option<&'static str> { /// fields take the data's values. fn xfa_after_data_change(doc: &mut pdfcraft_cos::Document, cx: &mut EditCtx, calculate: bool) -> Result<(), EditError> { if let Some(tpl) = cx.xfa.clone() { - let mut run = xfa::XfaRun { page_count: cx.pages, out: &mut cx.xfa_out, datasets: cx.xfa_datasets, ran_away: false }; + let mut run = xfa::XfaRun { page_count: cx.pages, out: &mut cx.xfa_out, datasets: cx.xfa_datasets, ran_away: false, highlight: cx.highlight }; let r = guard(|| xfa::on_data_replaced(doc, &tpl, calculate, &mut run)) .unwrap_or_else(|m| Err(format!("laying the form out again failed unexpectedly ({m})"))); cx.xfa_datasets = run.datasets; @@ -3337,7 +3345,7 @@ impl Session { /// Snapshot the session state that affects open-time scripts, dates and signature trust. pub fn open_context(&self) -> OpenContext { - OpenContext { now: self.now(), js_off: self.js_off, trust: self.trust.clone() } + OpenContext { now: self.now(), js_off: self.js_off, trust: self.trust.clone(), highlight: self.field_highlight } } /// Prepare a document without adding it to this session. @@ -3466,7 +3474,8 @@ impl Session { let mut work = cos.clone(); let pages = info.pages.len(); let ran = guard(|| { - let mut run = xfa::XfaRun { page_count: pages, out: &mut script_output, datasets: None, ran_away: false }; + let mut run = + xfa::XfaRun { page_count: pages, out: &mut script_output, datasets: None, ran_away: false, highlight: context.highlight }; let changes = xfa::on_open(&mut work, &tpl, &mut run); if run.ran_away { ran_away = true; @@ -3640,6 +3649,7 @@ impl Session { let now = self.now(); let today = self.today(); let js_off = self.js_off; + let highlight = self.field_highlight; let doc = self.doc_mut(id)?; doc.edit_notices.clear(); let name = doc.name.clone(); @@ -3649,6 +3659,7 @@ impl Session { cx.xfa = doc.xfa_template.clone(); } cx.pages = doc.info.pages.len(); + cx.highlight = highlight; cx.today = today; let reason = doc.read_only_reason.clone().unwrap_or_default(); let signed = doc.is_signed(); @@ -3679,7 +3690,7 @@ impl Session { } let mut next = editor.cos.clone(); if !js_off && uses_scripts(&edit) { - cx.js = Some(js::JsRunner::new(&next, &name)); + cx.js = Some(js::JsRunner::new(&next, &name, highlight)); } cx.renamed_fields.clear(); // `next` is a copy: if the edit fails or crashes, the document is unchanged. @@ -3744,7 +3755,8 @@ impl Session { let mut reset = false; collect(&edit, &mut changed, &mut reset); if !changed.is_empty() || reset { - let mut run = xfa::XfaRun { page_count: cx.pages, out: &mut cx.xfa_out, datasets: cx.xfa_datasets, ran_away: false }; + let mut run = + xfa::XfaRun { page_count: cx.pages, out: &mut cx.xfa_out, datasets: cx.xfa_datasets, ran_away: false, highlight: cx.highlight }; // Every changed field's scripts, then the calculations once (after a reset, // only those). let ran = guard(|| xfa::on_changes(&mut next, &tpl, &changed, &mut run)) diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index f125c41dac..4ceefebbef 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -6106,3 +6106,20 @@ fn undo_never_makes_an_unsaved_document_clean() { s.undo(new).unwrap(); assert!(!s.get(new).unwrap().dirty, "once saved, undo back to the saved state is clean"); } + +#[test] +fn a_forms_layout_ready_scripts_run_once_it_is_laid_out() { + let bytes = Arc::new(pdfcraft_xfa::fixtures::shell(&pdfcraft_xfa::fixtures::layout_ready_template())); + let mut s = Session::new(); + let id = s.open("ready.pdf", None, bytes.clone(), None).expect("opens"); + let value = |s: &Session, n: &str| s.get(id).unwrap().form.iter().find(|f| f.name == n).map(|f| f.value.clone()).unwrap_or_default(); + let out = s.take_js_output(id); + assert_eq!(out.requests, vec![js::Request::Highlight(true)], "the form turns field highlighting on: {out:?}"); + assert_eq!(value(&s, "stamp"), vec!["laid out".to_string()]); + assert!(value(&s, "pageNo").iter().all(|v| v.is_empty()), "page numbers are the layout's: {:?}", value(&s, "pageNo")); + assert!(value(&s, "note").iter().all(|v| v.is_empty()), "form:ready is another event"); + // With highlighting already on, the form has nothing to ask for. + s.set_field_highlight(true); + let id = s.open("ready.pdf", None, bytes, None).expect("opens"); + assert!(s.take_js_output(id).requests.is_empty()); +} diff --git a/crates/engine/src/xfa.rs b/crates/engine/src/xfa.rs index 0f37933afd..6a9fe2a8c6 100644 --- a/crates/engine/src/xfa.rs +++ b/crates/engine/src/xfa.rs @@ -127,6 +127,8 @@ pub(crate) struct XfaRun<'a> { pub datasets: Option, /// A script ran past its time limit and was abandoned: the form's scripts should stay off. pub ran_away: bool, + /// Whether the viewer highlights form fields, for `app.runtimeHighlight`. + pub highlight: bool, } fn to_js(n: &FormNode) -> XfaNode { @@ -307,7 +309,7 @@ impl<'a, 'b> Runner<'a, 'b> { /// The script engine for this event. fn session(&mut self) -> &mut XfaSession { - let doc_info = XfaDoc { file_name: String::new(), page: 0, page_count: self.run.page_count }; + let doc_info = XfaDoc { file_name: String::new(), page: 0, page_count: self.run.page_count, highlight: self.run.highlight }; self.session.get_or_insert_with(|| XfaSession::new(doc_info)) } @@ -632,6 +634,10 @@ impl<'a, 'b> Runner<'a, 'b> { } } XfaEffect::Beep => self.run.out.requests.push(Request::Beep), + XfaEffect::Highlight(on) => { + self.run.highlight = *on; + self.run.out.requests.push(Request::Highlight(*on)); + } XfaEffect::Recalculate => {} XfaEffect::Relayout => relayout = true, } @@ -764,13 +770,23 @@ fn collect_fields(n: &FormNode, out: &mut Vec<(String, String, String, String)>, } } -/// On open: initialize scripts, then calculations. Returns what they changed. +/// On open: initialize scripts, then layout:ready scripts (the form is laid out by now), then +/// calculations. Returns what they changed. +/// +/// layout:ready runs once here, where forms set themselves up (turning field highlighting on, +/// say); a viewer runs it after every layout. Scripts that number pages are left out: the +/// layout gives each page its number itself. pub(crate) fn on_open(doc: &mut pdfcraft_cos::Document, tpl: &Template, run: &mut XfaRun) -> Result { let mut r = Runner::new(doc, tpl, run, OPEN_BUDGET_MS); let events: Vec = r.live().events.iter().filter(|e| e.activity == "initialize" || e.activity == "docReady").cloned().collect(); for ev in &events { r.run(ev, "")?; } + let ready: Vec = + r.live().events.iter().filter(|e| e.activity == "layoutReady" && !pdfcraft_xfa::numbers_pages(&e.script)).cloned().collect(); + for ev in &ready { + r.run(ev, "")?; + } r.calculates()?; r.finish() } diff --git a/crates/js/src/lib.rs b/crates/js/src/lib.rs index d8d9fa8bac..9ace0cbc4f 100644 --- a/crates/js/src/lib.rs +++ b/crates/js/src/lib.rs @@ -141,6 +141,8 @@ pub struct DocInfo { pub page: usize, /// Document information (`this.info.title` etc.): lower-case key, value. pub info: Vec<(String, String)>, + /// Whether the viewer highlights form fields (`app.runtimeHighlight`). + pub highlight: bool, } /// Something a script asked the viewer to do. @@ -159,6 +161,8 @@ pub enum Request { Beep, /// `app.execMenuItem("SaveAs")` (XFA forms' Save buttons). SaveAs, + /// `app.runtimeHighlight = on`: highlight the form's fields, or stop. + Highlight(bool), } /// What running a script did. @@ -559,6 +563,23 @@ fn beep(_: &JsValue, _: &[JsValue], ctx: &mut Context) -> JsResult { Ok(JsValue::undefined()) } +/// `app.runtimeHighlight`: whether the viewer highlights form fields. +fn runtime_highlight(_: &JsValue, _: &[JsValue], ctx: &mut Context) -> JsResult { + Ok(JsValue::from(host(ctx)?.borrow().doc.highlight)) +} + +/// `app.runtimeHighlight = on`: asks the viewer to highlight form fields, or to stop. Later reads +/// in the script see the new setting. +fn set_runtime_highlight(_: &JsValue, args: &[JsValue], ctx: &mut Context) -> JsResult { + let on = arg(args, 0).to_boolean(); + let mut h = host(ctx)?.borrow_mut(); + if h.doc.highlight != on { + h.doc.highlight = on; + h.requests.push(Request::Highlight(on)); + } + Ok(JsValue::undefined()) +} + fn launch_url(_: &JsValue, args: &[JsValue], ctx: &mut Context) -> JsResult { let u = message(&arg(args, 0), "cURL", ctx)?; host(ctx)?.borrow_mut().requests.push(Request::LaunchUrl(u)); @@ -926,7 +947,10 @@ fn install(ctx: &mut Context) -> JsResult<()> { ctx.register_global_property(js_string!("documentFileName"), s(&file), rw)?; ctx.register_global_property(js_string!("info"), info, rw)?; + let highlight_get = function(ctx, NativeFunction::from_fn_ptr(runtime_highlight)); + let highlight_set = function(ctx, NativeFunction::from_fn_ptr(set_runtime_highlight)); let app = ObjectInitializer::new(ctx) + .accessor(js_string!("runtimeHighlight"), Some(highlight_get), Some(highlight_set), rw) .function(NativeFunction::from_fn_ptr(alert), js_string!("alert"), 1) .function(NativeFunction::from_fn_ptr(beep), js_string!("beep"), 0) .function(NativeFunction::from_fn_ptr(response), js_string!("response"), 1) diff --git a/crates/js/src/tests.rs b/crates/js/src/tests.rs index 61d3a71e51..43628137c6 100644 --- a/crates/js/src/tests.rs +++ b/crates/js/src/tests.rs @@ -16,7 +16,7 @@ fn fields() -> Vec { } fn doc() -> DocInfo { - DocInfo { file_name: "order.pdf".into(), num_pages: 3, page: 0, info: vec![("title".into(), "Order".into())] } + DocInfo { file_name: "order.pdf".into(), num_pages: 3, page: 0, info: vec![("title".into(), "Order".into())], highlight: false } } fn go(script: &str, event: &Event) -> Outcome { @@ -382,7 +382,7 @@ mod xfa_model { fn go(script: &str, target: &str, activity: &str) -> XfaOutcome { let ev = XfaEvent { activity: activity.into(), target: target.into(), ..Default::default() }; - let doc = XfaDoc { file_name: "f.pdf".into(), page: 1, page_count: 3 }; + let doc = XfaDoc { file_name: "f.pdf".into(), page: 1, page_count: 3, ..Default::default() }; run_xfa(script, &ev, &doc, &form(), Limits::default()) } @@ -596,7 +596,7 @@ mod formcalc { fn go(script: &str, target: &str, activity: &str) -> XfaOutcome { let ev = XfaEvent { activity: activity.into(), target: target.into(), ..Default::default() }; - let doc = XfaDoc { file_name: "f.pdf".into(), page: 1, page_count: 3 }; + let doc = XfaDoc { file_name: "f.pdf".into(), page: 1, page_count: 3, ..Default::default() }; run_formcalc(script, &ev, &doc, &form(), Limits::default()) } @@ -1132,3 +1132,33 @@ mod session { assert!(o.abandoned || o.error.as_deref().is_some_and(|e| e.contains("too long")), "{o:?}"); } } + +mod runtime_highlight { + use super::*; + use crate::xfa::{XfaDoc, XfaEffect, XfaEvent, XfaNode, run_xfa}; + + #[test] + fn app_runtime_highlight_reads_the_viewers_setting_and_asks_to_change_it() { + let ev = Event::field("Calculate", "total", ""); + let on = DocInfo { highlight: true, ..doc() }; + let o = run("event.value = String(app.runtimeHighlight);", &ev, &on, &fields(), &[], Limits::default()); + assert_eq!((o.value.as_str(), o.requests.len()), ("true", 0)); + let o = go("var was = app.runtimeHighlight; app.runtimeHighlight = true; event.value = was + ',' + app.runtimeHighlight;", &ev); + assert_eq!(o.error, None); + assert_eq!(o.value, "false,true", "the script reads back what it set"); + assert_eq!(o.requests, vec![Request::Highlight(true)]); + } + + #[test] + fn xfa_app_runtime_highlight_reads_the_setting_and_records_a_change() { + let ev = XfaEvent { activity: "layoutReady".into(), target: "form1[0]".into(), ..Default::default() }; + let root = XfaNode { name: "form1".into(), som: "form1[0]".into(), ..Default::default() }; + let script = "var was = app.runtimeHighlight; if (!app.runtimeHighlight) { app.runtimeHighlight = true; } was + ',' + app.runtimeHighlight"; + let o = run_xfa(script, &ev, &XfaDoc::default(), &root, Limits::default()); + assert_eq!(o.error, None, "{o:?}"); + assert_eq!(o.result.as_deref(), Some("false,true")); + assert_eq!(o.effects, vec![XfaEffect::Highlight(true)]); + let o = run_xfa(script, &ev, &XfaDoc { highlight: true, ..Default::default() }, &root, Limits::default()); + assert_eq!((o.result.as_deref(), o.effects.len()), (Some("true,true"), 0), "already on: nothing to ask for"); + } +} diff --git a/crates/js/src/xfa.rs b/crates/js/src/xfa.rs index a2361da7e4..53fd7582f0 100644 --- a/crates/js/src/xfa.rs +++ b/crates/js/src/xfa.rs @@ -84,6 +84,8 @@ pub struct XfaDoc { /// The current page, 0-based. pub page: usize, pub page_count: usize, + /// Whether the viewer highlights form fields (`app.runtimeHighlight`). + pub highlight: bool, } /// Something a script changed or asked for. @@ -121,6 +123,8 @@ pub enum XfaEffect { Recalculate, /// `xfa.form.remerge()`, `xfa.layout.relayout()`. Relayout, + /// `app.runtimeHighlight = on`. + Highlight(bool), } /// What running a script did. @@ -1000,6 +1004,22 @@ fn effect(ctx: &mut Context, e: XfaEffect) -> JsResult<()> { Ok(()) } +/// `app.runtimeHighlight`: whether the viewer highlights form fields. +fn runtime_highlight(_: &JsValue, _: &[JsValue], ctx: &mut Context) -> JsResult { + Ok(JsValue::from(host(ctx)?.borrow().doc.highlight)) +} + +/// `app.runtimeHighlight = on`: asks the viewer to highlight form fields, or to stop. +fn set_runtime_highlight(_: &JsValue, args: &[JsValue], ctx: &mut Context) -> JsResult { + let on = arg(args, 0).to_boolean(); + let mut h = host(ctx)?.borrow_mut(); + if h.doc.highlight != on { + h.doc.highlight = on; + h.push(XfaEffect::Highlight(on)); + } + Ok(JsValue::undefined()) +} + fn message_box(_: &JsValue, args: &[JsValue], ctx: &mut Context) -> JsResult { let m = clip(text(&arg(args, 0), ctx)?); effect(ctx, XfaEffect::MessageBox(m))?; @@ -1180,7 +1200,10 @@ fn install(ctx: &mut Context, event: &XfaEvent, doc: &XfaDoc, current: usize) -> ctx.register_global_property(js_string!("xfa"), xfa, rw)?; ctx.register_global_property(js_string!("event"), ev, rw)?; ctx.register_global_property(js_string!("__xfa_this"), this_node, rw)?; + let highlight_get = function(ctx, NativeFunction::from_fn_ptr(runtime_highlight)); + let highlight_set = function(ctx, NativeFunction::from_fn_ptr(set_runtime_highlight)); let app = ObjectInitializer::new(ctx) + .accessor(js_string!("runtimeHighlight"), Some(highlight_get), Some(highlight_set), rw) .function(NativeFunction::from_fn_ptr(message_box), js_string!("alert"), 4) .function(NativeFunction::from_fn_ptr(beep), js_string!("beep"), 1) .function(NativeFunction::from_fn_ptr(response), js_string!("response"), 1) diff --git a/crates/ui-egui/src/js_ui.rs b/crates/ui-egui/src/js_ui.rs index df461fc6e7..669077f387 100644 --- a/crates/ui-egui/src/js_ui.rs +++ b/crates/ui-egui/src/js_ui.rs @@ -67,6 +67,14 @@ impl PdfCraftApp { &[("u", &u)], ), Request::SaveAs => self.run_command("file.save_as"), + // As the Highlight fields button does: this document and the next ones. + Request::Highlight(on) => { + if let Some(i) = view { + self.views[i].highlight_fields = on; + } + self.view_defaults.highlight_fields = on; + self.session.set_field_highlight(on); + } Request::Focus(_) | Request::Beep | Request::Reset(_) => {} } } diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 9db9247591..55935ea092 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -1166,6 +1166,7 @@ impl PdfCraftApp { if let Some(ctx) = &self.ctx { worker.set_context(ctx); } + self.session.set_field_highlight(self.view_defaults.highlight_fields); let receiver = worker.submit(token, self.session.open_context(), request.name.clone(), request.path.clone(), request.password, request.input)?; self.next_open_token = token; @@ -1283,6 +1284,8 @@ impl PdfCraftApp { activate: bool, ) -> Result<(), String> { use pdfcraft_render::OpenError; + // A new document opens with the remembered Highlight fields, which its scripts read. + self.session.set_field_highlight(self.view_defaults.highlight_fields); let prepared = match self.session.prepare_open(name, path.clone(), bytes.clone(), password) { Ok(prepared) => prepared, Err(e @ (OpenError::NeedsPassword | OpenError::WrongPassword)) => { @@ -3128,6 +3131,9 @@ impl eframe::App for PdfCraftApp { for view in &mut self.views { view.begin_render_frame(); } + // Forms' scripts read Highlight fields as `app.runtimeHighlight`. + let highlight = self.active.and_then(|i| self.views.get(i)).map_or(self.view_defaults.highlight_fields, |v| v.highlight_fields); + self.session.set_field_highlight(highlight); // The window shows the active document's name (or title, if it asks for that). let title = self .active diff --git a/crates/ui-egui/tests/ui_group/xfa.rs b/crates/ui-egui/tests/ui_group/xfa.rs index 6565a9816f..45a609b5ec 100644 --- a/crates/ui-egui/tests/ui_group/xfa.rs +++ b/crates/ui-egui/tests/ui_group/xfa.rs @@ -231,3 +231,15 @@ fn a_secured_dynamic_xfa_form_shows_its_layout_and_its_security() { h.get_by_label("Highlight fields"); h.get_by_label("Security settings"); } + +/// A form that turns field highlighting on (`app.runtimeHighlight = true` in a `layout:ready` +/// script) gets it, and so does the next document, as with the Highlight fields button. +#[test] +fn a_form_that_turns_field_highlighting_on_gets_it() { + let mut h = open(pdfcraft_xfa::fixtures::shell(&pdfcraft_xfa::fixtures::layout_ready_template())); + h.run_steps(4); + assert!(h.state().views[0].highlight_fields, "the form's fields are highlighted"); + h.state_mut().open_bytes("next.pdf", None, pdfcraft_xfa::fixtures::shell(&pdfcraft_xfa::fixtures::template(1))).unwrap(); + h.run_steps(4); + assert!(h.state().views[1].highlight_fields, "the next document opens highlighted"); +} diff --git a/crates/xfa/src/fixtures.rs b/crates/xfa/src/fixtures.rs index 54bb9bc216..191f5c6afe 100644 --- a/crates/xfa/src/fixtures.rs +++ b/crates/xfa/src/fixtures.rs @@ -118,6 +118,28 @@ fn assemble(objs: Vec>) -> Vec { /// (5) = `total` (calculated); a check box `more` that shows the hidden `details` subform; /// a table whose `row` (item, amount) repeats, with `addRow`, `removeRow` and `hello` buttons; /// `grand`, the calculated sum of the amounts. +/// A form with `layout:ready` scripts: the root subform turns field highlighting on (as forms +/// do with `app.runtimeHighlight`), `stamp` takes a value, and `pageNo` numbers its page (left to +/// the layout). `note` has a `form:ready` script, a different event. +pub fn layout_ready_template() -> String { + r##" + + + +"## + .to_string() +} + pub fn scripted_template() -> String { r##" diff --git a/crates/xfa/src/lib.rs b/crates/xfa/src/lib.rs index bf4414970e..8a2a79e2fc 100644 --- a/crates/xfa/src/lib.rs +++ b/crates/xfa/src/lib.rs @@ -97,7 +97,7 @@ pub use data::{ }; pub use layout::{Action, BorderShape, Form, Item, MAX_PAGES, Page, Widget, WidgetKind, layout}; pub use packets::{Encoding, Packets, decode as decode_packet, encode as encode_packet, read_packets}; -pub use parse::{measure, parse}; +pub use parse::{measure, numbers_pages, parse}; pub use pdf::{CLICK_KEY, LAYOUT_KEY, SOM_KEY, Stripped, XfaShape, existing_layout, strip, xfa_shape}; pub use script::{ FormNode, LiveForm, MAX_FORM_NODES, NodeKind, OVERRIDES_KEY, Overrides, ScriptEvent, apply_overrides, data_of, fields_by_som, form_tree, diff --git a/crates/xfa/src/parse.rs b/crates/xfa/src/parse.rs index 4582d59b22..ea72eb0919 100644 --- a/crates/xfa/src/parse.rs +++ b/crates/xfa/src/parse.rs @@ -489,6 +489,9 @@ fn parse_events(n: XmlNode) -> Vec