mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-24 10:08:00 -06:00
M5: the arrow keys move the selected comment
With a comment selected, the arrow keys scrolled the view (↑ ↓) or turned the page (← →), so a text box, rectangle or line could only be placed with the mouse. Now an arrow key moves the selected comment a screen pixel, and ⌘/Ctrl or Shift and an arrow ten, as Edit text nudges objects; up is up on screen whatever the zoom, the page's rotation and the view's. Each press is one undo step (Edit::MoveAnnotation). The keys are taken before the view's navigation, which has them back once nothing is selected. Text markup stays with its text, and a locked comment where it is. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
d5047aa4ac
commit
9a76f90f07
@@ -225,6 +225,8 @@ Ranked; detail and the full milestone table in [docs/roadmap.md](docs/roadmap.md
|
||||
- **2026-10-20 (M3, #3541/#3620):** Comment hover follows the individual marked lines instead of the enclosing multiline rectangle, and overlapping comments follow paint order. Synthetic UI regressions cover all four text-markup kinds, page/view rotations, neighboring highlights, legacy rectangle fallback and unchanged PDF bytes. Estimates unchanged.
|
||||
- **2026-10-20 (M10, #3532 partial):** Vector printing retains existing widget artwork when its appearance lacks Subtype. A print-owned copy adds Form dispatch metadata. The source stream and all explicit subtypes stay unchanged. Synthetic source/output raster checks cover resources, selected/shared appearances, placement, flags and content modes. Appearance regeneration and physical printer validation remain outside this fix. Estimates unchanged.
|
||||
|
||||
- **2026-10-20 (M5, arrow keys move comments):** With a comment selected, the arrow keys move it a screen pixel and ⌘/Ctrl or Shift and an arrow ten, instead of scrolling the view or turning the page; up is up on screen at any zoom and rotation, and each press is one undo step. Text markup and locked comments stay put. Estimates unchanged.
|
||||
|
||||
- **2026-10-19 (M14, #3373):** The main menu and its long submenus scroll in short windows, keeping their commands reachable. Estimates unchanged.
|
||||
|
||||
- **2026-10-19 (M3, #3387):** Save-prompt shortcuts yield to later consent and utility dialogs; a synthetic UI regression checks that an edited document stays open. Estimates unchanged.
|
||||
|
||||
@@ -1823,6 +1823,55 @@ pub(crate) fn keys(ctx: &egui::Context, view: &mut DocView, tool: &mut QuickTool
|
||||
}
|
||||
}
|
||||
|
||||
/// The arrow keys move the selected comment a screen pixel; with ⌘/Ctrl or Shift, ten. Up is up
|
||||
/// on screen whatever the zoom, the page's rotation and the view's; the keys are taken before
|
||||
/// the view scrolls or turns the page with them. Each press is one undo step. Text markup stays
|
||||
/// with its text, and a locked comment where it is.
|
||||
pub(crate) fn nudge_keys(ctx: &egui::Context, view: &mut DocView, info: &DocInfo) {
|
||||
if ctx.egui_wants_keyboard_input() || view.pending_edit.is_some() {
|
||||
return;
|
||||
}
|
||||
let cv = &view.comments;
|
||||
if cv.gesture.is_some() || cv.composer.is_some() {
|
||||
return;
|
||||
}
|
||||
let Some((page, index)) = cv.selected else { return };
|
||||
let movable =
|
||||
info.annotations.iter().any(|a| a.page == page && a.index == index && a.in_reply_to.is_none() && !is_markup(&a.subtype) && !a.locked);
|
||||
let (Some(xf), Some(p)) = (view.page_xform(page), info.pages.get(page)) else { return };
|
||||
if !movable {
|
||||
return;
|
||||
}
|
||||
use egui::{Key, Modifiers};
|
||||
let mut step = egui::Vec2::ZERO;
|
||||
ctx.input_mut(|i| {
|
||||
for (key, direction) in
|
||||
[(Key::ArrowLeft, vec2(-1.0, 0.0)), (Key::ArrowRight, vec2(1.0, 0.0)), (Key::ArrowUp, vec2(0.0, -1.0)), (Key::ArrowDown, vec2(0.0, 1.0))]
|
||||
{
|
||||
// The modified patterns first: a plain one also matches a Shift press.
|
||||
if i.consume_key(Modifiers::COMMAND, key) || 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;
|
||||
}
|
||||
// The step on screen, in user space: from the page's centre to `step` pixels away.
|
||||
let user = |s: Pos2| {
|
||||
let (vx, vy) = xf.screen_to_view(s);
|
||||
let u = p.view_to_user(vx, vy);
|
||||
[f64::from(u[0]), f64::from(u[1])]
|
||||
};
|
||||
let (a, b) = (user(xf.rect.center()), user(xf.rect.center() + step));
|
||||
let (dx, dy) = (b[0] - a[0], b[1] - a[1]);
|
||||
if dx.is_finite() && dy.is_finite() && (dx != 0.0 || dy != 0.0) {
|
||||
view.pending_edit = Some(Edit::MoveAnnotation { page, index, dx, dy });
|
||||
}
|
||||
}
|
||||
|
||||
/// Save attachment… and, for a PDF, Open in a new tab, first in a file attachment comment's
|
||||
/// menus as in the Attachments panel: the comment is where the reader finds its file. Reading
|
||||
/// the file changes nothing, so a read-only document offers them too.
|
||||
|
||||
@@ -3023,6 +3023,8 @@ impl PdfCraftApp {
|
||||
&& let Some(doc) = self.session.get(self.views[i].id).filter(|d| d.allows_modification())
|
||||
{
|
||||
object_ui::nudge_keys(ctx, &mut self.views[i], doc);
|
||||
} else if let Some(doc) = self.session.get(self.views[i].id).filter(|d| d.allows_annotation()) {
|
||||
comments::nudge_keys(ctx, &mut self.views[i], &doc.info);
|
||||
}
|
||||
// A selected field moves with the arrows too (Prepare a form, #2608), before the view
|
||||
// turns the page with them.
|
||||
|
||||
@@ -860,6 +860,50 @@ fn line_ends_are_dragged_to_change_length_and_angle() {
|
||||
assert!((p[1][0] - 200.0).abs() < 1.0 && (p[1][1] - 160.0).abs() < 1.0, "{p:?}");
|
||||
}
|
||||
|
||||
/// The arrow keys move the selected comment a screen pixel, ⌘/Ctrl or Shift and an arrow ten,
|
||||
/// instead of scrolling or turning the page; each press is one undo step. Without a selection
|
||||
/// the arrows scroll and turn pages as before.
|
||||
#[test]
|
||||
fn arrow_keys_nudge_the_selected_comment() {
|
||||
use egui::{Key, Modifiers};
|
||||
let mut h = harness(|app| app.set_option("quick", "square").unwrap());
|
||||
drag_pt(&mut h, (40.0, 100.0), (140.0, 40.0));
|
||||
h.state_mut().set_option("quick", "select").unwrap();
|
||||
click_pt(&mut h, (90.0, 70.0));
|
||||
assert_eq!(h.state().views[0].comments.selected, Some((0, 0)));
|
||||
// One screen pixel in user space, at this zoom.
|
||||
let pixel = 300.0 / h.state().views[0].page_screen_rect(0).unwrap().width();
|
||||
let before = comments(&h)[0].rect;
|
||||
h.key_press(Key::ArrowRight);
|
||||
h.run_steps(3);
|
||||
let r = comments(&h)[0].rect;
|
||||
assert!((r[0] - before[0] - pixel).abs() < 0.01 && (r[1] - before[1]).abs() < 0.01, "one pixel right: {r:?} from {before:?}");
|
||||
assert_eq!(h.state().views[0].current, 0, "the page didn't turn");
|
||||
let id = h.state().views[0].id;
|
||||
assert_eq!(h.state().session.get(id).unwrap().can_undo(), Some("Move comment"));
|
||||
// ⌘/Ctrl and Shift: ten pixels (down on screen is down in user space here).
|
||||
h.key_press_modifiers(Modifiers::COMMAND, Key::ArrowDown);
|
||||
h.run_steps(3);
|
||||
let r2 = comments(&h)[0].rect;
|
||||
assert!((r[1] - r2[1] - 10.0 * pixel).abs() < 0.01 && (r2[0] - r[0]).abs() < 0.01, "ten pixels down: {r2:?} from {r:?}");
|
||||
h.key_press_modifiers(Modifiers::SHIFT, Key::ArrowLeft);
|
||||
h.run_steps(3);
|
||||
let r3 = comments(&h)[0].rect;
|
||||
assert!((r2[0] - r3[0] - 10.0 * pixel).abs() < 0.01, "ten pixels left: {r3:?} from {r2:?}");
|
||||
h.state_mut().undo();
|
||||
h.run_steps(2);
|
||||
assert!((comments(&h)[0].rect[0] - r2[0]).abs() < 0.01, "each press is one undo step");
|
||||
// Nothing selected: → turns the page again.
|
||||
h.key_press(Key::Escape);
|
||||
h.run_steps(2);
|
||||
assert_eq!(h.state().views[0].comments.selected, None);
|
||||
let rect = comments(&h)[0].rect;
|
||||
h.key_press(Key::ArrowRight);
|
||||
h.run_steps(3);
|
||||
assert_eq!(comments(&h)[0].rect, rect, "the comment stays");
|
||||
assert_eq!(h.state().views[0].current, 1, "and the view goes to the next page");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_shape_just_drawn_resizes_from_its_handles_without_the_select_tool() {
|
||||
// #2338: the new shape showed its handles, but only the Select tool answered them.
|
||||
|
||||
+1
-1
@@ -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); and in Prepare a form, for the selected field (#2608) | 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); a selected comment: a screen pixel, ⌘/Ctrl or Shift ten | 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 |
|
||||
|
||||
@@ -3971,6 +3971,18 @@ tools = ["comment_delete"]
|
||||
evidence = ["crates/ui-egui/tests/it/comments.rs::comments_are_selected_moved_and_deleted_with_the_select_tool", "crates/annot/src/tests.rs::delete_takes_popup_and_reply_chain"]
|
||||
notes = "deletes the pop-up and the reply thread"
|
||||
|
||||
[[feature]]
|
||||
id = "comment.keyboard-nudge"
|
||||
title = "Move the selected comment with the arrow keys"
|
||||
area = "E"
|
||||
tier = "P2"
|
||||
milestone = "M5"
|
||||
status = "shipped"
|
||||
commands = []
|
||||
tools = ["comment_edit"]
|
||||
evidence = ["crates/ui-egui/tests/it/comments.rs::arrow_keys_nudge_the_selected_comment"]
|
||||
notes = "With a comment selected (Select tool, or a Fill & Sign mark picked up), an arrow key moves it a screen pixel and ⌘/Ctrl or Shift and an arrow ten, up being up on screen whatever the zoom and rotation; each press is one undo step, and the arrows scroll and turn pages again once nothing is selected. Text markup stays with its text and a locked comment where it is. Headless, comment_edit's `move` takes the offset in points."
|
||||
|
||||
[[feature]]
|
||||
id = "comment.reshape"
|
||||
title = "Change a line's ends and a polygon's corners after drawing"
|
||||
|
||||
Reference in New Issue
Block a user