diff --git a/ROADMAP.md b/ROADMAP.md index 4f3d24633..47931133f 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -177,6 +177,7 @@ Ranked; detail and the full milestone table in [docs/roadmap.md](docs/roadmap.md - **2026-10-20 (M6, calculation order):** The order a form's fields calculate in (`/CO`) is read, checked, fixed and set headlessly with `form_calculation_order`: it says which fields read one calculated later (they show stale values), which read each other in a loop, and which the PDF's order leaves out; Fix order moves only what must move, in one undo step, and the form recalculates. Editing a formula no longer moves its field to the end of the order, and a field the order lists twice calculates once. Estimates unchanged. - **2026-10-19 (M6, renames keep formulas):** Renaming a field renames it in every calculation that reads it, in the same undo step: `AFSimple_Calculate` field lists, simplified field notation (the comment and the JavaScript after it) and `getField` calls naming it in one string literal; the rest of each script is kept as written. A sum used to drop the renamed field, notation read it as 0 and a script kept a stale value. Field Properties says how many formulas changed and `form_set_props` lists them as `formulas_updated`. Names built at run time and other events' scripts are not rewritten. Estimates unchanged. - **2026-10-20 (M6, #3502):** Form detection preserves comments across content streams and does not add fields over rectangles hidden by those comments. Synthetic tests cover visible controls, graphics state, and saved field inventories. Estimates unchanged. +- **2026-10-20 (M6, #2273 / PR #2279):** Rebased the field-highlight default and Appearance preference onto main, retaining secured-form parity evidence and updating secured AcroForm/XFA toggle expectations. Build and test validation delegated to GitHub CI. Estimates unchanged. - **2026-10-19 (M14, #3373):** The main menu and its long submenus scroll in short windows, keeping their commands reachable. Estimates unchanged. diff --git a/crates/ui-egui/src/appearance.rs b/crates/ui-egui/src/appearance.rs index 0d607cc91..536d8e4d9 100644 --- a/crates/ui-egui/src/appearance.rs +++ b/crates/ui-egui/src/appearance.rs @@ -139,6 +139,9 @@ pub fn preferences_section(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens) app.execute(mode_command(ThemePreference::Dark)); } }); + ui.add_space(4.0); + // Acrobat's "Highlight existing fields": shared with the toggle in the form bar. + ui.add(crate::widgets::checkbox(&mut app.view_defaults.highlight_fields, tl!("Highlight fields"))); } #[cfg(test)] @@ -176,6 +179,12 @@ mod tests { h.render().unwrap().save(path).unwrap(); } + // The field-highlight preference is on for new settings, and the box turns it off. + assert!(h.state().view_defaults.highlight_fields); + h.get_by_label("Highlight fields").click(); + h.run(); + assert!(!h.state().view_defaults.highlight_fields); + let combo = egui::accesskit::Role::ComboBox; h.get_by_role(combo).click(); h.run(); diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index 8145fd168..9e773af9e 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -174,14 +174,14 @@ pub struct ViewDefaults { /// The zoom with [`Fit::None`] (1.0 = 100%). pub zoom: f32, /// Highlight existing fields: a preference in Acrobat, so it carries over to the next - /// document and the next session. + /// document and the next session. On for new settings; a stored choice wins. pub highlight_fields: bool, } impl Default for ViewDefaults { fn default() -> Self { // Single page, scrolling continuously at fit width: the earlier "continuous" default. - Self { layout: PageLayout::Single, continuous: true, fit: Fit::Width, zoom: 1.0, highlight_fields: false } + Self { layout: PageLayout::Single, continuous: true, fit: Fit::Width, zoom: 1.0, highlight_fields: true } } } diff --git a/crates/ui-egui/tests/editing.rs b/crates/ui-egui/tests/editing.rs index 6885a2bb6..bf7c71361 100644 --- a/crates/ui-egui/tests/editing.rs +++ b/crates/ui-egui/tests/editing.rs @@ -2198,7 +2198,7 @@ fn a_secured_form_shows_its_form_notice_and_its_security() { }); h.run_steps(4); h.get_by_label_contains("interactive form fields. This document is secured. Some changes are restricted by its security settings."); - h.get_by_label("Highlight fields"); + h.get_by_label("Hide field highlights"); h.get_by_label("Security settings"); h.get_by_label("Unlock…"); } diff --git a/crates/ui-egui/tests/ui_group/animate.rs b/crates/ui-egui/tests/ui_group/animate.rs index 20008edd6..d01bf6de1 100644 --- a/crates/ui-egui/tests/ui_group/animate.rs +++ b/crates/ui-egui/tests/ui_group/animate.rs @@ -67,6 +67,7 @@ fn push_buttons_alone_show_no_form_bar_and_a_screen_annotation_is_not_a_comment( } assert!(h.query_by_label_contains("interactive form fields").is_none(), "nothing to fill in, so no form bar"); assert!(h.query_by_label("Highlight fields").is_none()); + assert!(h.query_by_label("Hide field highlights").is_none()); } #[test] @@ -75,5 +76,5 @@ fn a_fillable_field_beside_push_buttons_still_shows_the_form_bar() { let text = "<< /Type /Annot /Subtype /Widget /FT /Tx /T (name) /Rect [20 160 200 180] /P 3 0 R >>"; let h = open(pdf(&[&button, text, SCREEN])); h.get_by_label_contains("This document contains 2 interactive form fields."); - h.get_by_label("Highlight fields"); + h.get_by_label("Hide field highlights"); } diff --git a/crates/ui-egui/tests/ui_group/xfa.rs b/crates/ui-egui/tests/ui_group/xfa.rs index 6565a9816..e66658b0e 100644 --- a/crates/ui-egui/tests/ui_group/xfa.rs +++ b/crates/ui-egui/tests/ui_group/xfa.rs @@ -58,8 +58,8 @@ fn a_dynamic_xfa_form_is_laid_out_and_filled() { let doc = s.session.get(s.views[0].id).unwrap(); assert_eq!(doc.info.pages.len(), 2); assert!(doc.form.iter().any(|f| f.name == "familyName")); - // The notice offers to highlight the fields, like any form. - h.get_by_label("Highlight fields"); + // The notice offers the highlight toggle, like any form; highlights are on by default. + h.get_by_label("Hide field highlights"); } #[test] @@ -87,8 +87,8 @@ fn a_static_xfa_form_is_filled_from_its_data_and_says_so() { assert_eq!(doc.form.iter().find(|f| f.name == "form1[0].page1[0].name[0]").unwrap().value, vec!["Ada".to_string()]); assert_eq!(doc.form.iter().find(|f| f.name == "form1[0].page1[0].agree[0]").unwrap().value, vec!["1".to_string()]); assert!(!doc.dirty); - // The fields can still be highlighted from the notice. - h.get_by_label("Highlight fields"); + // The highlight toggle (on by default, so it reads "Hide") is still in the notice. + h.get_by_label("Hide field highlights"); } #[test] @@ -228,6 +228,6 @@ fn a_secured_dynamic_xfa_form_shows_its_layout_and_its_security() { .unwrap(); let h = open(pdfcraft_cos::write_full(&doc, &Default::default()).unwrap()); h.get_by_label_contains("laid out from its template: 2 pages, 11 fields. This document is secured."); - h.get_by_label("Highlight fields"); + h.get_by_label("Hide field highlights"); h.get_by_label("Security settings"); } diff --git a/crates/ui-egui/tests/view.rs b/crates/ui-egui/tests/view.rs index 94979fadb..7aee9ce02 100644 --- a/crates/ui-egui/tests/view.rs +++ b/crates/ui-egui/tests/view.rs @@ -1059,6 +1059,8 @@ fn highlight_fields_tints_the_field_area() { let _gpu = gpu(); let mut h = form_harness(); h.state_mut().set_option("panel", "none").unwrap(); + // Highlighting is on by default; start from plain fields. + h.state_mut().set_option("fields", "off").unwrap(); for _ in 0..100 { h.run_steps(2); if !h.state().render_pending() { @@ -1230,27 +1232,47 @@ fn required_radio_buttons_get_a_round_red_border() { assert!(!stroke(at(50.3, 339.7)), "the widget's corner, outside the circle, is not: {:?}", at(50.3, 339.7)); } -/// Acrobat's Highlight existing fields is a preference, not a per-document choice: turned on -/// once, the next document (and the next session) opens with fields highlighted. +/// Acrobat's Highlight existing fields is a preference, not a per-document choice: turned off +/// once, the next document (and the next session) opens with fields plain. New settings start +/// with it on. #[test] -fn field_highlighting_is_remembered() { +fn field_highlighting_is_on_by_default_and_remembered() { use egui_kittest::kittest::Queryable; let mut h = form_harness(); h.state_mut().set_option("panel", "none").unwrap(); h.run_steps(4); - assert!(!h.state().views[0].highlight_fields, "off by default"); - h.get_by_label("Highlight fields").click(); + assert!(h.state().views[0].highlight_fields, "on by default"); + h.get_by_label("Hide field highlights").click(); h.run_steps(4); - assert!(h.state().views[0].highlight_fields); + assert!(!h.state().views[0].highlight_fields); h.state_mut().open_bytes("radios.pdf", None, RADIOS.to_vec()).expect("opens"); h.run_steps(4); - assert!(h.state().views[1].highlight_fields, "the next document opens highlighted"); + assert!(!h.state().views[1].highlight_fields, "the next document opens plain"); let saved = h.state().persist(); let mut next = PdfCraftApp::new(); next.set_option("language", "en").unwrap(); next.restore(&saved); next.open_bytes("form.pdf", None, FORM.to_vec()).expect("opens"); - assert!(next.views[0].highlight_fields, "and so does the next session"); + assert!(!next.views[0].highlight_fields, "and so does the next session"); +} + +/// Settings saved before the default changed keep their stored choice; a missing or malformed +/// value falls back to the default instead of failing. +#[test] +fn stored_field_highlight_choice_wins_over_the_default() { + for (stored, expected) in [ + (r#"{"highlight_fields": false}"#, false), + (r#"{"highlight_fields": true}"#, true), + (r#"{}"#, true), + (r#"{"highlight_fields": "no"}"#, true), + (r#"{"highlight_fields": null}"#, true), + ] { + let mut app = PdfCraftApp::new(); + app.set_option("language", "en").unwrap(); + app.restore(stored); + app.open_bytes("form.pdf", None, FORM.to_vec()).expect("opens"); + assert_eq!(app.views[0].highlight_fields, expected, "{stored}"); + } } #[test] diff --git a/docs/roadmap.md b/docs/roadmap.md index 054711e0d..528c2a7a6 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -136,7 +136,7 @@ total matches the parity estimate. | M4 | Engine, history, save, organize | 100–180 | 95% | 8–15 | Done:
- command registry (menus, shortcuts and palette all use it);
- undo/redo; incremental, atomic and encrypted saves;
- autosave and crash recovery;
- organize, combine, extract, split and insert-from-file, with identical fonts and images stored once;
- CLI `edit/combine/extract/split`.
Done since: Combine files in its own tab (sortable, resizable columns; folders; early warnings; unlocking protected files), bookmark editing, page labels (Number pages), CLI `run`, Set Page Boxes, Crop tool, Duplicate pages, Ctrl+A / Cmd+A to select all pages in Organize, Replace pages, bookmarks from tagged headings. Missing: page transitions, recovery on the web M4: Insert-from-file preserves source bookmarks; integration tests retained with current main. | | M5 | Comments (all annotation types, XFDF) | 120–200 | 88% | 12–25 | Done: notes, highlight/underline/strikeout/squiggly, text boxes, ink, lines, arrows, rectangles, ovals, stamps (dynamic, Sign Here, Standard Business), with appearance streams; replies, status, checkmarks, locking, move/resize/restyle/delete; properties dialog; panel filter and sort; hide all; summaries (comments only); XFDF/FDF import and export (partial imports preserve existing reply and review-status links); flatten; Fill & Sign with adjustable text/date size; agent tools.
Done since: stamps placed on a rotated page read upright (#1398); polygons, connected lines, clouds, callouts, inserted text, colour and checkmark filters; image signature pointer previews, selection handles and live image movement/resizing with proportional corners and independent edge handles. Missing: custom stamps, replace-text proposals, summary layouts with the page | | M5 | Comments (all annotation types, XFDF) | 120–200 | 88% | 12–25 | Done: notes, highlight/underline/strikeout/squiggly, text boxes, ink, lines, arrows, rectangles, ovals, stamps (dynamic, Sign Here, Standard Business), with appearance streams; replies, status, checkmarks, locking, move/resize/restyle/delete; properties dialog; panel filter and sort; hide all; summaries (comments only); XFDF/FDF import and export (partial imports preserve existing reply and review-status links; nested FDF export uses lazy array owners and annotation positions, with retained entries budgeted before cloning under copied-graph limits and source `/Page` and `/Pages` dictionaries replaced by null at every copy depth); flatten; Fill & Sign with adjustable text/date size; agent tools.
Done since: polygons, connected lines, clouds, callouts, inserted text, colour and checkmark filters; image signature pointer previews, selection handles and live image movement/resizing with proportional corners and independent edge handles. A refused restyle or resize of a text box another program drew leaves that comment unchanged (#1327). Make Current Properties Default on an arrow updates arrows, not plain lines (#1302). Flattening keeps a visible comment it cannot draw (#1317). Missing: custom stamps, replace-text proposals, summary layouts with the page | -| M6 | Forms + JavaScript | 160–320 | 50% | 40–75 | Done: filling all field types with regenerated appearances; Prepare a form (every field type, multi-select/shared properties, move/resize/delete, Field Properties General/Appearance/Position/Options/Format/Validate/Calculate); Acrobat's AF format/keystroke/validate/calculate functions and simplified field notation run natively in Acrobat's event order; tab order; push-button actions; flatten; agent tools. Form data exchange (FDF, XFDF, XML, CSV, text) done; XFDF values retain text across XML comments and processing instructions. Plain AcroForm XML imports retain all direct text around comments and processing instructions, including CDATA and character references. CSV export and spreadsheet merging quote tabs, and import distinguishes quoted tabs from TSV delimiters without changing field identities. XML/XFDF exports preserve tabs and line breaks in names and comment metadata. Refreshed against upstream `99941daa` (2026-10-15).
Done since: JavaScript engine (boa, sandboxed) running custom keystroke/validate/calculate/format and button scripts, document JavaScripts, console. Done since: Push-button normal labels are editable per widget through Options and form_set_props, independently of field names; rendering, undo/redo and incremental/full save/reopen are covered. (#2892). Missing: Actions tab, wider object model (annotations, layers, dialogs), XFA
Done since: Multi-select choice fields write ascending `/I` option indices while preserving `/V` selection order; synthetic forms/engine regressions cover reverse selection and save/reopen. CSV export preserves carriage returns in names and values.
Done since: Form detection preserves comments across content streams and does not add fields over rectangles hidden by those comments. | +| M6 | Forms + JavaScript | 160–320 | 50% | 40–75 | Done: filling all field types with regenerated appearances; Prepare a form (every field type, multi-select/shared properties, move/resize/delete, Field Properties General/Appearance/Position/Options/Format/Validate/Calculate); Acrobat's AF format/keystroke/validate/calculate functions and simplified field notation run natively in Acrobat's event order; tab order; push-button actions; flatten; agent tools. Form data exchange (FDF, XFDF, XML, CSV, text) done; XFDF values retain text across XML comments and processing instructions. Plain AcroForm XML imports retain all direct text around comments and processing instructions, including CDATA and character references. CSV export and spreadsheet merging quote tabs, and import distinguishes quoted tabs from TSV delimiters without changing field identities. XML/XFDF exports preserve tabs and line breaks in names and comment metadata. Refreshed against upstream `99941daa` (2026-10-15).
Done since: JavaScript engine (boa, sandboxed) running custom keystroke/validate/calculate/format and button scripts, document JavaScripts, console. Done since: Push-button normal labels are editable per widget through Options and form_set_props, independently of field names; rendering, undo/redo and incremental/full save/reopen are covered. (#2892). Missing: Actions tab, wider object model (annotations, layers, dialogs), XFA
Done since: Multi-select choice fields write ascending `/I` option indices while preserving `/V` selection order; synthetic forms/engine regressions cover reverse selection and save/reopen. CSV export preserves carriage returns in names and values.
Done since: Form detection preserves comments across content streams and does not add fields over rectangles hidden by those comments. PR #2279 refreshed on main: field highlighting defaults on, stored choices are retained; secured-form toggle expectations updated. CI validation pending; estimates unchanged. | | M6 | Forms + JavaScript | 160–320 | 50% | 40–75 | Done: filling all field types with regenerated appearances; Prepare a form (every field type, multi-select/shared properties, move/resize/delete, Field Properties General/Appearance/Position/Options/Format/Validate/Calculate); Acrobat's AF format/keystroke/validate/calculate functions and simplified field notation run natively in Acrobat's event order; tab order; push-button actions; flatten; agent tools. Form data exchange (FDF, XFDF, XML, CSV, text) done; XFDF values retain text across XML comments and processing instructions. Plain AcroForm XML imports retain all direct text around comments and processing instructions, including CDATA and character references.
Done since: JavaScript engine (boa, sandboxed) running custom keystroke/validate/calculate/format and button scripts, document JavaScripts, console. Missing: Actions tab, wider object model (annotations, layers, dialogs), XFA
Done since: Multi-select choice fields write ascending `/I` option indices while preserving `/V` selection order; synthetic forms/engine regressions cover reverse selection and save/reopen. CSV export preserves carriage returns in names and values. | | M6 | Forms + JavaScript | 160–320 | 50% | 40–75 | Done: filling all field types with regenerated appearances; Prepare a form (every field type, multi-select/shared properties, move/resize/delete, Field Properties General/Appearance/Position/Options/Format/Validate/Calculate); Acrobat's AF format/keystroke/validate/calculate functions and simplified field notation run natively in Acrobat's event order; tab order; push-button actions; flatten; agent tools. Form data exchange (FDF, XFDF, XML, CSV, text) done; XFDF values retain text across XML comments and processing instructions. XML/XFDF exports preserve tabs and line breaks in names and comment metadata. Refreshed against upstream `99941daa` (2026-10-15).
Done since: JavaScript engine (boa, sandboxed) running custom keystroke/validate/calculate/format and button scripts, document JavaScripts, console. Missing: Actions tab, wider object model (annotations, layers, dialogs), XFA
Done since: Multi-select choice fields write ascending `/I` option indices while preserving `/V` selection order; synthetic forms/engine regressions cover reverse selection and save/reopen. CSV export preserves carriage returns in names and values. | | M6 | Forms + JavaScript | 160–320 | 50% | 40–75 | Done: filling all field types with regenerated appearances; Prepare a form (every field type, multi-select/shared properties, move/resize/delete, Field Properties General/Appearance/Position/Options/Format/Validate/Calculate); Acrobat's AF format/keystroke/validate/calculate functions and simplified field notation run natively in Acrobat's event order; tab order; push-button actions; flatten; agent tools. Form data exchange (FDF, XFDF, XML, CSV, text) done; XFDF values retain text across XML comments and processing instructions. XML/XFDF exports preserve tabs and line breaks in names and comment metadata. Plain AcroForm XML imports retain all direct text around comments and processing instructions, including CDATA and character references. CSV export and spreadsheet merging quote tabs, and import distinguishes quoted tabs from TSV delimiters without changing field identities.
Done since: JavaScript engine (boa, sandboxed) running custom keystroke/validate/calculate/format and button scripts, document JavaScripts, console. Missing: Actions tab, wider object model (annotations, layers, dialogs), XFA
Done since: Multi-select choice fields write ascending `/I` option indices while preserving `/V` selection order; synthetic forms/engine regressions cover reverse selection and save/reopen. CSV export preserves carriage returns in names and values. | diff --git a/parity/acrobat-features.toml b/parity/acrobat-features.toml index 5981e7b98..40df92963 100644 --- a/parity/acrobat-features.toml +++ b/parity/acrobat-features.toml @@ -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/view.rs::highlight_fields_leaves_read_only_fields_alone", "crates/ui-egui/tests/editing.rs::a_secured_form_shows_its_form_notice_and_its_security"] -notes = "only fields that can be filled are marked (a read-only field is left as drawn, so a form's locked boxes don't look fillable); 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_on_by_default_and_remembered", "crates/ui-egui/tests/view.rs::stored_field_highlight_choice_wins_over_the_default", "crates/ui-egui/tests/view.rs::highlight_fields_leaves_read_only_fields_alone", "crates/ui-egui/tests/editing.rs::a_secured_form_shows_its_form_notice_and_its_security"] +notes = "only fields that can be filled are marked (a read-only field is left as drawn, so a form's locked boxes don't look fillable); offered on secured forms too; on by default for new settings (a stored choice is kept) and remembered for the next document and session; toggled from the form bar and from Preferences > Appearance, where it sets the default for documents opened afterwards; 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"