mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-26 11:08:48 -06:00
feat(forms): nudge the selected field with the arrow keys in Prepare a form (#2608)
This commit is contained in:
@@ -156,6 +156,7 @@ Ranked; detail and the full milestone table in [docs/roadmap.md](docs/roadmap.md
|
||||
| 8 | 1.0 polish: performance, right-to-left, missing languages, keyboard-only (M14) | 55–95 |
|
||||
|
||||
## Progress log
|
||||
- **2026-10-15 (community, M6, #2608):** In Prepare a form the selected field moves with the arrow keys: a point per press, ten with Shift, as the object nudge and dragging it do. Each press is one undo step ("Change field properties"), and the keys no longer turn the page while a field is selected. Estimates unchanged.
|
||||
- **2026-10-13 (community, M3, #1569 review):** Pixel-only Wayland wheel, wheel-tilt and continuous input use standalone moved events, so Ctrl zoom does not remain latched after the modifier is released (#991); vendored winit patch (13), with a policy test. Estimates unchanged.
|
||||
- **2026-10-12 (M6):** XFDF field import and data-file extraction join all text nodes in a value, preserving text after XML comments and processing instructions. Synthetic regressions cover Unicode, CDATA, empty values and PDF save/reopen. Estimates unchanged.
|
||||
- **2026-10-12 (M7, picture right-click menu):** The Edit text & images menu of a picture gains Cut Image, Redact Image (a redaction mark over the picture, applied from Redact a PDF), Create Link… and Recognize Text… (for that page), in all 21 catalogs. The menu was drawn only while the pointer was over the picture, so its items outside the picture (most of a logo's menu, and Delete below a page) could not be clicked; an open menu now stays drawn. Still missing from Acrobat's menu: Arrange (bring to front, send backward), Remove crop, Draw freehand and Edit image using… (opening an external editor).
|
||||
|
||||
@@ -2813,6 +2813,13 @@ impl PdfCraftApp {
|
||||
{
|
||||
object_ui::nudge_keys(ctx, &mut self.views[i], doc);
|
||||
}
|
||||
// A selected field moves with the arrows too (Prepare a form, #2608), before the view
|
||||
// turns the page with them.
|
||||
if self.is_preparing()
|
||||
&& let Some(doc) = self.session.get(self.views[i].id).filter(|d| d.allows_modification())
|
||||
{
|
||||
prepare::nudge_keys(ctx, &mut self.views[i], doc);
|
||||
}
|
||||
canvas::shortcuts(&mut self.views[i], ctx, self.scroll_speed);
|
||||
self.copy_key(i, ctx);
|
||||
self.image_pick_keys(i, ctx);
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
use egui::{Color32, CornerRadius, Pos2, Rect, Stroke, vec2};
|
||||
use pdfcraft_engine::form_scripts::{CalcOp, Calculate, DATE_PRESETS, Format, TIME_PRESETS, Validate, format_value};
|
||||
use pdfcraft_engine::{BorderStyle, Edit, FieldFont, FieldLook, FieldProps, FormField, FormFieldKind, NewField};
|
||||
use pdfcraft_engine::{BorderStyle, Document, Edit, FieldFont, FieldLook, FieldProps, FormField, FormFieldKind, NewField};
|
||||
use pdfcraft_render::DocInfo;
|
||||
|
||||
use crate::canvas::{DocView, PageXform};
|
||||
@@ -565,6 +565,52 @@ pub(crate) fn delete_selected(view: &mut DocView) -> Option<Edit> {
|
||||
Some(Edit::Batch { label, edits: names.into_iter().map(|name| Edit::DeleteField { name }).collect() })
|
||||
}
|
||||
|
||||
/// The arrow keys move the selected field a point, Shift and an arrow ten points, as in Acrobat,
|
||||
/// as the object nudge does (`object_ui::nudge_keys`) and as dragging it does (#2608). The keys
|
||||
/// are consumed before the view turns the page with them; one press is one undo step.
|
||||
pub(crate) fn nudge_keys(ctx: &egui::Context, view: &mut DocView, doc: &Document) {
|
||||
if ctx.egui_wants_keyboard_input() || view.prepare.grab.is_some() {
|
||||
return;
|
||||
}
|
||||
let Some((name, wi)) = view.prepare.selected.clone() else { return };
|
||||
let Some(w) = doc.form.iter().find(|f| f.name == name).and_then(|f| f.widgets.get(wi)) else { return };
|
||||
let Some(page) = w.page else { return };
|
||||
let Some(xf) = view.page_xform(page) else { return };
|
||||
use egui::{Key, Modifiers};
|
||||
let mut step = egui::Vec2::ZERO;
|
||||
ctx.input_mut(|i| {
|
||||
for (key, direction) in [
|
||||
(Key::ArrowLeft, egui::vec2(-1.0, 0.0)),
|
||||
(Key::ArrowRight, egui::vec2(1.0, 0.0)),
|
||||
(Key::ArrowUp, egui::vec2(0.0, -1.0)),
|
||||
(Key::ArrowDown, egui::vec2(0.0, 1.0)),
|
||||
] {
|
||||
// Shift first: a plain key pattern also matches a Shift press.
|
||||
if i.consume_key(Modifiers::SHIFT, key) {
|
||||
step += direction * 10.0;
|
||||
} else if i.consume_key(Modifiers::NONE, key) {
|
||||
step += direction;
|
||||
}
|
||||
}
|
||||
});
|
||||
if step == egui::Vec2::ZERO {
|
||||
return;
|
||||
}
|
||||
// A point in view space whatever the zoom and whatever way the view is turned; the page's own
|
||||
// rotation turns it into the user-space move the widget's rect is stored in.
|
||||
let c = xf.rect.center();
|
||||
let unit = |d: egui::Vec2| {
|
||||
let (a, b) = (xf.screen_to_view(c), xf.screen_to_view(c + d));
|
||||
let v = egui::vec2(b.0 - a.0, b.1 - a.1);
|
||||
if v.length() > 0.0 { v / v.length() } else { egui::Vec2::ZERO }
|
||||
};
|
||||
let delta = unit(egui::vec2(1.0, 0.0)) * step.x + unit(egui::vec2(0.0, 1.0)) * step.y;
|
||||
let Ok(offset) = doc.object_move_offset(page, [f64::from(delta.x), f64::from(delta.y)]) else { return };
|
||||
let r = w.rect;
|
||||
let rect = [r[0] + offset[0], r[1] + offset[1], r[2] + offset[0], r[3] + offset[1]];
|
||||
view.pending_edit = Some(Edit::SetFieldProps { name, props: Box::new(FieldProps { rect: Some((wi, rect)), ..Default::default() }) });
|
||||
}
|
||||
|
||||
/// Delete removes the selected fields; Escape clears the selection.
|
||||
pub(crate) fn select_all(view: &mut DocView, form: &[FormField]) {
|
||||
let mut selected =
|
||||
|
||||
@@ -116,6 +116,46 @@ fn placing_moving_editing_and_deleting_a_field() {
|
||||
assert_eq!((r[2] - r[0], r[3] - r[1]), (14.0, 14.0));
|
||||
}
|
||||
|
||||
/// The arrow keys nudge the selected field a point, Shift and an arrow ten points (#2608), as they
|
||||
/// do for a selected object or image and as dragging it does. One press is one undo step, the page
|
||||
/// does not turn with them, and the field stays selected for the next press.
|
||||
#[test]
|
||||
fn arrow_keys_nudge_the_selected_field_instead_of_turning_the_page() {
|
||||
let mut h = harness();
|
||||
h.state_mut().execute("form.add.text");
|
||||
h.run_steps(1);
|
||||
// A click places a default-size text field and selects it.
|
||||
let p = at(&h, 40.0, 250.0);
|
||||
h.hover_at(p);
|
||||
h.run_steps(1);
|
||||
h.drag_at(p);
|
||||
h.run_steps(1);
|
||||
h.drop_at(p);
|
||||
h.run_steps(4);
|
||||
let id = h.state().views[0].id;
|
||||
let name = h.state().views[0].prepare.selected.clone().expect("the placed field is selected").0;
|
||||
let near = |a: f64, b: f64| (a - b).abs() < 0.01;
|
||||
let before = rect_of(&h, &name);
|
||||
h.key_press(egui::Key::ArrowRight);
|
||||
h.run_steps(2);
|
||||
let right = rect_of(&h, &name);
|
||||
assert!(near(right[0], before[0] + 1.0) && near(right[2], before[2] + 1.0), "{before:?} → {right:?}");
|
||||
assert!(near(right[2] - right[0], before[2] - before[0]), "moved, not resized");
|
||||
assert_eq!(h.state().session.get(id).unwrap().can_undo(), Some("Change field properties"));
|
||||
h.key_press_modifiers(egui::Modifiers::SHIFT, egui::Key::ArrowUp);
|
||||
h.run_steps(2);
|
||||
let up = rect_of(&h, &name);
|
||||
assert!(near(up[1], right[1] + 10.0) && near(up[3], right[3] + 10.0), "{right:?} → {up:?}");
|
||||
h.key_press(egui::Key::ArrowDown);
|
||||
h.run_steps(2);
|
||||
h.key_press(egui::Key::ArrowLeft);
|
||||
h.run_steps(2);
|
||||
let back = rect_of(&h, &name);
|
||||
assert!(near(back[0], up[0] - 1.0) && near(back[1], up[1] - 1.0), "{up:?} → {back:?}");
|
||||
assert!(h.state().views[0].prepare.selected.is_some(), "it stays selected for the next nudge");
|
||||
assert_eq!(h.state().views[0].current, 0, "the arrows moved the field, not the page");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_validate_and_calculate_tabs() {
|
||||
use pdfcraft_engine::form_scripts::{Calculate, Format, Validate};
|
||||
|
||||
+3
-2
@@ -1,6 +1,6 @@
|
||||
# UI and interaction parity
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (column select #740, multi-object move #844, Shift-⌘-G #810 fixed) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-15 · **Change:** minor (the selected field nudges with the arrow keys in Prepare a form, #2608) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS)
|
||||
|
||||
How PdfCraft's tools, panels, shortcuts and feel compare with Acrobat Pro's current interface (the
|
||||
2023+ "new Acrobat": All tools pane on the left, quick-action toolbar, Home view). Behaviour is
|
||||
@@ -52,7 +52,7 @@ No dump of Acrobat's menu tree has been diffed against the registry yet (gap 24)
|
||||
| Rulers, grids, snap to grid, guides | yes | no | planned |
|
||||
| Cursor coordinates, line weights | yes | no | planned |
|
||||
| Measuring snap to paths, endpoints, midpoints, intersections | yes | yes | shipped |
|
||||
| Nudge selected objects with arrow keys (Shift for larger steps) | yes | yes in Edit text: a point, Shift ten points, for selected objects, images and an open paragraph's box (#1520) | shipped |
|
||||
| Nudge selected objects with arrow keys (Shift for larger steps) | yes | yes in Edit text: a point, Shift ten points, for selected objects, images and an open paragraph's box (#1520); and in Prepare a form, for the selected field (#2608) | shipped |
|
||||
| Single-key tool accelerators (H, V, Z, …) | yes, opt-in | no | planned |
|
||||
| Context menus on pages, thumbnails, comments, fields | yes | thumbnails, comments, fields | partial |
|
||||
| Comment pop-ups as movable windows, connector lines | yes | inline in the panel | planned |
|
||||
@@ -81,5 +81,6 @@ No dump of Acrobat's menu tree has been diffed against the registry yet (gap 24)
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-15 | minor | A selected field nudges with the arrow keys in Prepare a form: a point, Shift ten points, one undo step per press (#2608) |
|
||||
| 2026-10-11 | minor | Column select shipped (#740, PR #765); several objects move together on one page (#844, PR #907); Shift-⌘-G goes back (#810, PR #835) |
|
||||
| 2026-10-10 | major | Created from the command registry, automation tools, the View area of the checklist and open UI issues |
|
||||
|
||||
@@ -4740,8 +4740,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/ui-egui/src/prepare.rs::corner_drags_stretch_freely_and_keep_proportions_with_shift", "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", "crates/ui-egui/tests/ui_group/bulk_fields.rs::shared_properties_preserve_mixed_values_and_undo_the_whole_change", "crates/ui-egui/tests/ui_group/bulk_fields.rs::select_all_in_prepare_a_form_selects_only_the_current_pages_fields", "crates/ui-egui/tests/ui_group/bulk_fields.rs::dragging_an_empty_page_area_selects_fields_without_moving_them"]
|
||||
notes = "fields shown with blue fill and name tags; select, move, resize by corners (Shift keeps the field's proportions, #2199), Delete (every selected field, one undo step); field tools return to Select after one field. Shared General, Appearance and applicable Options edits are atomic and opt-in; Ctrl/Command+A selects the current page, rectangle and panel selection are supported. Names, positions, choices/defaults and script tabs remain individual GUI edits"
|
||||
evidence = ["crates/ui-egui/tests/prepare.rs::placing_moving_editing_and_deleting_a_field", "crates/ui-egui/tests/prepare.rs::arrow_keys_nudge_the_selected_field_instead_of_turning_the_page", "crates/ui-egui/src/prepare.rs::corner_drags_stretch_freely_and_keep_proportions_with_shift", "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", "crates/ui-egui/tests/ui_group/bulk_fields.rs::shared_properties_preserve_mixed_values_and_undo_the_whole_change", "crates/ui-egui/tests/ui_group/bulk_fields.rs::select_all_in_prepare_a_form_selects_only_the_current_pages_fields", "crates/ui-egui/tests/ui_group/bulk_fields.rs::dragging_an_empty_page_area_selects_fields_without_moving_them"]
|
||||
notes = "fields shown with blue fill and name tags; select, move (drag, or the arrow keys: a point, Shift ten points, #2608), resize by corners (Shift keeps the field's proportions, #2199), Delete (every selected field, one undo step); field tools return to Select after one field. Shared General, Appearance and applicable Options edits are atomic and opt-in; Ctrl/Command+A selects the current page, rectangle and panel selection are supported. Names, positions, choices/defaults and script tabs remain individual GUI edits"
|
||||
|
||||
[[feature]]
|
||||
id = "form.add-text-field"
|
||||
|
||||
Reference in New Issue
Block a user