Forms: app.runtimeHighlight, and XFA layout:ready scripts run on opening

This commit is contained in:
Eric Kaden
2026-10-19 22:59:10 -04:00
parent ec0a965074
commit 98d9310309
16 changed files with 236 additions and 22 deletions
+1
View File
@@ -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 }),
}
}
+17 -3
View File
@@ -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 {
+18 -6
View File
@@ -2252,6 +2252,8 @@ struct EditCtx {
xfa_removed: Vec<String>,
/// Pages in the document, for `xfa.layout.pageCount()`.
pages: usize,
/// Whether the viewer highlights form fields, for `app.runtimeHighlight`.
highlight: bool,
date: Option<String>,
/// 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<String>,
/// Preferences ▸ Date format ▸ Language, when not following the interface language.
date_language: Option<String>,
/// 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<i64>,
js_off: bool,
trust: Arc<TrustStore>,
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))
+17
View File
@@ -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());
}
+18 -2
View File
@@ -127,6 +127,8 @@ pub(crate) struct XfaRun<'a> {
pub datasets: Option<pdfcraft_cos::ObjRef>,
/// 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<Changes, String> {
let mut r = Runner::new(doc, tpl, run, OPEN_BUDGET_MS);
let events: Vec<ScriptEvent> = r.live().events.iter().filter(|e| e.activity == "initialize" || e.activity == "docReady").cloned().collect();
for ev in &events {
r.run(ev, "")?;
}
let ready: Vec<ScriptEvent> =
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()
}
+24
View File
@@ -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<JsValue> {
Ok(JsValue::undefined())
}
/// `app.runtimeHighlight`: whether the viewer highlights form fields.
fn runtime_highlight(_: &JsValue, _: &[JsValue], ctx: &mut Context) -> JsResult<JsValue> {
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<JsValue> {
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<JsValue> {
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)
+33 -3
View File
@@ -16,7 +16,7 @@ fn fields() -> Vec<FieldState> {
}
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");
}
}
+23
View File
@@ -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<JsValue> {
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<JsValue> {
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<JsValue> {
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)
+8
View File
@@ -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(_) => {}
}
}
+6
View File
@@ -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
+12
View File
@@ -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");
}
+22
View File
@@ -118,6 +118,28 @@ fn assemble(objs: Vec<Vec<u8>>) -> Vec<u8> {
/// (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##"<?xml version="1.0" encoding="UTF-8"?>
<xdp:xdp xmlns:xdp="http://ns.adobe.com/xdp/">
<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">
<subform name="form" layout="tb">
<event activity="ready" ref="$layout"><script contentType="application/x-javascript">if (!app.runtimeHighlight) { app.runtimeHighlight = true; }</script></event>
<pageSet><pageArea name="front"><contentArea x="0.5in" y="0.5in" w="7.5in" h="10in"/><medium short="8.5in" long="11in"/></pageArea></pageSet>
<subform name="page" layout="tb">
<field name="stamp" w="2in" h="0.3in"><ui><textEdit/></ui><event activity="ready" ref="$layout"><script contentType="application/x-javascript">this.rawValue = "laid out";</script></event></field>
<field name="pageNo" w="1in" h="0.3in"><ui><textEdit/></ui><event activity="ready" ref="$layout"><script contentType="application/x-javascript">this.rawValue = xfa.layout.page(this);</script></event></field>
<field name="note" w="2in" h="0.3in"><ui><textEdit/></ui><event activity="ready" ref="$form"><script contentType="application/x-javascript">this.rawValue = "form ready";</script></event></field>
</subform>
</subform>
</template>
</xdp:xdp>
"##
.to_string()
}
pub fn scripted_template() -> String {
r##"<?xml version="1.0" encoding="UTF-8"?>
<xdp:xdp xmlns:xdp="http://ns.adobe.com/xdp/">
+1 -1
View File
@@ -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,
+14 -5
View File
@@ -489,6 +489,9 @@ fn parse_events(n: XmlNode) -> Vec<Script> {
for ev in children(n, "event").take(64) {
if let Some(s) = child(ev, "script") {
let activity = ev.attribute("activity").unwrap_or("click");
// `ready` is two events, told apart by `ref`: layout:ready (`$layout`, once the form
// is laid out) and form:ready (`$form`).
let activity = if activity == "ready" && ev.attribute("ref").is_some_and(|r| r.trim() == "$layout") { "layoutReady" } else { activity };
scripts.push(parse_script(s, activity));
}
}
@@ -605,15 +608,21 @@ fn parse_node(c: XmlNode, warnings: &mut Vec<String>, depth: usize) -> Option<No
})
}
/// Whether a `layout:ready` script numbers pages (`xfa.layout.page(this)`, `pageCount()`), which
/// the layout does itself.
pub fn numbers_pages(script: &str) -> bool {
script.contains("pageCount") || script.contains("layout.page(") || script.contains("layout.absPage(")
}
/// Which page-number role a field with a layout:ready script plays, if any.
fn page_role(f: &Field) -> Option<PageRole> {
let script = f.scripts.iter().find(|s| s.activity == "ready" || s.activity == "layoutReady")?;
if script.text.contains("pageCount") {
Some(PageRole::Count)
} else if script.text.contains("layout.page(") || script.text.contains("layout.absPage(") {
Some(PageRole::Number)
} else {
if !numbers_pages(&script.text) {
None
} else if script.text.contains("pageCount") {
Some(PageRole::Count)
} else {
Some(PageRole::Number)
}
}
+20
View File
@@ -2067,3 +2067,23 @@ fn som_indices_count_earlier_same_named_siblings() {
let sibs = SomIndices::of(&children);
assert_eq!((0..=children.len()).map(|i| sibs.at(i)).collect::<Vec<_>>(), [0, 0, 1, 0, 2, 1, 0]);
}
#[test]
fn a_layout_ready_event_is_told_apart_from_form_ready() {
let (tpl, _) = parse(&crate::fixtures::layout_ready_template()).unwrap();
assert_eq!(tpl.root.scripts.iter().map(|s| s.activity.as_str()).collect::<Vec<_>>(), ["layoutReady"]);
let Node::Subform(page) = &tpl.root.children[0] else { panic!() };
let activity = |name: &str| match page.children.iter().find(|n| n.common().name.as_deref() == Some(name)) {
Some(Node::Field(f)) => f.scripts.iter().map(|s| s.activity.clone()).collect::<Vec<_>>(),
_ => panic!("no field {name}"),
};
assert_eq!(activity("stamp"), ["layoutReady"]);
assert_eq!(activity("note"), ["ready"], "form:ready keeps its name");
// The live form's events carry the same names.
let live = form_tree(&tpl, None, &Overrides::default());
assert!(live.events.iter().any(|e| e.som == "form[0]" && e.activity == "layoutReady"), "{:?}", live.events);
// Page numbering is what the layout does itself.
assert!(crate::numbers_pages("this.rawValue = xfa.layout.page(this);"));
assert!(crate::numbers_pages("this.rawValue = xfa.layout.pageCount();"));
assert!(!crate::numbers_pages("this.rawValue = \"laid out\";"));
}
+2 -2
View File
@@ -4392,8 +4392,8 @@ milestone = "M3"
status = "shipped"
commands = []
tools = []
evidence = ["crates/ui-egui/tests/view.rs::highlight_fields_tints_the_field_area", "crates/ui-egui/tests/view.rs::required_radio_buttons_get_a_round_red_border", "crates/ui-egui/tests/view.rs::field_highlighting_is_remembered", "crates/ui-egui/tests/editing.rs::a_secured_form_shows_its_form_notice_and_its_security"]
notes = "offered on secured forms too; remembered for the next document and session; required fields get a red border (round for radio buttons); the tint is drawn over the field, where Acrobat paints the highlight colour behind it"
evidence = ["crates/ui-egui/tests/view.rs::highlight_fields_tints_the_field_area", "crates/ui-egui/tests/view.rs::required_radio_buttons_get_a_round_red_border", "crates/ui-egui/tests/view.rs::field_highlighting_is_remembered", "crates/ui-egui/tests/ui_group/xfa.rs::a_form_that_turns_field_highlighting_on_gets_it", "crates/engine/src/tests.rs::a_forms_layout_ready_scripts_run_once_it_is_laid_out", "crates/ui-egui/tests/editing.rs::a_secured_form_shows_its_form_notice_and_its_security"]
notes = "a form's scripts read and set it as app.runtimeHighlight (XFA forms usually in layout:ready, which runs once on opening), and a setting they make carries over as the button's does; offered on secured forms too; remembered for the next document and session; required fields get a red border (round for radio buttons); the tint is drawn over the field, where Acrobat paints the highlight colour behind it"
[[feature]]
id = "form.field-appearance-display"