mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-07 03:01:55 +00:00
fix(ui): right-clicks with Prepare, the Link tool and Add text as before
Routing right-clicks by what lies on top under the pointer changed three cases beyond the rulings: - **Prepare.** A right-click while a field was dragged selected the field under the pointer, and the release then moved that one. Prepare used to ignore right-clicks during a drag; it does again (`prepare::context` returns while a grab is held). - **The Link tool.** A note over a link took the right-click, though the Link tool's press there is the link's. The Link tool's right-click on a link is the link's again, whatever lies over it. - **Add text.** Comments took right-clicks away from paragraphs and added items. Add text used to take every right-click on a page it can change; it does again. Tests: those three, and two that pin ruling 6's right-click half with the Select tool: no link menu on a link with no action, and none on a form field over a link. Each was seen failing with its fix reverted. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
734cb4c13a
commit
b1a0657c42
4 files changed
+116
-5
No files matched your search
@@ -316,9 +316,13 @@ fn tool_input(
|
||||
/// selected; links take theirs from what this returns.
|
||||
fn right_click(hits: &PageHits<'_>, view: &mut DocView, p: Pos2) -> Option<Owner> {
|
||||
let context = hits.context_at(view, p);
|
||||
// Edit text's own paragraphs and pictures keep the right-click as they take it today, and a
|
||||
// Prepare field tool the whole page.
|
||||
let editing_here = matches!(hits.tool, QuickTool::AddText | QuickTool::EditText) && top(hits.at(view, p), hits.tool).is_some();
|
||||
// Add text takes every right-click on a page it can change, Edit text those on its own
|
||||
// paragraphs and pictures, and a Prepare field tool the whole page, as before.
|
||||
let editing_here = match hits.tool {
|
||||
QuickTool::AddText => hits.can_modify || top(hits.at(view, p), hits.tool).is_some(),
|
||||
QuickTool::EditText => top(hits.at(view, p), hits.tool).is_some(),
|
||||
_ => false,
|
||||
};
|
||||
if !editing_here && hits.field_tool.is_none() {
|
||||
let comment = match context {
|
||||
Some(Owner::Comment { index, .. }) => Some(index),
|
||||
@@ -385,8 +389,16 @@ impl PageHits<'_> {
|
||||
/// What a right-click at `p` is on: what lies on top there, as for a press, but with every
|
||||
/// tool reaching comments and links, as a comment's menu shows whatever the tool. A highlight
|
||||
/// over a link is the highlight's ([`comments::context_hit`]).
|
||||
/// The Link tool edits links, so its right-click on one is the link's whatever lies over it, as
|
||||
/// its press there is.
|
||||
fn context_at(&self, view: &DocView, p: Pos2) -> Option<Owner> {
|
||||
top(self.hits(view, p, true), QuickTool::Select).map(|h| h.owner)
|
||||
let hits = self.hits(view, p, true);
|
||||
if self.tool == QuickTool::Link
|
||||
&& let Some(link) = hits.iter().rev().find(|h| matches!(h.owner, Owner::Link { .. }))
|
||||
{
|
||||
return Some(link.owner);
|
||||
}
|
||||
top(hits, QuickTool::Select).map(|h| h.owner)
|
||||
}
|
||||
|
||||
/// The hits at `p`, bottom up: links, comments; Edit text's pictures under its paragraphs (text
|
||||
|
||||
@@ -462,11 +462,15 @@ pub(crate) fn page_input(
|
||||
|
||||
/// A right-click on widget `widget` of field `field` (the top hit there, [`hit`]): the field is
|
||||
/// selected, so the menu acts on it. Part of a selection of several, it becomes the anchor the
|
||||
/// others line up with.
|
||||
/// others line up with. Not while a field is dragged or a rectangle drawn: the release acts on
|
||||
/// the selection the drag began with.
|
||||
pub(crate) fn context(form: &[FormField], field: usize, widget: usize, view: &mut DocView) {
|
||||
let Some(f) = form.get(field) else { return };
|
||||
let key = (f.name.clone(), widget);
|
||||
let prep = &mut view.prepare;
|
||||
if prep.grab.is_some() || prep.ignore_release {
|
||||
return;
|
||||
}
|
||||
if let Some(i) = prep.also.iter().position(|k| *k == key) {
|
||||
prep.also.remove(i);
|
||||
if let Some(old) = prep.selected.replace(key) {
|
||||
|
||||
@@ -1226,3 +1226,61 @@ fn a_fields_tooltip_shows_only_where_the_field_takes_the_press() {
|
||||
rest_at(&mut h, on_field);
|
||||
assert!(h.query_all_by_label("Your name").next().is_some(), "the field's tooltip where it takes the press");
|
||||
}
|
||||
|
||||
/// With the Link tool a right-click on a link is the link's, as its press there is, though a note
|
||||
/// lies over it: the Link tool edits links and reaches no comment.
|
||||
#[test]
|
||||
fn a_right_click_with_the_link_tool_on_a_note_over_a_link_selects_the_link() {
|
||||
let mut h = open(note_over_link_pdf(), "150", false);
|
||||
link_tool(&mut h);
|
||||
let on_both = at(&h, 150.0, 325.0);
|
||||
right_click(&mut h, on_both);
|
||||
let v = &h.state().views[0];
|
||||
assert_eq!((v.links.selected, v.links.context, v.comments.selected), (Some((0, 0)), Some((0, 0)), None), "the link, not the note");
|
||||
}
|
||||
|
||||
/// With Add text a right-click leaves comments alone, as before: Add text takes the page's
|
||||
/// right-clicks.
|
||||
#[test]
|
||||
fn a_right_click_with_add_text_selects_no_comment() {
|
||||
let mut h = open(comment_on_text_pdf(), "150", false);
|
||||
h.state_mut().quick_tool = QuickTool::AddText;
|
||||
h.run_steps(4);
|
||||
// Above the line of text the comment covers, where no paragraph is.
|
||||
let on_comment = at(&h, 100.0, 317.0);
|
||||
right_click(&mut h, on_comment);
|
||||
assert_eq!(h.state().views[0].comments.selected, None, "the comment under the pointer is not selected");
|
||||
}
|
||||
|
||||
/// With the Select tool a right-click finds a link only where a click follows it: a link that
|
||||
/// does nothing has no menu. The Link tool's own test used to give it one.
|
||||
#[test]
|
||||
fn a_right_click_with_the_select_tool_on_a_link_with_no_action_opens_no_link_menu() {
|
||||
let mut h = open(dead_link_pdf(), "150", false);
|
||||
let dead = at(&h, 25.0, 325.0);
|
||||
right_click(&mut h, dead);
|
||||
assert_eq!(h.state().views[0].links.context, None);
|
||||
assert!(h.query_by_label("Remove all links").is_none(), "the page's menu has no link items");
|
||||
}
|
||||
|
||||
/// [`link_pdf`], with a check box over the middle of its link.
|
||||
fn field_over_link_pdf() -> Vec<u8> {
|
||||
pdf(&[
|
||||
"<< /Type /Catalog /Pages 2 0 R /AcroForm << /Fields [5 0 R] /DA (/ZaDb 0 Tf 0 g) /DR << /Font << /ZaDb 6 0 R >> >> >> >>",
|
||||
PAGES,
|
||||
"<< /Type /Page /Parent 2 0 R /Annots [4 0 R 5 0 R] >>",
|
||||
"<< /Type /Annot /Subtype /Link /Rect [50 300 250 350] /Border [0 0 0] /A << /S /URI /URI (https://example.org/) >> >>",
|
||||
"<< /Type /Annot /Subtype /Widget /FT /Btn /T (agree) /V /Off /AS /Off /Rect [140 315 160 335] /P 3 0 R /F 4 /MK << /CA (4) >> /DA (/ZaDb 11 Tf 0 g) >>",
|
||||
"<< /Type /Font /Subtype /Type1 /BaseFont /ZapfDingbats >>",
|
||||
])
|
||||
}
|
||||
|
||||
/// A right-click on a form field over a link is the field's, as a press there is: no link menu.
|
||||
#[test]
|
||||
fn a_right_click_on_a_field_over_a_link_opens_no_link_menu() {
|
||||
let mut h = open(field_over_link_pdf(), "150", false);
|
||||
let on_field = at(&h, 150.0, 325.0);
|
||||
right_click(&mut h, on_field);
|
||||
assert_eq!(h.state().views[0].links.context, None, "the field is on top");
|
||||
assert!(h.query_by_label("Remove all links").is_none());
|
||||
}
|
||||
@@ -1562,3 +1562,40 @@ fn button_export_properties_apply_cancel_and_undo() {
|
||||
assert_eq!(f.export_of(0), Some(original.as_str()));
|
||||
}
|
||||
}
|
||||
|
||||
/// The screen rectangle of widget 0 of field `name` on the first view.
|
||||
fn field_rect(h: &Harness<'static, PdfCraftApp>, name: &str) -> egui::Rect {
|
||||
let s = h.state();
|
||||
let doc = s.session.get(s.views[0].id).unwrap();
|
||||
let f = doc.form.iter().find(|f| f.name == name).unwrap();
|
||||
pdfcraft_ui_egui::forms_ui::field_screen_rect(&s.views[0], &doc.info, f, 0).expect("on screen")
|
||||
}
|
||||
|
||||
/// A right-click while a field is dragged leaves the dragged field selected, so the drag moves it
|
||||
/// and not the field right-clicked. The right-click used to select the other field, and the release
|
||||
/// moved that one.
|
||||
#[test]
|
||||
fn a_right_click_during_a_field_drag_moves_the_dragged_field() {
|
||||
let mut h = harness();
|
||||
h.state_mut().execute("form.prepare");
|
||||
h.run_steps(3);
|
||||
let (name, city) = (field_rect(&h, "name"), field_rect(&h, "city"));
|
||||
let a = name.center();
|
||||
h.hover_at(a);
|
||||
h.run_steps(1);
|
||||
h.drag_at(a);
|
||||
h.run_steps(1);
|
||||
h.hover_at(a + egui::vec2(0.0, 30.0));
|
||||
h.run_steps(1);
|
||||
for pressed in [true, false] {
|
||||
h.event(egui::Event::PointerButton { pos: city.center(), button: egui::PointerButton::Secondary, pressed, modifiers: Default::default() });
|
||||
}
|
||||
h.run_steps(1);
|
||||
let end = a + egui::vec2(0.0, 60.0);
|
||||
h.hover_at(end);
|
||||
h.run_steps(1);
|
||||
h.drop_at(end);
|
||||
h.run_steps(3);
|
||||
assert_eq!(field_rect(&h, "city"), city, "the field right-clicked stays where it was");
|
||||
assert_ne!(field_rect(&h, "name"), name, "the dragged field moved");
|
||||
}
|
||||
Reference in new issue
Block a user