diff --git a/ROADMAP.md b/ROADMAP.md index 40f62ab883..2e5daa8d16 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -245,6 +245,7 @@ Newest first: the date, what moved, and the new overall estimate. - **2026-10-12 (M3, GNOME title bar, #553):** On GNOME's Wayland session, whose compositor draws no window frames (it offers no `zxdg_decoration_manager_v1`), PdfCraft no longer shows winit's fallback title bar: it turns the frame off and the tab strip becomes the header bar, as in GNOME's own apps. Minimize, maximize/restore and close are drawn in Adwaita's style, in the order and on the side GNOME's `button-layout` names (Fedora's default shows only close); the window's edges and corners resize it, and a hairline outlines it while it isn't maximized. Preferences ▸ Use system title bar (shown only there) gives the window back winit's frame at once, without a restart (part of #1001), and `PDFCRAFT_TITLEBAR=app|system` overrides the choice. KDE, Sway, Hyprland, COSMIC and X11 keep the frame their desktop draws. Not done: right-clicking the header bar doesn't open GNOME's window menu (egui can't ask for it), the window has no shadow or rounded corners, the resize edges lie just inside the window rather than outside it, and Windows still uses its system frame (the rest of #1001). Estimates unchanged. - **2026-10-12 (M0, build label):** A local or patched build can carry a label, set at compile time with `PDFCRAFT_BUILD_LABEL`: `pdfcraft --version`, `pdfcraft-cli --version`, About and the log show `0.7.1 (titlebar)`, and `packaging/linux/package.sh` names the files `pdfcraft-0.7.1-titlebar-…` and makes the .rpm/.deb release `1.titlebar`, which upgrades over the release of the same version (and the next release over it). The version itself is unchanged, so the update check still compares it with published releases. Unlabelled builds are as before. Estimates unchanged. +- **2026-10-11 (M6, #1208):** Dropdown and list-box Field Properties expose the existing Left/Center/Right alignment picker. Existing alignment stays intact until changed; cancel, undo/redo and save/reopen are covered through the real UI without changing options, values or flags. Estimates unchanged. - 2026-10-11: Organize Pages shows an editable, synchronized physical page-range selection (#1092), with bounded validation and UI/operation regression coverage; overall estimate unchanged. - **2026-10-11 (M5.7, scanned signatures, #1110):** Fill & Sign ▸ Add signature has a Scan tab: a phone photo of a signature on paper is framed automatically (all words; table, sheet edge and dust ignored; a sideways signature turned upright), the frame can be moved, resized or redrawn, the photo turned, and an ink-sensitivity slider picks up faint ink. A morphological paper estimate removes shadows and uneven light, and the trimmed ink is saved and reused like an image signature. Photos and images can also be dropped on the Image and Scan tabs. Photos keep the image-signature limits (#1124: a 4 MiB file, up to 32 megapixels decoded, scaled down to 4 megapixels), so a scan never processes more than 4 megapixels. `fill_sign_add` takes `scan` and `sensitivity`. Synthetic engine, UI and automation tests; checked by hand on real phone photos. Milestone and effort estimates unchanged. diff --git a/crates/ui-egui/src/prepare.rs b/crates/ui-egui/src/prepare.rs index e8d3db838c..386c42e872 100644 --- a/crates/ui-egui/src/prepare.rs +++ b/crates/ui-egui/src/prepare.rs @@ -921,6 +921,22 @@ fn flag_box(ui: &mut egui::Ui, flags: &mut u32, bit: u32, inverted: bool, label: } } +/// Shared PDF `/Q` picker for text and choice-field Options. +fn alignment(ui: &mut egui::Ui, quadding: &mut i64) { + ui.label(tl!("Alignment:")); + egui::ComboBox::from_id_salt("quadding") + .selected_text(tl!(match (*quadding).clamp(0, 2) { + 1 => "Center", + 2 => "Right", + _ => "Left", + })) + .show_ui(ui, |ui| { + for (q, label) in [(0, tl!("Left")), (1, tl!("Center")), (2, tl!("Right"))] { + ui.selectable_value(quadding, q, label); + } + }); +} + /// Draw Field Properties; returns (apply, cancel). pub(crate) fn body(ui: &mut egui::Ui, d: &mut FieldDraft, t: &crate::theme::Tokens) -> (bool, bool) { use crate::widgets; @@ -1074,14 +1090,7 @@ pub(crate) fn body(ui: &mut egui::Ui, d: &mut FieldDraft, t: &crate::theme::Toke FormFieldKind::Text => { use pdfcraft_engine::field_flags as ff; egui::Grid::new("text-options").num_columns(2).spacing([12.0, 8.0]).show(ui, |ui| { - ui.label(tl!("Alignment:")); - egui::ComboBox::from_id_salt("quadding") - .selected_text(tl!(["Left", "Center", "Right"][d.quadding.clamp(0, 2) as usize])) - .show_ui(ui, |ui| { - for (q, l) in [(0, tl!("Left")), (1, tl!("Center")), (2, tl!("Right"))] { - ui.selectable_value(&mut d.quadding, q, l); - } - }); + alignment(ui, &mut d.quadding); ui.end_row(); let l = ui.label(tl!("Default Value:")); ui.add(crate::widgets::line(&mut d.default).desired_width(260.0)).labelled_by(l.id); @@ -1142,6 +1151,8 @@ pub(crate) fn body(ui: &mut egui::Ui, d: &mut FieldDraft, t: &crate::theme::Toke } } _ => { + ui.horizontal(|ui| alignment(ui, &mut d.quadding)); + ui.add_space(6.0); ui.horizontal(|ui| { let l = ui.label(tl!("Item:")); let r = ui.add(crate::widgets::line(&mut d.new_option).desired_width(240.0)).labelled_by(l.id); diff --git a/crates/ui-egui/tests/prepare.rs b/crates/ui-egui/tests/prepare.rs index b23f1a9f8f..eb25e42a03 100644 --- a/crates/ui-egui/tests/prepare.rs +++ b/crates/ui-egui/tests/prepare.rs @@ -250,6 +250,69 @@ fn options_tab_sets_the_check_box_style() { assert_eq!(s.session.get(s.views[0].id).unwrap().field_check_style("agree"), Some(CheckStyle::Cross)); } +#[test] +fn dropdown_alignment_is_editable_and_preserved() { + choice_alignment(pdfcraft_engine::NewField::Combo { options: vec!["One".into(), "Two".into()], editable: false }, "Center", 1); +} + +#[test] +fn list_box_alignment_is_editable_and_preserved() { + choice_alignment(pdfcraft_engine::NewField::List { options: vec!["One".into(), "Two".into()], multi: true }, "Left", 0); +} + +fn choice_alignment(kind: pdfcraft_engine::NewField, label: &str, quadding: i64) { + use pdfcraft_engine::{Edit, FieldProps, FieldValue}; + let mut h = harness(); + h.state_mut().apply_edit(Edit::AddField { page: 0, rect: [40.0, 260.0, 240.0, 310.0], kind, name: Some("aligned_choice".into()) }); + h.state_mut() + .apply_edit(Edit::SetFieldProps { name: "aligned_choice".into(), props: Box::new(FieldProps { quadding: Some(2), ..Default::default() }) }); + h.state_mut().apply_edit(Edit::SetFieldValue { name: "aligned_choice".into(), value: FieldValue::Choice(vec!["Two".into()]) }); + let field = |h: &Harness<'static, PdfCraftApp>| { + let app = h.state(); + app.session.get(app.views[0].id).unwrap().form.iter().find(|f| f.name == "aligned_choice").unwrap().clone() + }; + let original = field(&h); + h.state_mut().open_field_props("aligned_choice", 0); + h.run_steps(2); + h.get_by_label("Options").click(); + h.run_steps(2); + h.get_by_label("Alignment:"); + // The imported right alignment is displayed and unchanged until a choice is made. + h.get_by_value("Right").click(); + h.run_steps(2); + h.get_by_label(label).click(); + h.run_steps(2); + h.get_by_label("Cancel").click(); + h.run_steps(2); + assert_eq!(field(&h).quadding, 2, "cancel keeps the existing /Q"); + h.state_mut().open_field_props("aligned_choice", 0); + h.run_steps(2); + h.get_by_label("Options").click(); + h.run_steps(2); + h.get_by_value("Right").click(); + h.run_steps(2); + h.get_by_label(label).click(); + h.run_steps(2); + h.get_by_label("OK").click(); + h.run_steps(3); + let changed = field(&h); + assert_eq!(changed.quadding, quadding); + assert_eq!((changed.options, changed.value, changed.flags), (original.options, original.value, original.flags)); + let app = h.state(); + let id = app.views[0].id; + assert_eq!(app.session.get(id).unwrap().can_undo(), Some("Change field properties")); + let bytes = app.session.save_bytes(id).unwrap(); + let mut reopened = pdfcraft_engine::Session::new(); + let reopened_id = reopened.open("saved.pdf", None, bytes, None).unwrap(); + assert_eq!(reopened.get(reopened_id).unwrap().form.iter().find(|f| f.name == "aligned_choice").unwrap().quadding, quadding); + h.state_mut().execute("edit.undo"); + h.run_steps(3); + assert_eq!(field(&h).quadding, 2); + h.state_mut().execute("edit.redo"); + h.run_steps(3); + assert_eq!(field(&h).quadding, quadding); +} + #[test] fn options_tab_sets_flags_alignment_and_defaults() { use pdfcraft_engine::field_flags as ff; diff --git a/docs/roadmap.md b/docs/roadmap.md index 0ea0212daa..70935194af 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -199,6 +199,7 @@ M2 (renderer), M7 (content editing), M10 (OCR and Office), M11 (Preflight and st | 2026-10-12 | minor | M7/#1200 candidate: bounded local GoToR opening, root-confined automation, and action-preserving appearance edits; named destinations and window behavior remain partial; estimates unchanged. | | 2026-10-12 | minor | Exclusive image crops resize with their pixels across nested states, intervening operators and content streams; shared clips remain fixed. Synthetic engine/tool regressions and private image save/reopen checks; estimates unchanged. | | 2026-10-11 | minor | M4: editable Organize page-range selection (#1092), synchronized with thumbnails and available through UI control. Estimates unchanged. | +| 2026-10-11 | minor | M6 #1208: expose dropdown and list-box alignment; preserve existing values and cover cancel, undo/redo, save/reopen. Estimates unchanged. | | 2026-10-11 | minor | M10: Word invoice export regressions (#773); estimates unchanged. | | 2026-10-12 | minor | M2: short same-line redraws split before placement; estimates unchanged. | | 2026-10-12 | minor | M5: Ctrl+Enter posts highlight comment text and replies; Shift+Enter adds newlines. Estimates unchanged. | diff --git a/parity/acrobat-features.toml b/parity/acrobat-features.toml index 7edc529a00..fdf045e01a 100644 --- a/parity/acrobat-features.toml +++ b/parity/acrobat-features.toml @@ -4483,8 +4483,8 @@ milestone = "M6" status = "partial" commands = ["form.fields"] tools = ["form_fill", "form_set_props"] -evidence = ["crates/forms/src/tests.rs::text_fields_get_new_appearances", "crates/forms/src/tests.rs::auto_text_size_fits_single_and_multiline_text", "crates/forms/src/tests.rs::choices_accept_exports_or_display_text", "crates/forms/src/tests.rs::rotated_fields_draw_in_their_quadrant", "crates/engine/tests/form_rotation.rs::a_rotated_field_draws_along_the_vertical_edge", "crates/automation/tests/automation.rs::rotating_a_field_through_tools", "crates/ui-egui/tests/prepare.rs::position_tab_rotates_a_field"] -notes = "auto-size, comb, multiline, password, quadding, /MK colours and /MK /R rotation (0, 90, 180, 270); approximate Helvetica metrics" +evidence = ["crates/forms/src/tests.rs::text_fields_get_new_appearances", "crates/forms/src/tests.rs::auto_text_size_fits_single_and_multiline_text", "crates/forms/src/tests.rs::choices_accept_exports_or_display_text", "crates/forms/src/tests.rs::rotated_fields_draw_in_their_quadrant", "crates/engine/tests/form_rotation.rs::a_rotated_field_draws_along_the_vertical_edge", "crates/automation/tests/automation.rs::rotating_a_field_through_tools", "crates/ui-egui/tests/prepare.rs::position_tab_rotates_a_field", "crates/ui-egui/tests/prepare.rs::dropdown_alignment_is_editable_and_preserved", "crates/ui-egui/tests/prepare.rs::list_box_alignment_is_editable_and_preserved"] +notes = "auto-size, comb, multiline, password, quadding (text, dropdown and list-box Options controls), /MK colours and /MK /R rotation (0, 90, 180, 270); approximate Helvetica metrics" [[feature]] id = "form.rich-text-fields"