From 7d419a6c73d2a5fd19198d6fb212a4c4e6da2a4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20Cie=C5=9Blik?= <132496025+Hathor875@users.noreply.github.com> Date: Mon, 12 Oct 2026 09:31:39 +0200 Subject: [PATCH] feat(edit): remove all links command and context menu (#539) (#1213) * feat(edit): remove all links command and context menu (#539) - Expose 'edit.remove_links' and 'edit.remove_all_links' commands to remove all links across the document or on specified pages. - Add right-click context menu options for links: Properties, Delete, and Remove all links in both Link tool and Select tool modes. - Extend automation tool 'links_remove' with optional 'pages' parameter and removal count. - Add notification feedback in UI for link deletion. - Mark 'edit.remove-all-links' as shipped in Parity with evidence tests. - Add unit and integration tests covering automation, engine, and UI undo/redo. * Remove all links: one command id, catalog rows, link menu only on a link - Drop the duplicate edit.remove_all_links command (same label as the existing edit.remove_links, so it showed twice); edit.remove_links now runs remove_all_links with its toast. - Add the toast strings (No links to remove, Removed 1 link, Removed {n} links) to all 20 catalogs. - The canvas context menu shows the link items only when the right-click hit a link (LinkView::context, set per right-click), so a link left selected no longer takes over the comments menu, and the Link tool no longer replaces the comments menu away from links. Regression test. Co-Authored-By: Claude Opus 5.5 * Restore the PR's links_remove pages test (lost with the workdir conflict) Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Brandon Thomas Co-authored-by: Claude Opus 5.5 --- ATTRIBUTION.toml | 40 +++++++------- crates/automation/src/links.rs | 11 +++- crates/automation/src/tools.rs | 4 +- crates/automation/tests/automation.rs | 7 ++- crates/engine/src/tests.rs | 6 +++ crates/ui-egui/src/canvas.rs | 32 +++++++++++ crates/ui-egui/src/commands.rs | 4 +- crates/ui-egui/src/i18n/ar.tsv | 3 ++ crates/ui-egui/src/i18n/bg.tsv | 3 ++ crates/ui-egui/src/i18n/cs.tsv | 3 ++ crates/ui-egui/src/i18n/de.tsv | 3 ++ crates/ui-egui/src/i18n/el.tsv | 3 ++ crates/ui-egui/src/i18n/es.tsv | 3 ++ crates/ui-egui/src/i18n/fr.tsv | 3 ++ crates/ui-egui/src/i18n/hu.tsv | 3 ++ crates/ui-egui/src/i18n/it.tsv | 3 ++ crates/ui-egui/src/i18n/ja.tsv | 3 ++ crates/ui-egui/src/i18n/kk.tsv | 3 ++ crates/ui-egui/src/i18n/pl.tsv | 3 ++ crates/ui-egui/src/i18n/pt-br.tsv | 3 ++ crates/ui-egui/src/i18n/ru.tsv | 3 ++ crates/ui-egui/src/i18n/sl.tsv | 3 ++ crates/ui-egui/src/i18n/te.tsv | 3 ++ crates/ui-egui/src/i18n/tr.tsv | 3 ++ crates/ui-egui/src/i18n/uk.tsv | 3 ++ crates/ui-egui/src/i18n/zh-hans.tsv | 3 ++ crates/ui-egui/src/i18n/zh-hant.tsv | 3 ++ crates/ui-egui/src/link_ui.rs | 30 ++++++++++- crates/ui-egui/tests/ui_group/pdf_links.rs | 62 ++++++++++++++++++++++ parity/acrobat-features.toml | 16 ++++-- 30 files changed, 238 insertions(+), 34 deletions(-) diff --git a/ATTRIBUTION.toml b/ATTRIBUTION.toml index 616c4bae..22a5be6f 100644 --- a/ATTRIBUTION.toml +++ b/ATTRIBUTION.toml @@ -7,7 +7,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Greek UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "f158bd9cc2557593365171f28f76fc38b95e7ce07faf2ff8b651d99a540f82a6" +sha256 = "74d52e8de164f17e2f54905dbb53e547b41ecd892c5402f89088a42c205892f7" # ATTRIBUTION.toml: one entry per asset (AGENTS.md §1). The source of truth for provenance and licences. # ATTRIBUTION.md is generated from this file: `cargo xtask assets --write`. Checked by `cargo xtask assets` (part of `xtask ci`). @@ -30,7 +30,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Simplified Chinese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "26c2f0ead5f4dd87d946ba11b4c2b8d442e87d54b2ec8dc15f7abfe187eccc3f" +sha256 = "a0c2b7210671794d9c4b9f1eed1990ce6e34f91fac54f330b55a5d163487c417" [[asset]] path = "crates/ui-egui/src/i18n/zh-hant.tsv" @@ -41,7 +41,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Traditional Chinese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "658f1c4a24029fd767cabecb8f8f3be2eacc0d09f710eae0e7aceb03cc628f32" +sha256 = "c155ca42d4fe98076aa69bcb45571b034e5c685323024e3652a7056990dec60d" [[asset]] path = "crates/ui-egui/src/i18n/cs.tsv" @@ -52,7 +52,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Czech UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "640636e260b8bc726d40d6d7101698640de5028df0d67935a16741491fb3cb5f" +sha256 = "8d8e221cec2447eeaf2de1e4c22a0ad3bbb2056df445c9a4aab9cc641408abb6" [[asset]] path = "crates/ui-egui/src/i18n/pt-br.tsv" @@ -63,7 +63,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Brazilian Portuguese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "bf92cc2ebf7a1a7ae1f7c4fd270ca7643530bc6f8b38d5cba4da3594498f2889" +sha256 = "851e4c262d5cc48c0556845403fdea3c8909c86d3c83175ac56e66da6c3df2c2" [[asset]] path = "crates/ui-egui/src/i18n/es.tsv" @@ -74,7 +74,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Spanish UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "7c12f6bf221a654ce22e77fc723e621daf8dd52ceafd6bff075b7b070d484aa2" +sha256 = "47be4a80633789d29ed0e5d70f8b4f7fc5e2c5af2181076c4773f7808620e598" [[asset]] path = "crates/ui-egui/src/i18n/fr.tsv" @@ -85,7 +85,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "French UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "3d16bf1a86f7d8108ec64569e18db00eafba174092f27ca93c8281ffb93a8c39" +sha256 = "c9792fd691ab7b8d1d575b31787813d45aacb6ac53af252dffeaab2d0b5f867b" [[asset]] path = "crates/ui-egui/src/i18n/de.tsv" @@ -96,7 +96,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "German UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "64cfe3444b8c0cf1bc91938567b52ed52fccd9124e2a567f8ac2d710510b7c30" +sha256 = "97faaaf19582d9c3d34325a14e0db1b1b5bdf6b49151e8dda5d98e7a80049f12" [[asset]] path = "crates/ui-egui/src/i18n/te.tsv" @@ -107,7 +107,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Telugu UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "7e1c2cebada9fd1d065d10496e77e6c0ae4b08ef9145292b1ce216fe476036ba" +sha256 = "85e2ba7e083a629c493dd88911614e81ecb26be5166f2859a6d840a410a0399e" [[asset]] path = "crates/ui-egui/src/i18n/it.tsv" @@ -118,7 +118,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Italian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "4ed39c62cf09008372d5f079458bb7ab80ed62786c58e8193ded3d35e8fea521" +sha256 = "4a40714c86b012d5e4810b6279824a6a576d7f5a8202a5932dd2a7de7b23bcdd" [[asset]] path = "crates/ui-egui/src/i18n/pl.tsv" @@ -129,7 +129,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Polish UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "92ceee115cfb37b94dd7c5aad674896d3529b1f5637b1fd9529458b7d91ab8df" +sha256 = "fc907f19f2273c5a3fd69544207c757d1e43c97f371e0bebefea409a266af640" [[asset]] path = "crates/ui-egui/src/i18n/ru.tsv" @@ -140,7 +140,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Russian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "1e1524a817f7ee2b53aa7d48e0d755173578060d2fae9fa3d9c7ecd18ec3b0f0" +sha256 = "dc674b7b60359e26fe994594ee57160772f35997550888fc8b41065d9c248023" [[asset]] path = "crates/ui-egui/src/i18n/hu.tsv" @@ -151,7 +151,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Hungarian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "faee788eefd418d93caded400a81ea33a5ec5ea20ae62bf36f344a694027c7b5" +sha256 = "f081f92d60e4b3ed75ecef178698170346965b0f36d49e2c46ec6bbd0de37f7e" [[asset]] path = "crates/ui-egui/src/i18n/bg.tsv" @@ -162,7 +162,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Bulgarian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "d88ae2bc2ad294ef9daa783768feb9827c3a90bf60f0c022cf714ddd8d5e2606" +sha256 = "ec0a0349c33e5f22a4c27367d55454a4946f1c7288f65f7a8b1e4fc417191ebe" [[asset]] path = "crates/ui-egui/src/i18n/kk.tsv" @@ -173,7 +173,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Kazakh UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "203245e46fdddd99a6e9c2fac1b05db6f6e3a3347369a2d0dc5846dcd0293c32" +sha256 = "b8e71686440249c9247bf99089d828a172e08cc5e4a85939af05ddb60589518b" [[asset]] path = "crates/ui-egui/src/i18n/ja.tsv" @@ -184,7 +184,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Japanese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "d9e8acae91cb85b22abd2f8e7d285709d200f8b9b60e09370e3c1919328607c0" +sha256 = "0ea5c0904da3166e252746803c96f9f72a69242e151328f7b8b454052db12dd7" [[asset]] path = "crates/ui-egui/src/i18n/uk.tsv" @@ -195,7 +195,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Ukrainian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "bcb35726d63d4f154df5a389d3be46b53b6de9836e79d79b5fb13cd5aa9c5157" +sha256 = "380e545b11a8e3325362acb6cb99f57203262baa2487665d57a2edbd504aeafb" [[asset]] path = "crates/ui-egui/src/i18n/ar.tsv" @@ -206,7 +206,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Arabic UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "7550018b4737b4ee689d38c31b5ea2093e26a18ccd717661b80a68778a0fb652" +sha256 = "369fc4f6c0fe1501cfb4cb310e0007a054287ee5e3b813617b783782c6759bfd" [[asset]] path = "crates/ui-egui/src/i18n/tr.tsv" @@ -217,7 +217,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Turkish UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "45580772203b30915881c8945d33e6edc6363d7b5583fc0eff5acface163b37b" +sha256 = "cd346d40188a768468adc7631150e93527a52c1693669425c8569bd9ba548b60" [[asset]] path = "assets/icons/accessibility.svg" @@ -3328,4 +3328,4 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Slovene UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "a9c8fd94278a41acda913e7d3242da627701207a1ac187f2bf2b5a9fddcf6aa8" +sha256 = "72bb3bac963553ad79268dc3b7ef02d25f03d71fb79043a0c6df3bd38d984b32" diff --git a/crates/automation/src/links.rs b/crates/automation/src/links.rs index fd77b1d7..5375f2b6 100644 --- a/crates/automation/src/links.rs +++ b/crates/automation/src/links.rs @@ -188,8 +188,15 @@ impl Automation { } pub(crate) fn links_remove(&mut self, a: &Args) -> Result { - let before = self.doc(a)?.links.len(); - let mut out = self.apply(a, Edit::RemoveLinks { pages: None })?; + let pages = match a.opt_ints("pages")? { + Some(_) => Some(self.pages(a, "pages")?), + None => None, + }; + let before = match &pages { + Some(ps) => self.doc(a)?.links.iter().filter(|l| ps.contains(&l.page)).count(), + None => self.doc(a)?.links.len(), + }; + let mut out = self.apply(a, Edit::RemoveLinks { pages })?; out["removed"] = json!(before); Ok(out) } diff --git a/crates/automation/src/tools.rs b/crates/automation/src/tools.rs index 56d5aad5..e90d5bdc 100644 --- a/crates/automation/src/tools.rs +++ b/crates/automation/src/tools.rs @@ -581,10 +581,10 @@ pub fn tools() -> Vec { t("links_from_urls", "Create links from URLs", "Find web addresses (http://, https://, www.) in the text of every page and make them clickable links. Returns the URLs. Undoable.") .cmd("edit.links_from_urls") .with(schema(json!({ "doc": doc() }), &["doc"])), - t("links_remove", "Remove all links", "Remove every link in the document. Undoable.") + t("links_remove", "Remove all links", "Remove every link in the document (or on the given pages). Undoable.") .destructive() .cmd("edit.remove_links") - .with(schema(json!({ "doc": doc() }), &["doc"])), + .with(schema(json!({ "doc": doc(), "pages": pages("whose links to remove (default: all)") }), &["doc"])), t("measure_distance", "Measure distance", "Add an undoable two-point distance annotation using the scale of the first point's viewport.") .cmd("measure.distance").with(measure_schema()), t("measure_perimeter", "Measure perimeter", "Add an undoable connected-line length annotation. To include a closing edge, repeat the first point at the end.") diff --git a/crates/automation/tests/automation.rs b/crates/automation/tests/automation.rs index 7e65d6d0..a68cd9f6 100644 --- a/crates/automation/tests/automation.rs +++ b/crates/automation/tests/automation.rs @@ -3161,9 +3161,14 @@ fn links_through_tools() { let list = ok(&mut a, "link_list", json!({ "doc": doc })); assert!(list["links"].as_array().unwrap().iter().any(|l| l["url"] == "https://pdfcraft.dev")); ok(&mut a, "link_delete", json!({ "doc": doc, "page": 1, "index": added["index"] })); - let r = ok(&mut a, "links_remove", json!({ "doc": doc })); + let r = ok(&mut a, "links_remove", json!({ "doc": doc, "pages": [1] })); assert_eq!(r["removed"], 2); assert_eq!(ok(&mut a, "link_list", json!({ "doc": doc }))["count"], 0); + // Add one more link and test links_remove across all pages + ok(&mut a, "link_add", json!({ "doc": doc, "page": 1, "rect": [72, 300, 200, 320], "url": "https://example.org" })); + let r2 = ok(&mut a, "links_remove", json!({ "doc": doc })); + assert_eq!(r2["removed"], 1); + assert_eq!(ok(&mut a, "link_list", json!({ "doc": doc }))["count"], 0); assert!(matches!(a.call("link_add", &json!({ "doc": doc, "page": 1, "rect": [0, 0, 50, 20] })), Err(ToolError::InvalidArgs(_)))); } diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index 372dc30f..6e09d46a 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -2047,6 +2047,12 @@ fn links_from_urls_and_link_edits() { assert_eq!(d.links.len(), 1); assert_eq!(d.info.links.len(), 1, "the viewer follows it"); assert!(s.find_urls(id).is_empty(), "already linked"); + // Remove all links and undo. + s.apply(id, Edit::RemoveLinks { pages: None }).unwrap(); + assert!(s.get(id).unwrap().links.is_empty()); + assert_eq!(s.get(id).unwrap().can_undo(), Some("Remove all links")); + s.undo(id).unwrap(); + assert_eq!(s.get(id).unwrap().links.len(), 1); // Link Properties and delete. let l = s.get(id).unwrap().links[0].clone(); s.apply(id, Edit::SetLink { page: 0, index: l.index, rect: None, action: Some(LinkAction::Page(0)), style: None }).unwrap(); diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index 31d1f423..b68a6d2b 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -2375,6 +2375,10 @@ pub fn document_area(app: &mut PdfCraftApp, index: usize, ui: &mut egui::Ui) { let mut current_overlap = -1.0f32; let mut current_height = f32::INFINITY; let pointer = ui.input(|i| i.pointer.hover_pos()); + if resp.secondary_clicked() { + // Set again below when the right-click lands on a link. + view.links.context = None; + } for &i in &visible_pages { let r = snap_to_pixels(rects[i].translate(origin.to_vec2()), ppp); if !r.intersects(visible.expand(400.0)) { @@ -2671,6 +2675,14 @@ pub fn document_area(app: &mut PdfCraftApp, index: usize, ui: &mut egui::Ui) { text_used || image_used }; let on_link = tool == QuickTool::Link && can_modify && crate::link_ui::page_input(ui, &resp, &xf, i, info, &doc_links, view); + if tool == QuickTool::Select + && resp.secondary_clicked() + && can_modify + && let Some(p) = pointer.filter(|p| xf.rect.contains(*p)) + && let Some(l) = doc.links.iter().find(|l| l.page == i && crate::link_ui::screen_rect(&xf, info, i, l.rect).expand(2.0).contains(p)) + { + view.links.context = Some((i, l.index)); + } let consumed = on_objects || on_edit_text || on_link || on_content || boxing || on_field || comments::page_input(ui, &resp, &pcx, view); let preview_target = @@ -2999,6 +3011,26 @@ pub fn document_area(app: &mut PdfCraftApp, index: usize, ui: &mut egui::Ui) { } return; } + if let Some((page, index)) = view.links.context { + if ui.button(tl!("Properties…")).clicked() { + view.links.open_existing = Some((page, index)); + ui.close(); + } + if ui.add_enabled(can_modify, egui::Button::new(tl!("Delete"))).clicked() { + view.pending_edit = Some(Edit::DeleteLink { page, index }); + view.links.selected = None; + view.links.context = None; + ui.close(); + } + ui.separator(); + if ui.add_enabled(can_modify, egui::Button::new(tl!("Remove all links"))).clicked() { + view.pending_edit = Some(Edit::RemoveLinks { pages: None }); + view.links.selected = None; + view.links.context = None; + ui.close(); + } + return; + } canvas_action = comments::context_menu(ui, view, info, prefs, allowed); }); (wanted, visible_now) diff --git a/crates/ui-egui/src/commands.rs b/crates/ui-egui/src/commands.rs index 126f5ab1..2266cb30 100644 --- a/crates/ui-egui/src/commands.rs +++ b/crates/ui-egui/src/commands.rs @@ -408,9 +408,7 @@ impl PdfCraftApp { self.notify_tr("Drag a rectangle to create a link; double-click a link to edit it"); } "edit.links_from_urls" => self.links_from_urls(), - "edit.remove_links" => { - self.apply_edit(Edit::RemoveLinks { pages: None }); - } + "edit.remove_links" => self.remove_all_links(), "redact.mark" => { self.quick_tool = crate::QuickTool::Redact; self.left = crate::LeftPanel::Tool("redact"); diff --git a/crates/ui-egui/src/i18n/ar.tsv b/crates/ui-egui/src/i18n/ar.tsv index 29702ad4..ef6e10b0 100644 --- a/crates/ui-egui/src/i18n/ar.tsv +++ b/crates/ui-egui/src/i18n/ar.tsv @@ -225,6 +225,9 @@ Add or edit links إضافة روابط أو تحريرها Create links from URLs إنشاء روابط من عناوين URL Remove all links إزالة كل الروابط + No links to remove لا توجد روابط لإزالتها + Removed 1 link تمت إزالة رابط واحد + Removed {n} links الروابط التي أزيلت: {n} Add Bates numbering… إضافة ترقيم بيتس… Add image… إضافة صورة… Redact text and images حجب النص والصور diff --git a/crates/ui-egui/src/i18n/bg.tsv b/crates/ui-egui/src/i18n/bg.tsv index 719f3129..7add6c44 100644 --- a/crates/ui-egui/src/i18n/bg.tsv +++ b/crates/ui-egui/src/i18n/bg.tsv @@ -1172,6 +1172,9 @@ Redact pages… Заличи страници… Redaction properties… Свойства на заличаването… Remove all links Премахни всички връзки + No links to remove Няма връзки за премахване + Removed 1 link Премахната е 1 връзка + Removed {n} links Премахнати връзки: {n} Remove background Премахни фона Remove header & footer Премахни горния и долния колонтитул Remove hidden information… Премахни скритата информация… diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index 1d4f057c..3361a323 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -342,6 +342,9 @@ key Space Mezerník Add or edit links Přidat nebo upravit odkazy Create links from URLs Vytvořit odkazy z adres URL Remove all links Odebrat všechny odkazy + No links to remove Nejsou žádné odkazy k odebrání + Removed 1 link Odebrán 1 odkaz + Removed {n} links Odebrané odkazy: {n} Add Bates numbering… Přidat Batesovo číslování… Add image… Přidat obrázek… Redact text and images Začernit text a obrázky diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index e99ed095..87398a25 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -198,6 +198,9 @@ Add or edit links Links hinzufügen oder bearbeiten Create links from URLs Links aus URLs erstellen Remove all links Alle Links entfernen + No links to remove Keine Links zum Entfernen + Removed 1 link 1 Link entfernt + Removed {n} links {n} Links entfernt Add Bates numbering… Bates-Nummerierung hinzufügen… Add image… Bild hinzufügen… Redact text and images Text und Bilder schwärzen diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index a2ddff0b..1077de9d 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -2147,6 +2147,9 @@ Add or edit links Προσθήκη ή επεξεργασία συνδέσμων Create links from URLs Δημιουργία συνδέσμων από URL Remove all links Αφαίρεση όλων των συνδέσμων + No links to remove Δεν υπάρχουν σύνδεσμοι για κατάργηση + Removed 1 link Καταργήθηκε 1 σύνδεσμος + Removed {n} links Σύνδεσμοι που καταργήθηκαν: {n} Add Bates numbering… Προσθήκη αρίθμησης Bates… Add image… Προσθήκη εικόνας… Redact text and images Οριστική απόκρυψη κειμένου και εικόνων diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index 47c03919..7a6c672c 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -221,6 +221,9 @@ Add or edit links Añadir o editar enlaces Create links from URLs Crear enlaces a partir de URL Remove all links Quitar todos los enlaces + No links to remove No hay vínculos que quitar + Removed 1 link Se quitó 1 vínculo + Removed {n} links Se quitaron {n} vínculos Add Bates numbering… Añadir numeración Bates… Add image… Añadir imagen… Redact text and images Censurar texto e imágenes diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index d605b850..776c4aa7 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -198,6 +198,9 @@ Add or edit links Ajouter ou modifier des liens Create links from URLs Créer des liens à partir des URL Remove all links Retirer tous les liens + No links to remove Aucun lien à retirer + Removed 1 link 1 lien retiré + Removed {n} links Liens retirés : {n} Add Bates numbering… Ajouter une numérotation Bates… Add image… Ajouter une image… Redact text and images Biffer du texte et des images diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index 28e24cd9..388897ff 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -196,6 +196,9 @@ Add or edit links Hivatkozások hozzáadása vagy szerkesztése Create links from URLs Hivatkozások létrehozása URL-ekből Remove all links Összes hivatkozás eltávolítása + No links to remove Nincs eltávolítható hivatkozás + Removed 1 link 1 hivatkozás eltávolítva + Removed {n} links {n} hivatkozás eltávolítva Add Bates numbering… Bates-számozás hozzáadása… Add image… Kép hozzáadása… Redact text and images Szöveg és képek kitakarása diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index 7f81eab8..b55031cb 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -222,6 +222,9 @@ Add or edit links Aggiungi o modifica collegamenti Create links from URLs Crea collegamenti dagli URL Remove all links Rimuovi tutti i collegamenti + No links to remove Nessun collegamento da rimuovere + Removed 1 link 1 collegamento rimosso + Removed {n} links {n} collegamenti rimossi Add Bates numbering… Aggiungi numerazione Bates… Add image… Aggiungi immagine… Redact text and images Oscura testo e immagini diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index 33e434e5..287bf4b9 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -225,6 +225,9 @@ Add or edit links リンクを追加・編集 Create links from URLs URL からリンクを作成 Remove all links すべてのリンクを削除 + No links to remove 削除するリンクはありません + Removed 1 link リンクを 1 件削除しました + Removed {n} links リンクを {n} 件削除しました Add Bates numbering… ベイツ番号を追加… Add image… 画像を追加… Redact text and images テキストと画像を墨消し diff --git a/crates/ui-egui/src/i18n/kk.tsv b/crates/ui-egui/src/i18n/kk.tsv index d198b3db..2546c3e6 100644 --- a/crates/ui-egui/src/i18n/kk.tsv +++ b/crates/ui-egui/src/i18n/kk.tsv @@ -186,6 +186,9 @@ Add or edit links Сілтемелерді қосу немесе түзету Create links from URLs URL-ден сілтемелер жасау Remove all links Барлық сілтемелерді алып тастау + No links to remove Жоятын сілтеме жоқ + Removed 1 link 1 сілтеме жойылды + Removed {n} links Жойылған сілтемелер: {n} Add Bates numbering… Bates нөмірлеуін қосу… Add image… Сурет қосу… Redact text and images Мәтін мен суреттерді жасыру diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index 8fb27b3f..b2417bd7 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -221,6 +221,9 @@ Add or edit links Dodaj lub edytuj łącza Create links from URLs Utwórz łącza z adresów URL Remove all links Usuń wszystkie łącza + No links to remove Brak łączy do usunięcia + Removed 1 link Usunięto 1 łącze + Removed {n} links Usunięte łącza: {n} Add Bates numbering… Dodaj numerację Batesa… Add image… Dodaj obraz… Redact text and images Utajnij tekst i obrazy diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index bdcaf04c..7509e455 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -1116,6 +1116,9 @@ Redact pages… Ocultar páginas… Redaction properties… Propriedades da ocultação… Remove all links Remover todos os links + No links to remove Não há links para remover + Removed 1 link 1 link removido + Removed {n} links {n} links removidos Remove background Remover plano de fundo Remove header & footer Remover cabeçalho e rodapé Remove hidden information… Remover informações ocultas… diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index fcb886e5..bd30b518 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -218,6 +218,9 @@ Add or edit links Добавить или изменить ссылки Create links from URLs Создать ссылки из URL Remove all links Удалить все ссылки + No links to remove Нет ссылок для удаления + Removed 1 link Удалена 1 ссылка + Removed {n} links Удалено ссылок: {n} Add Bates numbering… Добавить нумерацию Бейтса… Add image… Добавить изображение… Redact text and images Скрыть текст и изображения diff --git a/crates/ui-egui/src/i18n/sl.tsv b/crates/ui-egui/src/i18n/sl.tsv index 71acd021..7f61cf81 100644 --- a/crates/ui-egui/src/i18n/sl.tsv +++ b/crates/ui-egui/src/i18n/sl.tsv @@ -1429,6 +1429,9 @@ Reliable character encoding is provided Zagotovljeno je zanesljivo kodiranje znakov Remove Odstrani Remove all links Odstrani vse povezave + No links to remove Ni povezav za odstranitev + Removed 1 link Odstranjena je 1 povezava + Removed {n} links Odstranjene povezave: {n} Remove background Odstrani ozadje Remove checkmark Odstrani kljukico Remove from Recent Odstrani iz nedavnih diff --git a/crates/ui-egui/src/i18n/te.tsv b/crates/ui-egui/src/i18n/te.tsv index 55aeea98..7548f22f 100644 --- a/crates/ui-egui/src/i18n/te.tsv +++ b/crates/ui-egui/src/i18n/te.tsv @@ -202,6 +202,9 @@ comment menu Edit సవరించు Add or edit links లింక్‌లను జోడించు లేదా సవరించు Create links from URLs URLల నుండి లింక్‌లను సృష్టించు Remove all links అన్ని లింక్‌లను తీసివేయి + No links to remove తీసివేయడానికి లింక్‌లు లేవు + Removed 1 link 1 లింక్ తీసివేయబడింది + Removed {n} links తీసివేసిన లింక్‌లు: {n} Add Bates numbering… బేట్స్ నంబరింగ్ జోడించు… Add image… చిత్రాన్ని జోడించు… Redact text and images టెక్స్ట్‌ను, చిత్రాలను రెడాక్ట్ చేయి diff --git a/crates/ui-egui/src/i18n/tr.tsv b/crates/ui-egui/src/i18n/tr.tsv index 2cf8ba8f..a237ad82 100644 --- a/crates/ui-egui/src/i18n/tr.tsv +++ b/crates/ui-egui/src/i18n/tr.tsv @@ -183,6 +183,9 @@ Add or edit links Bağlantı ekle veya düzenle Create links from URLs URL'lerden bağlantı oluştur Remove all links Tüm bağlantıları kaldır + No links to remove Kaldırılacak bağlantı yok + Removed 1 link 1 bağlantı kaldırıldı + Removed {n} links {n} bağlantı kaldırıldı Add Bates numbering… Bates numaralandırması ekle… Add image… Resim ekle… Redact text and images Metin ve resimleri karart diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index f35aadb6..749db70a 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -223,6 +223,9 @@ Add or edit links Додати або редагувати посилання Create links from URLs Створити посилання з URL Remove all links Видалити всі посилання + No links to remove Немає посилань для вилучення + Removed 1 link Вилучено 1 посилання + Removed {n} links Вилучено посилань: {n} Add Bates numbering… Додати нумерацію Бейтса… Add image… Додати зображення… Redact text and images Вилучити текст і зображення diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index f313fe45..36ed61cb 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -1179,6 +1179,9 @@ Redact pages… 密文页面… Redaction properties… 密文属性… Remove all links 删除所有链接 + No links to remove 没有可删除的链接 + Removed 1 link 已删除 1 个链接 + Removed {n} links 已删除 {n} 个链接 Remove background 删除背景 Remove header & footer 删除页眉和页脚 Remove hidden information… 删除隐藏信息… diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index 437c07ae..894038fc 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -1123,6 +1123,9 @@ Redact pages… 遮蔽頁面… Redaction properties… 遮蔽內容設定… Remove all links 移除所有連結 + No links to remove 沒有可移除的連結 + Removed 1 link 已移除 1 個連結 + Removed {n} links 已移除 {n} 個連結 Remove background 移除背景 Remove header & footer 移除頁首與頁尾 Remove hidden information… 移除隱藏資訊… diff --git a/crates/ui-egui/src/link_ui.rs b/crates/ui-egui/src/link_ui.rs index 248bb25c..a969a195 100644 --- a/crates/ui-egui/src/link_ui.rs +++ b/crates/ui-egui/src/link_ui.rs @@ -17,6 +17,10 @@ pub struct LinkView { /// A link rectangle being drawn: (page, start). pub drag: Option<(usize, Pos2)>, pub selected: Option<(usize, usize)>, + /// The link the last right-click on a page hit (page, index), if any: the canvas + /// context menu shows the link items only for it, so a right-click elsewhere (on a + /// comment, say) keeps its own menu even while a link stays selected. + pub context: Option<(usize, usize)>, /// Ask the app to open Link Properties for a new rect (page, user rect) or an existing /// link (page, index). pub open_new: Option<(usize, [f64; 4])>, @@ -41,7 +45,7 @@ pub struct LinkDraft { pub style: LinkStyle, } -fn screen_rect(xf: &PageXform, info: &DocInfo, page: usize, r: [f64; 4]) -> Rect { +pub(crate) fn screen_rect(xf: &PageXform, info: &DocInfo, page: usize, r: [f64; 4]) -> Rect { xf.user_rect(info, page, [r[0] as f32, r[1] as f32, r[2] as f32, r[3] as f32]) } @@ -80,6 +84,13 @@ pub(crate) fn page_input( let Some(p) = pointer.filter(|p| xf.rect.contains(*p)) else { return false }; let hit = links.iter().filter(|l| l.page == page).find(|l| screen_rect(xf, info, page, l.rect).expand(2.0).contains(p)); ui.ctx().set_cursor_icon(if hit.is_some() { egui::CursorIcon::PointingHand } else { egui::CursorIcon::Crosshair }); + if resp.secondary_clicked() + && let Some(l) = hit + { + lv.selected = Some((page, l.index)); + lv.context = Some((page, l.index)); + return true; + } if resp.clicked() || resp.double_clicked() { let now = ui.input(|i| i.time); // A second click on the same link soon after the first is a double-click. @@ -188,6 +199,23 @@ impl PdfCraftApp { } } } + + /// Remove every link in the active document. + pub fn remove_all_links(&mut self) { + let Some((_, id)) = self.active_ids() else { return }; + let count = self.session.get(id).map_or(0, |d| d.links.len()); + if count == 0 { + self.notify_tr("No links to remove"); + return; + } + if self.apply_edit(Edit::RemoveLinks { pages: None }) { + if count == 1 { + self.notify_tr("Removed 1 link"); + } else { + self.notify_fmt("Removed {n} links", &[("n", &count.to_string())]); + } + } + } } /// Link Properties. Returns (apply, cancel). diff --git a/crates/ui-egui/tests/ui_group/pdf_links.rs b/crates/ui-egui/tests/ui_group/pdf_links.rs index d5eabb2a..0916de8e 100644 --- a/crates/ui-egui/tests/ui_group/pdf_links.rs +++ b/crates/ui-egui/tests/ui_group/pdf_links.rs @@ -83,3 +83,65 @@ fn drawing_editing_and_deleting_links() { h.run_steps(3); assert!(links(&h).is_empty()); } + +#[test] +fn removing_all_links_and_undo() { + let mut h = harness(); + assert!(h.state_mut().execute("edit.link")); + let (a, b) = (at(&h, 40.0, 300.0), at(&h, 160.0, 320.0)); + drag(&mut h, a, b); + h.state_mut().link_draft.as_mut().unwrap().url = "https://example.org".into(); + h.get_by_label("OK").click(); + h.run_steps(3); + assert_eq!(links(&h).len(), 1); + + // Execute remove all links command + assert!(h.state_mut().execute("edit.remove_links")); + h.run_steps(3); + assert_eq!(links(&h).len(), 0); + + // Undo restores the link + h.state_mut().undo(); + h.run_steps(3); + assert_eq!(links(&h).len(), 1); +} + +fn right_click(h: &mut Harness<'static, PdfCraftApp>, p: Pos2) { + h.hover_at(p); + h.run_steps(1); + h.event(egui::Event::PointerButton { pos: p, button: egui::PointerButton::Secondary, pressed: true, modifiers: Default::default() }); + h.event(egui::Event::PointerButton { pos: p, button: egui::PointerButton::Secondary, pressed: false, modifiers: Default::default() }); + h.run_steps(3); +} + +#[test] +fn the_link_context_menu_shows_only_for_a_right_click_on_a_link() { + let mut h = harness(); + assert!(h.state_mut().execute("edit.link")); + let (a, b) = (at(&h, 40.0, 100.0), at(&h, 160.0, 120.0)); + drag(&mut h, a, b); + h.state_mut().link_draft.as_mut().unwrap().url = "https://example.org".into(); + h.get_by_label("OK").click(); + h.run_steps(3); + let (on_link, away) = (at(&h, 100.0, 110.0), at(&h, 250.0, 200.0)); + // Select the link with the Link tool, then right-click away from it: the menu there is + // not the link's. + h.hover_at(on_link); + h.run_steps(1); + h.event(egui::Event::PointerButton { pos: on_link, button: egui::PointerButton::Primary, pressed: true, modifiers: Default::default() }); + h.event(egui::Event::PointerButton { pos: on_link, button: egui::PointerButton::Primary, pressed: false, modifiers: Default::default() }); + h.run_steps(3); + assert!(h.state().views[0].links.selected.is_some()); + right_click(&mut h, away); + assert!(h.query_by_label("Remove all links").is_none(), "a right-click away from the link got the link menu"); + h.key_press(egui::Key::Escape); + h.run_steps(3); + // A right-click on the link gets it, with the Select tool too. + h.state_mut().quick_tool = pdfcraft_ui_egui::QuickTool::Select; + h.run_steps(2); + right_click(&mut h, on_link); + h.get_by_label("Remove all links").click(); + h.run_steps(3); + assert!(links(&h).is_empty()); + assert_eq!(h.state().session.get(h.state().views[0].id).unwrap().can_undo(), Some("Remove all links")); +} diff --git a/parity/acrobat-features.toml b/parity/acrobat-features.toml index cccdc2e1..33a3154d 100644 --- a/parity/acrobat-features.toml +++ b/parity/acrobat-features.toml @@ -2211,11 +2211,17 @@ title = "Remove all links" area = "C" tier = "P2" milestone = "M7" -status = "planned" -commands = [] -tools = [] -evidence = [] -notes = "" +status = "shipped" +commands = ["edit.remove_links"] +tools = ["links_remove"] +evidence = [ + "crates/engine/src/tests.rs::links_from_urls_and_link_edits", + "crates/automation/tests/automation.rs::links_through_tools", + "crates/annot/src/tests.rs::links_are_added_edited_listed_and_removed", + "crates/ui-egui/tests/ui_group/pdf_links.rs::removing_all_links_and_undo", + "crates/ui-egui/tests/ui_group/pdf_links.rs::the_link_context_menu_shows_only_for_a_right_click_on_a_link", +] +notes = "removes all link annotations across the document or on specified pages (links_remove pages); a right-click on a link offers Properties, Delete and Remove all links; undoable, with a toast giving the count" [[feature]] id = "edit.action-goto"