diff --git a/crates/engine/src/js.rs b/crates/engine/src/js.rs index a6fbea8abe..348052aff4 100644 --- a/crates/engine/src/js.rs +++ b/crates/engine/src/js.rs @@ -2025,6 +2025,7 @@ mod widget_budget_review_tests { rotation: 0, caption: None, mouse_up_script: None, + mouse_down: false, list: None, round: false, }; diff --git a/crates/forms/src/author/lifecycle.rs b/crates/forms/src/author/lifecycle.rs index 50d552ccdb..c079b40f13 100644 --- a/crates/forms/src/author/lifecycle.rs +++ b/crates/forms/src/author/lifecycle.rs @@ -173,6 +173,7 @@ pub fn add_field(doc: &mut Document, page: usize, rect: [f64; 4], kind: &NewFiel rotation: 0, caption: None, mouse_up_script: None, + mouse_down: false, list: None, round: true, }; diff --git a/crates/forms/src/lib.rs b/crates/forms/src/lib.rs index 96d6ea2a6e..0c71c4077f 100644 --- a/crates/forms/src/lib.rs +++ b/crates/forms/src/lib.rs @@ -140,6 +140,9 @@ pub struct Widget { pub caption: Option, /// Text widget JavaScript to run on a mouse-up click. pub mouse_up_script: Option, + /// The widget has a mouse-down action (`/AA /D`): a press runs it (the LaTeX `animate` + /// package's controls act on mouse down), so a push button with only that still takes a press. + pub mouse_down: bool, /// List boxes: where the options are drawn, so a click can pick the one under the pointer. pub list: Option, /// Drawn round: a radio button, or a check box laid out for a round XFA check button diff --git a/crates/forms/src/read.rs b/crates/forms/src/read.rs index 537237254a..49d7e91562 100644 --- a/crates/forms/src/read.rs +++ b/crates/forms/src/read.rs @@ -733,6 +733,7 @@ fn walk( } else { None }, + mouse_down: wd.get(b"AA").map(|a| doc.resolve(a)).as_deref().and_then(Object::as_dict).is_some_and(|aa| aa.contains(b"D")), list: (kind == FieldKind::List).then(|| appearance::list_rows_of(doc, inh.da.as_deref().unwrap_or("/Helv 0 Tf 0 g"), Some(d), wd)), round: kind == FieldKind::Radio || (kind == FieldKind::CheckBox && appearance::is_round(doc, wd)), }) diff --git a/crates/ui-egui/src/canvas/sign_tab_tests.rs b/crates/ui-egui/src/canvas/sign_tab_tests.rs index c2d5c20748..79789eddcf 100644 --- a/crates/ui-egui/src/canvas/sign_tab_tests.rs +++ b/crates/ui-egui/src/canvas/sign_tab_tests.rs @@ -58,6 +58,7 @@ fn field(name: &str, page: usize, rect: [f64; 4]) -> FormField { tooltip: None, widgets: vec![FormWidget { mouse_up_script: None, + mouse_down: false, obj: pdfcraft_cos::ObjRef::new(4, 0), page: Some(page), rect, diff --git a/crates/ui-egui/src/tools/forms_ui.rs b/crates/ui-egui/src/tools/forms_ui.rs index 94aafda3f7..6a5abcecf2 100644 --- a/crates/ui-egui/src/tools/forms_ui.rs +++ b/crates/ui-egui/src/tools/forms_ui.rs @@ -432,11 +432,13 @@ pub(crate) fn tab_under<'a>(tabs: &[(&'a str, SignTab)], p: egui::Pos2) -> Optio tabs.iter().rev().find(|(_, tab)| tab.rect.contains(p)).map(|(name, _)| *name) } -/// Whether `w`, a widget of `f`, takes a press: fillable, a push button with an action, or an +/// Whether `w`, a widget of `f`, takes a press: fillable, a push button with an action (a click +/// or mouse-down one), or an /// empty signature field (signed by clicking it, Use a certificate). A radio button of a group can /// be read-only on its own. fn usable(f: &FormField, w: &FormWidget, allowed: bool) -> bool { - let pressable = f.kind == FormFieldKind::PushButton && !f.read_only() && f.button.is_some(); + // A button acting only on mouse down (`/AA /D`) takes the press too, which runs it (#4751). + let pressable = f.kind == FormFieldKind::PushButton && !f.read_only() && (f.button.is_some() || w.mouse_down); allowed && !w.read_only && (fillable(f) || pressable || f.kind == FormFieldKind::Signature) } @@ -551,6 +553,8 @@ pub(crate) fn page_input( _ if f.read_only() || w.read_only => view.forms.notice = Some(FormNotice::ReadOnly(f.name.clone())), FormFieldKind::PushButton => match &f.button { Some(a) => view.forms.button = Some((f.name.clone(), pdfcraft_engine::js::field_object_id(f.obj), a.clone())), + // Its press already ran its mouse-down action. + None if w.mouse_down => {} None => view.forms.notice = Some(FormNotice::NoAction(f.name.clone())), }, FormFieldKind::Text if let Some(script) = &w.mouse_up_script => { diff --git a/crates/ui-egui/tests/it/field_triggers.rs b/crates/ui-egui/tests/it/field_triggers.rs index f648b7a445..d57397e72e 100644 --- a/crates/ui-egui/tests/it/field_triggers.rs +++ b/crates/ui-egui/tests/it/field_triggers.rs @@ -1,6 +1,7 @@ //! A form field widget's pointer scripts in the app (#269, ported from #344): the pointer //! moving onto and off a widget runs its Mouse Enter and Mouse Exit JavaScript, and a click on a -//! check box runs its Mouse Up JavaScript after the click stored the new value (#247). +//! check box runs its Mouse Up JavaScript after the click stored the new value (#247). A push +//! button with only a mouse-down action runs it when pressed (#4751). use egui_kittest::Harness; use pdfcraft_ui_egui::PdfCraftApp; @@ -9,15 +10,16 @@ use pdfcraft_ui_egui::PdfCraftApp; /// `strike` (a check box) shows or hides `note` from its Mouse Up script by its own new value. fn form() -> Vec { let objs = [ - "<< /Type /Catalog /Pages 2 0 R /AcroForm << /Fields [4 0 R 5 0 R 6 0 R 8 0 R] /DA (/Helv 0 Tf 0 g) /DR << /Font << /Helv 7 0 R >> >> >> >>", + "<< /Type /Catalog /Pages 2 0 R /AcroForm << /Fields [4 0 R 5 0 R 6 0 R 8 0 R 10 0 R] /DA (/Helv 0 Tf 0 g) /DR << /Font << /Helv 7 0 R >> >> >> >>", "<< /Type /Pages /Kids [3 0 R] /Count 1 /MediaBox [0 0 300 400] >>", - "<< /Type /Page /Parent 2 0 R /Annots [4 0 R 5 0 R 6 0 R 8 0 R] >>", + "<< /Type /Page /Parent 2 0 R /Annots [4 0 R 5 0 R 6 0 R 8 0 R 10 0 R] >>", "<< /Type /Annot /Subtype /Widget /FT /Tx /T (log) /V () /Rect [20 350 280 370] /P 3 0 R /F 4 >>", "<< /Type /Annot /Subtype /Widget /FT /Tx /T (hot) /V () /Rect [20 250 280 290] /P 3 0 R /F 4 /AA << /E << /S /JavaScript /JS (getField\\('log'\\).value += 'in;';) >> /X << /S /JavaScript /JS (getField\\('log'\\).value += 'out;';) >> >> >>", "<< /Type /Annot /Subtype /Widget /FT /Btn /T (strike) /V /Off /AS /Off /Rect [20 300 50 330] /P 3 0 R /F 4 /MK << /CA (4) >> /AP << /N << /Yes 9 0 R /Off 9 0 R >> >> /A << /S /JavaScript /JS (getField\\('note'\\).display = event.target.value == 'Off' ? display.hidden : display.visible; getField\\('log'\\).value += 'up:' + event.target.value + ';';) >> >>", "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>", "<< /Type /Annot /Subtype /Widget /FT /Tx /T (note) /V (struck) /Rect [60 300 280 330] /P 3 0 R /F 6 >>", "<< /Type /XObject /Subtype /Form /BBox [0 0 30 30] /Length 0 >>\nstream\n\nendstream", + "<< /Type /Annot /Subtype /Widget /FT /Btn /Ff 65536 /T (step) /Rect [20 200 120 230] /P 3 0 R /F 4 /MK << /CA (Step) >> /AA << /D << /S /JavaScript /JS (getField\\('log'\\).value += event.name + ';';) >> >> >>", ]; let mut out = b"%PDF-1.7\n".to_vec(); let mut offsets = Vec::new(); @@ -102,3 +104,19 @@ fn a_check_box_mouse_up_script_sees_the_value_its_click_stored() { crate::support::wait_until(&mut h, "unchecked and its Mouse Up ran", |app| field(app, "log").value.join(",") == "up:Yes;up:Off;"); assert!(field(h.state(), "note").widgets[0].hidden, "hidden again"); } + +/// A push button with only a mouse-down action (`/AA /D`, as the LaTeX `animate` package's +/// controls have) takes the press and runs it as Mouse Down, once per click (#4751). +#[test] +fn a_push_button_with_only_a_mouse_down_action_runs_it_on_press() { + let mut h = harness(); + let step = centre(&h, "step"); + h.hover_at(step); + h.run_steps(1); + h.drag_at(step); + h.run_steps(1); + h.drop_at(step); + crate::support::wait_until(&mut h, "Mouse Down ran", |app| field(app, "log").value.join(",") == "Mouse Down;"); + h.run_steps(4); + assert_eq!(log(&h), "Mouse Down;", "the click runs nothing more"); +}