M5: Escape leaves a Fill & Sign tool, as it leaves a comment tool (#4539)

* Escape leaves a Fill & Sign tool, as it leaves a comment tool

The first Escape closes the open type box, the next one goes back to
Select; Fill & Sign's Text tool otherwise opened a box at every click with
no way out by the keyboard.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Fill & Sign: a new text box deselects, so two presses of Escape close it and leave the tool

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ohagmannandClaude Opus 5.5 authored and GitHub committed 2026-11-05 13:13:56 +01:00
1 parent 863af864a7
commit 8d569066b8
5 files changed
+64 -6

No files matched your search

+1
View File
@@ -299,6 +299,7 @@ Ranked; detail and the full milestone table in [docs/roadmap.md](docs/roadmap.md
Estimates unchanged.
- **2026-10-21 (M3, window resize and move on Wayland):** With PdfCraft's own title bar on GNOME/Wayland, a second resize by a window edge, or a second move by the empty tab strip, works straight after the first; it waited for a click inside the window, because the compositor keeps the button's release from egui. A unit test covers the release given back; checked by hand in a test build. Estimates unchanged.
- **2026-10-24 (M5, move preview):** A comment being moved is drawn where it goes while it is dragged (a rectangle or oval showed only its blue frame): the comment is rendered alone once as the drag starts and laid over the page, which shows it where it was until the move is applied. Engine `Document::comment_layer`; synthetic engine and UI regressions. Estimates unchanged.
- **2026-10-24 (M5, Escape in Fill & Sign):** Escape leaves a Fill & Sign tool as it leaves a comment tool: the first press closes the open type box, the next goes back to Select, so the Text tool no longer opens a box at every click with no way out by the keyboard. A synthetic UI regression covers the Text and Checkmark tools. Estimates unchanged.
- **2026-10-22 (M4, #3916 partial):** Document edits and Undo/Redo cancel an Organize page drag. Pointer-release regressions check exact page order and save/reopen.
- **2026-10-22 (M10, #3752 paper):** CUPS jobs ask for the chosen paper in every size. The 13 sizes the Print dialog sent no paper for (A6, A0–A2, ARCH, ANSI C–E) use PWG 5101.1 names, checked against a synthetic PPD with `cupsfilter`, and `doc_print` to a printer sends the paper too. The macOS tray half (Auto Select instead of the PPD's default tray) is a craft-print change that needs a pin bump here. Estimates unchanged.
+4 -2
View File
@@ -606,9 +606,11 @@ pub(crate) fn keys(ctx: &egui::Context, view: &mut DocView, tool: &mut QuickTool
}
let cv = &mut view.comments;
if ctx.input(|i| i.key_pressed(egui::Key::Escape)) {
// One step back per press: cancel the gesture, then deselect, then drop the tool.
// One step back per press: cancel the gesture, then deselect, then drop the tool (a
// comment tool, or a Fill & Sign one: its Text tool would otherwise open a box at every
// click with no way out by the keyboard).
let cancelled = cv.gesture.take().is_some() || cv.selected.take().is_some();
if !cancelled && matches!(tool, QuickTool::Comment(_)) {
if !cancelled && matches!(tool, QuickTool::Comment(_) | QuickTool::Fill(_)) {
*tool = QuickTool::Select;
}
}
+6 -2
View File
@@ -491,7 +491,10 @@ pub(crate) fn page_input(cx: PageToolContext<'_>, input: PageInput<'_>, free: bo
match tool {
FillTool::Text => {
// Clicking away from an open box commits it: the page sees this click before the box
// sees its lost focus, so replacing the box first would drop what was typed.
// sees its lost focus, so replacing the box first would drop what was typed. A new box
// deselects, as a comment tool's note or text box does: Escape then closes the box and
// leaves the tool in two presses.
view.comments.selected = None;
let open = view.fill_text.replace(TypeBox { page, at: top, text: String::new(), size: text_size, color: text_color, focus: true });
open.filter(|tb| !tb.text.trim().is_empty()).map(|tb| {
FillAction::Edit(Box::new(typed_with_color(
@@ -587,7 +590,8 @@ pub(crate) fn type_box(ctx: &egui::Context, view: &mut DocView, info: &DocInfo,
r.request_focus();
t.focus = false;
}
if ui.input(|i| i.key_pressed(egui::Key::Escape)) {
// Taken here: the same press must not also leave the tool (one step back per press).
if ui.input_mut(|i| i.consume_key(egui::Modifiers::NONE, egui::Key::Escape)) {
cancel = true;
} else if r.lost_focus() {
commit = true;
+51
View File
@@ -159,6 +159,57 @@ fn clicking_elsewhere_with_add_text_keeps_the_typed_text() {
assert!(h.state().views[0].fill_text.as_ref().is_some_and(|t| t.text.is_empty()), "a new, empty box is open");
}
/// Escape steps back out of a Fill & Sign tool as out of a comment tool: the first press closes
/// the open type box, the next one leaves the Text tool for Select, so a click opens no new box.
#[test]
fn escape_leaves_the_fill_and_sign_tools() {
let mut h = harness();
h.state_mut().execute("sign.fill.text");
click(&mut h, 50.0, 250.0);
assert!(h.state().views[0].fill_text.is_some(), "a box is open");
h.key_press(egui::Key::Escape);
h.run_steps(2);
assert!(h.state().views[0].fill_text.is_none(), "the first Escape closes the box");
assert_eq!(h.state().quick_tool, QuickTool::Fill(FillTool::Text), "the tool stays for the next box");
h.key_press(egui::Key::Escape);
h.run_steps(2);
assert_eq!(h.state().quick_tool, QuickTool::Select, "the next Escape leaves the tool");
click(&mut h, 50.0, 150.0);
assert!(h.state().views[0].fill_text.is_none(), "no new box");
assert!(items(&h).is_empty(), "nothing was added: {:?}", items(&h));
// A mark tool too.
h.state_mut().execute("sign.fill.check");
h.run_steps(2);
assert_eq!(h.state().quick_tool, QuickTool::Fill(FillTool::Check));
h.key_press(egui::Key::Escape);
h.run_steps(2);
assert_eq!(h.state().quick_tool, QuickTool::Select);
}
/// A box opened while a comment is selected deselects it: two presses of Escape still close the
/// box and leave the tool.
#[test]
fn escape_leaves_the_text_tool_in_two_presses_after_a_selection() {
let mut h = harness();
h.state_mut().execute("sign.fill.text");
click(&mut h, 50.0, 250.0);
h.event(egui::Event::Text("Ada".into()));
h.run_steps(1);
commit_typed_text(&mut h);
assert_eq!(items(&h).len(), 1, "{:?}", items(&h));
h.state_mut().views[0].fill_text = None;
h.state_mut().views[0].comments.selected = Some((0, 0));
click(&mut h, 50.0, 150.0);
assert!(h.state().views[0].fill_text.is_some(), "a box is open");
assert_eq!(h.state().views[0].comments.selected, None, "the new box deselected the comment");
h.key_press(egui::Key::Escape);
h.run_steps(2);
assert!(h.state().views[0].fill_text.is_none(), "the first Escape closes the box");
h.key_press(egui::Key::Escape);
h.run_steps(2);
assert_eq!(h.state().quick_tool, QuickTool::Select, "the second Escape leaves the tool");
}
#[test]
fn fill_text_size_controls_new_text_and_survives_settings_restore() {
let mut h = harness();
+2 -2
View File
@@ -5423,8 +5423,8 @@ milestone = "M5"
status = "shipped"
commands = ["sign.fill.text"]
tools = ["fill_sign_add"]
evidence = ["crates/annot/src/tests.rs::indirect_typewriter_names_keep_outer_whitespace_in_summaries", "crates/ui-egui/tests/it/comments.rs::typewriter_edit_composer_keeps_trailing_blank_lines", "crates/ui-egui/tests/it/comments.rs::other_edit_composers_keep_trimming_trailing_whitespace", "crates/ui-egui/tests/it/comments.rs::moved_fill_and_sign_text_is_drawn_where_it_goes_before_release", "crates/engine/src/tests.rs::fill_sign_multiline_text_survives_engine_save_reopen_and_render", "crates/ui-egui/tests/it/fill_sign.rs::fill_sign_enter_adds_lines_outside_click_commits_and_escape_cancels", "crates/automation/tests/it/automation.rs::multiline_fill_sign_engine_round_trip_preserves_lines_and_appearance", "crates/ui-egui/tests/it/fill_sign.rs::text_marks_and_date", "crates/annot/src/tests.rs::fill_and_sign_items_are_drawn", "crates/automation/tests/it/automation.rs::fill_and_sign_through_tools", "crates/engine/src/tests.rs::typed_text_reads_across_as_displayed_on_rotated_pages", "crates/ui-egui/tests/it/fill_sign.rs::fill_text_colour_survives_preview_commit_settings_and_save", "crates/automation/tests/it/automation.rs::fill_sign_text_and_date_colours_are_saved_and_validated"]
notes = "Indirect IT is resolved when recognizing typewriter contents and Fill & Sign marks; comment listing and properties retain direct Subtype as before; composer edits preserve the typewriter text as written, while other comments retain their existing trimming. Enter inserts a newline; the editor grows with its lines, outside clicks commit and Escape cancels. Nonempty text preserves leading/internal/trailing line breaks, including after save/reopen; fill_sign_add sizes all wrapped rows for the saved appearance (#5850). typewriter FreeText, 10 pt Helvetica. Reads across as displayed on pages with /Rotate (sideways-stored scans): the box follows the displayed axes and the appearance is turned back (PdfCraft key /PCTextRotate); other apps that regenerate the appearance from /DA draw it unturned. A later page rotation does not re-turn it. Field detection does not work on scanned (image-only) forms yet; typed fractional font sizes commit once; Fill & Sign text/date colour is remembered and preserved in preview and saved appearances (#2444)"
evidence = ["crates/annot/src/tests.rs::indirect_typewriter_names_keep_outer_whitespace_in_summaries", "crates/ui-egui/tests/it/comments.rs::typewriter_edit_composer_keeps_trailing_blank_lines", "crates/ui-egui/tests/it/comments.rs::other_edit_composers_keep_trimming_trailing_whitespace", "crates/ui-egui/tests/it/comments.rs::moved_fill_and_sign_text_is_drawn_where_it_goes_before_release", "crates/engine/src/tests.rs::fill_sign_multiline_text_survives_engine_save_reopen_and_render", "crates/ui-egui/tests/it/fill_sign.rs::fill_sign_enter_adds_lines_outside_click_commits_and_escape_cancels", "crates/automation/tests/it/automation.rs::multiline_fill_sign_engine_round_trip_preserves_lines_and_appearance", "crates/ui-egui/tests/it/fill_sign.rs::text_marks_and_date", "crates/annot/src/tests.rs::fill_and_sign_items_are_drawn", "crates/automation/tests/it/automation.rs::fill_and_sign_through_tools", "crates/engine/src/tests.rs::typed_text_reads_across_as_displayed_on_rotated_pages", "crates/ui-egui/tests/it/fill_sign.rs::fill_text_colour_survives_preview_commit_settings_and_save", "crates/automation/tests/it/automation.rs::fill_sign_text_and_date_colours_are_saved_and_validated", "crates/ui-egui/tests/it/fill_sign.rs::escape_leaves_the_fill_and_sign_tools"]
notes = "Indirect IT is resolved when recognizing typewriter contents and Fill & Sign marks; comment listing and properties retain direct Subtype as before; composer edits preserve the typewriter text as written, while other comments retain their existing trimming. Enter inserts a newline; the editor grows with its lines, outside clicks commit and Escape cancels. Nonempty text preserves leading/internal/trailing line breaks, including after save/reopen; fill_sign_add sizes all wrapped rows for the saved appearance (#5850). typewriter FreeText, 10 pt Helvetica. Escape closes the open box, and a second Escape leaves the Fill & Sign tool for Select, as it leaves a comment tool. Reads across as displayed on pages with /Rotate (sideways-stored scans): the box follows the displayed axes and the appearance is turned back (PdfCraft key /PCTextRotate); other apps that regenerate the appearance from /DA draw it unturned. A later page rotation does not re-turn it. Field detection does not work on scanned (image-only) forms yet; typed fractional font sizes commit once; Fill & Sign text/date colour is remembered and preserved in preview and saved appearances (#2444)"
[[feature]]
id = "form.fill-sign-marks"