From e4191e0d2c65377ec0fbae7a4009dd4c874453d7 Mon Sep 17 00:00:00 2001 From: bflatastic Date: Wed, 7 Oct 2026 09:31:35 +0700 Subject: [PATCH] Prepare a form: Detect form fields in the panel, delete several fields at once (#93, #95) - #93: field detection already ran when Prepare a form starts on a document without fields, and as the form.detect command, but nothing in the tool panel offered it. Prepare a form > Fields now lists Detect form fields; on a form that already has fields it only adds new ones. - #95: with several fields selected (Shift/Cmd-click), Delete and the context menu's Delete removed only the right-clicked one. They now remove every selected field as one undoable step ("Delete 3 fields"). One prepare::delete_selected serves both. Editing the shared properties of several fields at once is still to do. Co-Authored-By: Claude Opus 5.5 --- crates/engine/src/catalog.rs | 1 + crates/ui-egui/src/canvas.rs | 6 +++--- crates/ui-egui/src/prepare.rs | 24 +++++++++++++++++++++--- crates/ui-egui/tests/prepare.rs | 29 +++++++++++++++++++++++++++++ parity/acrobat-features.toml | 8 ++++---- 5 files changed, 58 insertions(+), 10 deletions(-) diff --git a/crates/engine/src/catalog.rs b/crates/engine/src/catalog.rs index 3b3101f1..dc3d6b2c 100644 --- a/crates/engine/src/catalog.rs +++ b/crates/engine/src/catalog.rs @@ -323,6 +323,7 @@ pub static TOOL_GROUPS: &[ToolGroup] = &[ title: "Fields", items: &[ item("Field list", "list", "form.fields", Ready), + item("Detect form fields", "scan", "form.detect", Ready), item("Clear form", "eraser", "form.clear", Ready), item("Field properties", "settings-2", "form.field.properties", Ready), item("Tab order by rows", "rows-3", "form.tab_order.row", Ready), diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index 71e0d5f4..d75ced88 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -1485,9 +1485,9 @@ pub fn document_area(app: &mut PrintCraftApp, index: usize, ui: &mut egui::Ui) { ui.close(); } ui.separator(); - if ui.add_enabled(can_modify, egui::Button::new("Delete")).clicked() { - view.prepare.selected = None; - view.pending_edit = Some(printcraft_engine::Edit::DeleteField { name }); + let delete = if view.prepare.also.is_empty() { "Delete" } else { "Delete selected fields" }; + if ui.add_enabled(can_modify, egui::Button::new(delete)).clicked() { + view.pending_edit = crate::prepare::delete_selected(view); ui.close(); } return; diff --git a/crates/ui-egui/src/prepare.rs b/crates/ui-egui/src/prepare.rs index 42cb1a83..02da3764 100644 --- a/crates/ui-egui/src/prepare.rs +++ b/crates/ui-egui/src/prepare.rs @@ -498,16 +498,34 @@ pub(crate) fn paint_page(ui: &egui::Ui, painter: &egui::Painter, xf: &PageXform, } } -/// Delete removes the selected field; Escape clears the selection. +/// Delete the selected field and every field selected with it (#95), as one undoable step, and +/// clear the selection. +pub(crate) fn delete_selected(view: &mut DocView) -> Option { + let (first, _) = view.prepare.selected.take()?; + let mut names = vec![first]; + for (name, _) in std::mem::take(&mut view.prepare.also) { + if !names.contains(&name) { + names.push(name); + } + } + if names.len() == 1 { + return names.pop().map(|name| Edit::DeleteField { name }); + } + let label = format!("Delete {} fields", names.len()); + Some(Edit::Batch { label, edits: names.into_iter().map(|name| Edit::DeleteField { name }).collect() }) +} + +/// Delete removes the selected fields; Escape clears the selection. pub(crate) fn keys(ctx: &egui::Context, view: &mut DocView) { if view.prepare.selected.is_none() || ctx.egui_wants_keyboard_input() { return; } let (del, esc) = ctx.input(|i| (i.key_pressed(egui::Key::Delete) || i.key_pressed(egui::Key::Backspace), i.key_pressed(egui::Key::Escape))); - if del && let Some((name, _)) = view.prepare.selected.take() { - view.pending_edit = Some(Edit::DeleteField { name }); + if del { + view.pending_edit = delete_selected(view); } else if esc { view.prepare.selected = None; + view.prepare.also.clear(); } } diff --git a/crates/ui-egui/tests/prepare.rs b/crates/ui-egui/tests/prepare.rs index 917f40b7..ca0b2521 100644 --- a/crates/ui-egui/tests/prepare.rs +++ b/crates/ui-egui/tests/prepare.rs @@ -285,6 +285,35 @@ fn aligning_distributing_and_sizing_several_fields() { assert!(arrange(&form(&h), &a, &[("b".into(), 0)], Arrange::DistributeV).is_none(), "distributing needs three"); } +#[test] +fn deleting_several_selected_fields_and_detecting_fields_from_the_panel() { + let mut h = harness(); + for (name, x) in [("a", 20.0), ("b", 120.0), ("c", 220.0)] { + h.state_mut().apply_edit(printcraft_engine::Edit::AddField { + page: 0, + rect: [x, 300.0, x + 60.0, 320.0], + kind: printcraft_engine::NewField::Text { multiline: false }, + name: Some(name.into()), + }); + } + assert!(h.state_mut().execute("form.prepare")); + h.run_steps(2); + // #93: field detection is in the panel, not only run when a form has no fields. + h.get_by_label("Detect form fields"); + // #95: Delete removes every selected field, as one undoable step. + { + let p = &mut h.state_mut().views[0].prepare; + p.selected = Some(("a".into(), 0)); + p.also = vec![("b".into(), 0), ("c".into(), 0)]; + } + h.key_press(egui::Key::Delete); + h.run_steps(3); + let left = names(&h); + assert!(!["a", "b", "c"].iter().any(|n| left.iter().any(|l| l == n)), "{left:?}"); + assert_eq!(h.state().session.get(h.state().views[0].id).unwrap().can_undo(), Some("Delete 3 fields")); + assert!(h.state().views[0].prepare.selected.is_none() && h.state().views[0].prepare.also.is_empty()); +} + #[test] fn preview_fills_the_form_and_locked_fields_keep_their_properties() { let mut h = harness(); diff --git a/parity/acrobat-features.toml b/parity/acrobat-features.toml index 91978823..80ee7b1f 100644 --- a/parity/acrobat-features.toml +++ b/parity/acrobat-features.toml @@ -4650,8 +4650,8 @@ milestone = "M6" status = "shipped" commands = ["form.prepare"] tools = ["form_add_field", "form_set_props", "form_delete_field"] -evidence = ["crates/ui-egui/tests/prepare.rs::placing_moving_editing_and_deleting_a_field", "crates/engine/src/tests.rs::preparing_a_form_adds_renames_and_deletes_fields"] -notes = "fields shown with blue fill and name tags; select, move, resize by corners, Delete; field tools return to Select after one field" +evidence = ["crates/ui-egui/tests/prepare.rs::placing_moving_editing_and_deleting_a_field", "crates/ui-egui/tests/prepare.rs::deleting_several_selected_fields_and_detecting_fields_from_the_panel", "crates/engine/src/tests.rs::preparing_a_form_adds_renames_and_deletes_fields"] +notes = "fields shown with blue fill and name tags; select, move, resize by corners, Delete (every selected field, one undo step); field tools return to Select after one field. Not yet: editing shared properties of several fields at once" [[feature]] id = "form.add-text-field" @@ -4986,8 +4986,8 @@ milestone = "M6" status = "partial" commands = ["form.detect", "form.prepare"] tools = ["form_detect_fields"] -evidence = ["crates/forms/src/tests.rs::detection_finds_blanks_rules_boxes_and_names_them", "crates/engine/src/tests.rs::detecting_fields_on_a_printed_form", "crates/automation/tests/automation.rs::detecting_form_fields_through_tools", "crates/ui-egui/tests/javascript.rs::prepare_a_form_detects_fields_on_a_paper_form"] -notes = "Underscore runs, rules, empty boxes and small squares (check boxes); runs when Prepare a form starts on a document without fields. Not yet: comb boxes, tables of cells, scanned forms (needs OCR first), radio groups." +evidence = ["crates/forms/src/tests.rs::detection_finds_blanks_rules_boxes_and_names_them", "crates/engine/src/tests.rs::detecting_fields_on_a_printed_form", "crates/automation/tests/automation.rs::detecting_form_fields_through_tools", "crates/ui-egui/tests/javascript.rs::prepare_a_form_detects_fields_on_a_paper_form", "crates/ui-egui/tests/prepare.rs::deleting_several_selected_fields_and_detecting_fields_from_the_panel"] +notes = "Underscore runs, rules, empty boxes and small squares (check boxes); runs when Prepare a form starts on a document without fields, and from Prepare a form ▸ Detect form fields at any time (existing fields are skipped). Not yet: comb boxes, tables of cells, scanned forms (needs OCR first), radio groups." [[feature]] id = "form.auto-name-fields"