diff --git a/crates/engine/src/js.rs b/crates/engine/src/js.rs index a59743dbb..aa43d3dba 100644 --- a/crates/engine/src/js.rs +++ b/crates/engine/src/js.rs @@ -200,6 +200,11 @@ impl crate::Document { self.editor.as_ref().and_then(|e| pdfcraft_forms::field_actions(&e.cos, name).ok()).unwrap_or_default() } + /// Field Properties ▸ Actions: how many actions are chained after each trigger's first one. + pub fn field_action_chains(&self, name: &str) -> Vec<(pdfcraft_forms::Trigger, usize)> { + self.editor.as_ref().and_then(|e| pdfcraft_forms::field_action_chains(&e.cos, name).ok()).unwrap_or_default() + } + /// Document JavaScripts (name, source), in name order. pub fn document_scripts(&self) -> Vec<(String, String)> { self.editor.as_ref().map(|e| document_scripts(&e.cos)).unwrap_or_default() diff --git a/crates/forms/src/actions.rs b/crates/forms/src/actions.rs index cdc89e563..f58bd1078 100644 --- a/crates/forms/src/actions.rs +++ b/crates/forms/src/actions.rs @@ -159,6 +159,60 @@ pub fn field_actions(doc: &Document, name: &str) -> Result usize { + let mut seen = std::collections::HashSet::new(); + if let Object::Ref(r) = action { + seen.insert(*r); + } + let mut stack: Vec = doc.resolve(action).as_dict().and_then(|d| d.get(b"Next").cloned()).into_iter().collect(); + let mut count = 0; + while let Some(o) = stack.pop() { + if count >= MAX_CHAIN { + break; + } + if let Object::Ref(r) = &o + && !seen.insert(*r) + { + continue; + } + match &*doc.resolve(&o) { + Object::Array(items) => stack.extend(items.iter().cloned()), + Object::Dict(d) => { + count += 1; + stack.extend(d.get(b"Next").cloned()); + } + _ => {} + } + } + count +} + +/// For each trigger of field `name` (its first widget) whose action has others chained after it, +/// how many there are. +pub fn field_action_chains(doc: &Document, name: &str) -> Result, FormError> { + let all = fields(doc); + let f = all.iter().find(|f| f.name == name).ok_or_else(|| FormError::NoSuchField(name.into()))?; + let Some(w) = f.widgets.first() else { return Ok(Vec::new()) }; + let wd = doc.get(w.obj).as_dict().cloned().unwrap_or_default(); + let aa = wd.get(b"AA").map(|a| doc.resolve(a)).and_then(|a| a.as_dict().cloned()).unwrap_or_default(); + Ok(Trigger::ALL + .into_iter() + .filter_map(|t| { + let o = match t.key() { + None => wd.get(b"A").cloned().or_else(|| aa.get(b"U").cloned()), + Some(k) => aa.get(k).cloned(), + }?; + let n = chained(doc, &o); + (n > 0).then_some((t, n)) + }) + .collect()) +} + fn write(doc: &mut Document, a: &FieldAction, pages: &[pdfcraft_cos::ObjRef]) -> Result, FormError> { let mut d = Dict::new(); d.set(b"Type".to_vec(), Object::name("Action")); @@ -207,11 +261,13 @@ fn write(doc: &mut Document, a: &FieldAction, pages: &[pdfcraft_cos::ObjRef]) -> } /// Replace field `name`'s actions (on every widget) with `actions`; triggers not listed lose -/// theirs. `Other` actions are left as they are. +/// theirs. `Other` actions, and actions that read the same as before, are left as they are (with +/// their chain and the keys this module doesn't read); a replaced action keeps its chain. pub fn set_field_actions(doc: &mut Document, name: &str, actions: &[(Trigger, FieldAction)]) -> Result<(), FormError> { let all = fields(doc); let f = all.iter().find(|f| f.name == name).ok_or_else(|| FormError::NoSuchField(name.into()))?.clone(); let pages = crate::page_refs(doc); + let mut scripts = crate::script_budget(); for w in &f.widgets { let wd = doc.get(w.obj).as_dict().cloned().unwrap_or_default(); let mut aa = wd.get(b"AA").map(|a| doc.resolve(a)).and_then(|a| a.as_dict().cloned()).unwrap_or_default(); @@ -221,10 +277,23 @@ pub fn set_field_actions(doc: &mut Document, name: &str, actions: &[(Trigger, Fi if matches!(new, Some(FieldAction::Other(_))) { continue; } + let old = match t.key() { + None => wd.get(b"A").cloned().or_else(|| aa.get(b"U").cloned()), + Some(k) => aa.get(k).cloned(), + }; + let old_action = old.as_ref().and_then(|o| read(doc, o, &pages, &mut scripts)); + if new.is_some() && new == old_action.as_ref() { + continue; + } + // The list shows a trigger's first action: replacing it keeps the ones chained after it. + let next = new.and(old).and_then(|o| doc.resolve(&o).as_dict().and_then(|d| d.get(b"Next").cloned())); let obj = match new { Some(a) => write(doc, a, &pages)?, None => None, }; + if let (Some(Object::Ref(r)), Some(next)) = (&obj, next) { + doc.update_dict(*r, |d| d.set(b"Next".to_vec(), next))?; + } match t.key() { None => { a_entry = obj; diff --git a/crates/forms/src/lib.rs b/crates/forms/src/lib.rs index 672d27975..0577b08ca 100644 --- a/crates/forms/src/lib.rs +++ b/crates/forms/src/lib.rs @@ -24,7 +24,7 @@ mod calc_order; pub mod detect; mod scripting; pub mod table; -pub use actions::{FieldAction, Trigger, field_actions, set_field_actions}; +pub use actions::{FieldAction, Trigger, field_action_chains, field_actions, set_field_actions}; pub use author::{ BorderStyle, CheckStyle, FieldFont, FieldProps, Look, LookPatch, MIN_FIELD_SIDE, NewField, add_field, check_style, delete_field, delete_field_object, duplicate_field, look, redraw_field, set_button_icon, set_props, diff --git a/crates/forms/src/tests.rs b/crates/forms/src/tests.rs index f6a50399c..9a186c7da 100644 --- a/crates/forms/src/tests.rs +++ b/crates/forms/src/tests.rs @@ -1976,6 +1976,80 @@ fn field_actions_round_trip_on_every_trigger() { assert_eq!(fields(&doc).iter().find(|f| f.name == "go").unwrap().button, Some(af::ButtonAction::Named("Print".into()))); } +#[test] +fn editing_one_script_keeps_the_other_actions_and_the_chain() { + let mut doc = fixture(); + let go = field(&fields(&doc), "go").obj; + let action = |doc: &mut Document, entries: Vec<(&[u8], Object)>| { + let mut a = Dict::new(); + for (k, v) in entries { + a.set(k.to_vec(), v); + } + doc.add(Object::Dict(a)) + }; + let next = action(&mut doc, vec![(b"S", Object::name("URI")), (b"URI", Object::String(PdfString::literal(b"https://example.org".to_vec())))]); + let up = action( + &mut doc, + vec![(b"S", Object::name("JavaScript")), (b"JS", Object::String(PdfString::text("app.alert(1);"))), (b"Next", Object::Ref(next))], + ); + let blur = action( + &mut doc, + vec![(b"S", Object::name("JavaScript")), (b"JS", Object::String(PdfString::text("this.dirty = true;"))), (b"Foo", Object::Int(7))], + ); + let mut aa = Dict::new(); + aa.set(b"Bl".to_vec(), Object::Ref(blur)); + doc.update_dict(go, |d| { + d.set(b"A".to_vec(), Object::Ref(up)); + d.set(b"AA".to_vec(), Object::Dict(aa)); + }) + .unwrap(); + let mut acts = field_actions(&doc, "go").unwrap(); + assert_eq!(acts[0], (Trigger::MouseUp, FieldAction::JavaScript("app.alert(1);".into()))); + acts[0].1 = FieldAction::JavaScript("app.alert(2);".into()); + set_field_actions(&mut doc, "go", &acts).unwrap(); + let w = doc.get(go).as_dict().cloned().unwrap(); + // The action that did not change is the same object, with its keys this crate doesn't read. + let aa = w.get(b"AA").unwrap().as_dict().unwrap().clone(); + assert_eq!(aa.get(b"Bl"), Some(&Object::Ref(blur))); + // The edited script keeps the actions chained after it. + let a = doc.resolve(w.get(b"A").unwrap()).as_dict().cloned().unwrap(); + assert_eq!(a.get(b"Next"), Some(&Object::Ref(next))); + assert_eq!(field_actions(&doc, "go").unwrap()[0], (Trigger::MouseUp, FieldAction::JavaScript("app.alert(2);".into()))); +} + +#[test] +fn replacing_a_chained_action_with_another_type_keeps_the_chain() { + let mut doc = fixture(); + let go = field(&fields(&doc), "go").obj; + let s = |t: &str| Object::String(PdfString::text(t)); + let mut named = Dict::new(); + named.set(b"S".to_vec(), Object::name("Named")); + named.set(b"N".to_vec(), Object::name("NextPage")); + let named = doc.add(Object::Dict(named)); + // A chain as an array: a link, then a menu item that points back at the link (a loop). + let mut uri = Dict::new(); + uri.set(b"S".to_vec(), Object::name("URI")); + uri.set(b"URI".to_vec(), s("https://example.org")); + let uri = doc.add(Object::Dict(uri)); + doc.update_dict(named, |d| d.set(b"Next".to_vec(), Object::Ref(uri))).unwrap(); + let chain = Object::Array(vec![Object::Ref(uri), Object::Ref(named)]); + let mut up = Dict::new(); + up.set(b"S".to_vec(), Object::name("JavaScript")); + up.set(b"JS".to_vec(), s("app.alert(1);")); + up.set(b"Next".to_vec(), chain.clone()); + doc.update_dict(go, |d| d.set(b"A".to_vec(), Object::Dict(up))).unwrap(); + assert_eq!(field_action_chains(&doc, "go").unwrap(), [(Trigger::MouseUp, 2)]); + set_field_actions(&mut doc, "go", &[(Trigger::MouseUp, FieldAction::Uri("https://example.com".into()))]).unwrap(); + let w = doc.get(go).as_dict().cloned().unwrap(); + let a = doc.resolve(w.get(b"A").unwrap()).as_dict().cloned().unwrap(); + assert_eq!(a.get(b"Next"), Some(&chain)); + assert_eq!(field_actions(&doc, "go").unwrap(), [(Trigger::MouseUp, FieldAction::Uri("https://example.com".into()))]); + assert_eq!(field_action_chains(&doc, "go").unwrap(), [(Trigger::MouseUp, 2)]); + // Deleting the trigger's action deletes its chain with it. + set_field_actions(&mut doc, "go", &[]).unwrap(); + assert_eq!(field_action_chains(&doc, "go").unwrap(), []); +} + #[test] fn push_buttons_read_hide_actions() { let mut doc = fixture(); diff --git a/crates/ui-egui/src/prepare.rs b/crates/ui-egui/src/prepare.rs index bb94d52e5..f3913401a 100644 --- a/crates/ui-egui/src/prepare.rs +++ b/crates/ui-egui/src/prepare.rs @@ -741,6 +741,7 @@ impl crate::PdfCraftApp { self.session.get(id).map(|doc| doc.form.iter().filter(|x| x.name != name).map(|x| x.name.clone()).collect()).unwrap_or_default(); d.others = others.clone(); d.actions = self.session.get(id).map(|doc| doc.field_actions(name)).unwrap_or_default(); + d.chained = self.session.get(id).map(|doc| doc.field_action_chains(name)).unwrap_or_default(); if let Some(o) = d.original.as_mut() { o.others = others; o.actions = d.actions.clone(); @@ -853,6 +854,11 @@ pub struct FieldDraft { /// Actions tab: each trigger's action, and the one being added. pub actions: Vec<(pdfcraft_engine::FieldTrigger, pdfcraft_engine::FieldAction)>, pub new_action: ActionDraft, + /// Actions tab: the trigger whose script `new_action` is editing (Edit), if any. + pub editing: Option, + /// Actions tab: how many actions the document chains after each trigger's first one. They are + /// kept when that first action is replaced. + pub chained: Vec<(pdfcraft_engine::FieldTrigger, usize)>, original: Box>, } @@ -915,6 +921,8 @@ impl FieldDraft { others: Vec::new(), actions: Vec::new(), new_action: ActionDraft::default(), + editing: None, + chained: Vec::new(), original: Box::new(None), }; d.original = Box::new(Some(d.clone())); @@ -1535,21 +1543,27 @@ fn validate_tab(ui: &mut egui::Ui, d: &mut FieldDraft) { fn actions_tab(ui: &mut egui::Ui, d: &mut FieldDraft, t: &crate::theme::Tokens) { use pdfcraft_engine::FieldTrigger as T; - ui.label(egui::RichText::new(tl!("Add an Action")).font(theme::semibold(13.0))); + let editing = d.editing.is_some(); + ui.label(egui::RichText::new(if editing { tl!("Edit Action") } else { tl!("Add an Action") }).font(theme::semibold(13.0))); let a = &mut d.new_action; egui::Grid::new("field-actions-add").num_columns(2).spacing([12.0, 8.0]).show(ui, |ui| { ui.label(tl!("Select Trigger:")); - egui::ComboBox::from_id_salt("action-trigger").selected_text(tl!(a.trigger.label())).width(220.0).show_ui(ui, |ui| { - for tr in T::ALL { - ui.selectable_value(&mut a.trigger, tr, tl!(tr.label())); - } + // Editing changes the script of one trigger's action: its trigger and kind stay. + ui.add_enabled_ui(!editing, |ui| { + egui::ComboBox::from_id_salt("action-trigger").selected_text(tl!(a.trigger.label())).width(220.0).show_ui(ui, |ui| { + for tr in T::ALL { + ui.selectable_value(&mut a.trigger, tr, tl!(tr.label())); + } + }); }); ui.end_row(); ui.label(tl!("Select Action:")); - egui::ComboBox::from_id_salt("action-kind").selected_text(tl!(ACTION_KINDS[a.kind])).width(220.0).show_ui(ui, |ui| { - for (i, k) in ACTION_KINDS.iter().enumerate() { - ui.selectable_value(&mut a.kind, i, tl!(k)); - } + ui.add_enabled_ui(!editing, |ui| { + egui::ComboBox::from_id_salt("action-kind").selected_text(tl!(ACTION_KINDS[a.kind])).width(220.0).show_ui(ui, |ui| { + for (i, k) in ACTION_KINDS.iter().enumerate() { + ui.selectable_value(&mut a.kind, i, tl!(k)); + } + }); }); ui.end_row(); let hint = match a.kind { @@ -1570,7 +1584,12 @@ fn actions_tab(ui: &mut egui::Ui, d: &mut FieldDraft, t: &crate::theme::Tokens) ui.end_row(); }); let mut error = None; - if ui.button(tl!("Add")).clicked() { + let (mut commit, mut discard) = (false, false); + ui.horizontal(|ui| { + commit = ui.button(if editing { tl!("Update") } else { tl!("Add") }).clicked(); + discard = editing && ui.button(tl!("Discard")).clicked(); + }); + if commit { match d.new_action.action() { Ok(act) => { let tr = d.new_action.trigger; @@ -1578,10 +1597,15 @@ fn actions_tab(ui: &mut egui::Ui, d: &mut FieldDraft, t: &crate::theme::Tokens) d.actions.push((tr, act)); d.actions.sort_by_key(|(x, _)| T::ALL.iter().position(|y| y == x)); d.new_action.text.clear(); + d.editing = None; } Err(e) => error = Some(e), } } + if discard { + d.new_action = ActionDraft::default(); + d.editing = None; + } if let Some(e) = error { ui.label(egui::RichText::new(tl!(&e)).small().color(t.text_muted)); } @@ -1592,19 +1616,40 @@ fn actions_tab(ui: &mut egui::Ui, d: &mut FieldDraft, t: &crate::theme::Tokens) if d.actions.is_empty() { ui.label(egui::RichText::new(tl!("No actions")).color(t.text_muted)); } - let mut remove = None; + let (mut remove, mut edit) = (None, None); for (i, (tr, act)) in d.actions.iter().enumerate() { ui.horizontal(|ui| { ui.label(egui::RichText::new(tl!(tr.label())).strong()); - ui.label(act.describe()); + let more = d.chained.iter().find(|(x, _)| x == tr).map_or(0, |(_, n)| *n); + if more == 0 { + ui.label(act.describe()); + } else { + let then = crate::i18n::trn(crate::i18n::current(), more as u64, "then {n} more action", "then {n} more actions"); + ui.label(format!("{}, {then}", act.describe())); + } ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - if ui.push_id(i, |ui| ui.button(tl!("Delete"))).inner.clicked() { - remove = Some(i); - } + ui.push_id(i, |ui| { + if ui.button(tl!("Delete")).clicked() { + remove = Some(i); + } + if let pdfcraft_engine::FieldAction::JavaScript(js) = act + && ui.add_enabled(d.editing.is_none(), egui::Button::new(tl!("Edit"))).clicked() + { + edit = Some((*tr, js.clone())); + } + }); }); }); } + if let Some((trigger, text)) = edit { + d.new_action = ActionDraft { trigger, kind: 0, text }; + d.editing = Some(trigger); + } if let Some(i) = remove { + if d.actions.get(i).map(|(x, _)| Some(*x)) == Some(d.editing) { + d.new_action = ActionDraft::default(); + d.editing = None; + } d.actions.remove(i); } }); diff --git a/crates/ui-egui/tests/it/prepare.rs b/crates/ui-egui/tests/it/prepare.rs index 106d2d539..13aa5b981 100644 --- a/crates/ui-egui/tests/it/prepare.rs +++ b/crates/ui-egui/tests/it/prepare.rs @@ -9,11 +9,15 @@ use pdfcraft_engine::field_table::TableColumn; use pdfcraft_ui_egui::{Dialog, PdfCraftApp, QuickTool}; fn harness() -> Harness<'static, PdfCraftApp> { - let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(|cc| { + harness_with("form.pdf", include_bytes!("../data/form.pdf").to_vec()) +} + +fn harness_with(name: &'static str, bytes: Vec) -> Harness<'static, PdfCraftApp> { + let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(move |cc| { cc.egui_ctx.options_mut(|options| options.warn_on_id_clash = true); let mut app = PdfCraftApp::new(); app.set_option("language", "en").unwrap(); - app.open_bytes("form.pdf", None, include_bytes!("../data/form.pdf").to_vec()).unwrap(); + app.open_bytes(name, None, bytes).unwrap(); app.set_option("zoom", "150").unwrap(); app }); @@ -712,6 +716,134 @@ fn actions_tab_adds_and_removes_actions() { ); } +#[test] +fn actions_tab_edits_the_script_of_an_existing_action() { + use pdfcraft_engine::{FieldAction, FieldTrigger}; + let mut h = harness(); + h.state_mut().execute("form.prepare"); + h.run_steps(2); + let script = "if (event.value > 10) {\n app.alert('Too many');\n}"; + let actions = |h: &Harness<'static, PdfCraftApp>| { + let s = h.state(); + s.session.get(s.views[0].id).unwrap().field_actions("city") + }; + let open = |h: &mut Harness<'static, PdfCraftApp>| { + h.state_mut().open_field_props("city", 0); + h.run_steps(2); + h.get_by_label("Actions").click(); + h.run_steps(2); + }; + // A script on Mouse Up and a link on Blur, saved with the field. + open(&mut h); + for (trigger, kind, text) in [(FieldTrigger::MouseUp, 0, script), (FieldTrigger::OnBlur, 1, "https://example.org")] { + let d = h.state_mut().field_props.as_mut().unwrap(); + d.new_action = pdfcraft_ui_egui::prepare::ActionDraft { trigger, kind, text: text.into() }; + h.run_steps(3); + h.get_by_label("Add").click(); + h.run_steps(2); + } + h.get_by_label("OK").click(); + h.run_steps(3); + let saved = actions(&h); + // Edit shows the script exactly as stored; Discard leaves everything as it was. + open(&mut h); + h.get_all_by_label("Edit").last().unwrap().click(); + h.run_steps(2); + h.get_by_label("Edit Action"); + assert_eq!(h.state().field_props.as_ref().unwrap().new_action.text, script); + h.state_mut().field_props.as_mut().unwrap().new_action.text = "app.alert('changed');".into(); + h.run_steps(2); + h.get_by_label("Discard").click(); + h.run_steps(2); + h.get_by_label("Add an Action"); + h.get_by_label("OK").click(); + h.run_steps(3); + assert_eq!(actions(&h), saved); + // Update replaces that script only: same trigger, the link untouched. + open(&mut h); + h.get_all_by_label("Edit").last().unwrap().click(); + h.run_steps(2); + h.state_mut().field_props.as_mut().unwrap().new_action.text = "app.alert('changed');".into(); + h.run_steps(2); + h.get_by_label("Update").click(); + h.run_steps(2); + h.get_by_label("OK").click(); + h.run_steps(3); + assert_eq!( + actions(&h), + [ + (FieldTrigger::MouseUp, FieldAction::JavaScript("app.alert('changed');".into())), + (FieldTrigger::OnBlur, FieldAction::Uri("https://example.org".into())) + ] + ); +} + +/// One page with a push button "go": Mouse Up runs a script with a link chained after it, On Blur +/// runs another script. +fn chained_action_fixture() -> Vec { + let objs = [ + "<< /Type /Catalog /Pages 2 0 R /AcroForm << /Fields [4 0 R] /DA (/Helv 0 Tf 0 g) >> >>", + "<< /Type /Pages /Kids [3 0 R] /Count 1 /MediaBox [0 0 300 400] >>", + "<< /Type /Page /Parent 2 0 R /Annots [4 0 R] >>", + "<< /Type /Annot /Subtype /Widget /FT /Btn /Ff 65536 /T (go) /Rect [20 300 100 320] /P 3 0 R /A 5 0 R /AA << /Bl << /S /JavaScript /JS (this.dirty = true;) >> >> >>", + "<< /S /JavaScript /JS (app.alert\\(1\\);) /Next 6 0 R >>", + "<< /S /URI /URI (https://example.org) >>", + ]; + let mut out = b"%PDF-1.7\n".to_vec(); + let mut offs = Vec::new(); + for (i, o) in objs.iter().enumerate() { + offs.push(out.len()); + out.extend_from_slice(format!("{} 0 obj\n{o}\nendobj\n", i + 1).as_bytes()); + } + let x = out.len(); + out.extend_from_slice(format!("xref\n0 {}\n0000000000 65535 f \n", objs.len() + 1).as_bytes()); + for o in offs { + out.extend_from_slice(format!("{o:010} 00000 n \n").as_bytes()); + } + out.extend_from_slice(format!("trailer\n<< /Size {} /Root 1 0 R >>\nstartxref\n{x}\n%%EOF\n", objs.len() + 1).as_bytes()); + out +} + +#[test] +fn actions_tab_shows_a_chain_and_keeps_it_when_the_first_action_is_replaced() { + use egui_kittest::kittest::NodeT; + use pdfcraft_engine::FieldTrigger; + let mut h = harness_with("chain.pdf", chained_action_fixture()); + h.state_mut().execute("form.prepare"); + h.run_steps(2); + let open = |h: &mut Harness<'static, PdfCraftApp>| { + h.state_mut().open_field_props("go", 0); + h.run_steps(2); + h.get_by_label("Actions").click(); + h.run_steps(2); + }; + open(&mut h); + h.get_by_label("Run a JavaScript, then 1 more action"); + // The first Edit is the menu bar's; the rows' follow (Mouse Up, On Blur). + assert_eq!(h.get_all_by_label("Edit").count(), 3); + h.get_all_by_label("Edit").nth(1).unwrap().click(); + h.run_steps(2); + assert_eq!(h.state().field_props.as_ref().unwrap().editing, Some(FieldTrigger::MouseUp)); + // While one script is open, no other Edit can overwrite it. + assert!(h.get_all_by_label("Edit").skip(1).all(|n| n.accesskit_node().is_disabled())); + h.get_by_label("Discard").click(); + h.run_steps(2); + assert!(h.get_all_by_label("Edit").skip(1).all(|n| !n.accesskit_node().is_disabled())); + // Replace the Mouse Up script with a link: the chained link stays after it. + { + let d = h.state_mut().field_props.as_mut().unwrap(); + d.actions.retain(|(t, _)| *t != FieldTrigger::MouseUp); + d.new_action = pdfcraft_ui_egui::prepare::ActionDraft { trigger: FieldTrigger::MouseUp, kind: 1, text: "https://example.com".into() }; + } + h.run_steps(3); + h.get_by_label("Add").click(); + h.run_steps(2); + h.get_by_label("OK").click(); + h.run_steps(3); + open(&mut h); + h.get_by_label("Open a web link: https://example.com, then 1 more action"); +} + /// Text field table: pick the tool and drag out an area in the empty lower part of page 1, 200 by /// 120 points from (40, 240) as displayed. The table's window opens for it. fn draw_table(h: &mut Harness<'static, PdfCraftApp>) { diff --git a/parity/acrobat-features.toml b/parity/acrobat-features.toml index 333e0784c..2b2c00a51 100644 --- a/parity/acrobat-features.toml +++ b/parity/acrobat-features.toml @@ -4956,8 +4956,8 @@ milestone = "M6" status = "shipped" commands = ["form.prepare"] tools = ["form_actions", "form_set_actions"] -evidence = ["crates/forms/src/tests.rs::field_actions_round_trip_on_every_trigger", "crates/automation/tests/it/automation.rs::field_actions_through_tools", "crates/ui-egui/tests/it/prepare.rs::actions_tab_adds_and_removes_actions"] -notes = "Six triggers; Run a JavaScript, Open a web link, Reset a form, Execute a menu item, Go to a page view, Show/hide a field, Submit a form. Other action types are kept." +evidence = ["crates/forms/src/tests.rs::field_actions_round_trip_on_every_trigger", "crates/automation/tests/it/automation.rs::field_actions_through_tools", "crates/ui-egui/tests/it/prepare.rs::actions_tab_adds_and_removes_actions", "crates/ui-egui/tests/it/prepare.rs::actions_tab_edits_the_script_of_an_existing_action", "crates/forms/src/tests.rs::editing_one_script_keeps_the_other_actions_and_the_chain", "crates/forms/src/tests.rs::replacing_a_chained_action_with_another_type_keeps_the_chain", "crates/ui-egui/tests/it/prepare.rs::actions_tab_shows_a_chain_and_keeps_it_when_the_first_action_is_replaced"] +notes = "Six triggers; Run a JavaScript, Open a web link, Reset a form, Execute a menu item, Go to a page view, Show/hide a field, Submit a form. Other action types are kept. Edit opens the script of an existing Run a JavaScript action; unchanged actions are kept as they are, and replacing a trigger's first action keeps the actions chained after it, which the row counts. Edit for the other action types, and editing the chained actions, are missing." [[feature]] id = "form.props-format"