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 <noreply@anthropic.com>

* Restore the PR's links_remove pages test (lost with the workdir conflict)

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

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Krzysztof Cieślik 2026-10-12 09:31:39 +02:00 committed by GitHub
parent e8e275cbd4
commit 7d419a6c73
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
30 changed files with 238 additions and 34 deletions

View File

@ -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"

View File

@ -188,8 +188,15 @@ impl Automation {
}
pub(crate) fn links_remove(&mut self, a: &Args) -> Result<Value> {
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)
}

View File

@ -581,10 +581,10 @@ pub fn tools() -> Vec<ToolDef> {
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.")

View File

@ -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(_))));
}

View File

@ -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();

View File

@ -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)

View File

@ -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");

View File

@ -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 حجب النص والصور

Can't render this file because it contains an unexpected character in line 6 and column 47.

View File

@ -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… Премахни скритата информация…

Can't render this file because it contains an unexpected character in line 8 and column 16.

View File

@ -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

Can't render this file because it contains an unexpected character in line 6 and column 47.

View File

@ -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

Can't render this file because it contains an unexpected character in line 6 and column 51.

View File

@ -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 Οριστική απόκρυψη κειμένου και εικόνων

Can't render this file because it has a wrong number of fields in line 5.

View File

@ -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

Can't render this file because it contains an unexpected character in line 6 and column 60.

View File

@ -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

Can't render this file because it contains an unexpected character in line 6 and column 39.

View File

@ -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

Can't render this file because it contains an unexpected character in line 7 and column 44.

View File

@ -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

Can't render this file because it contains an unexpected character in line 6 and column 47.

View File

@ -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 テキストと画像を墨消し

Can't render this file because it contains an unexpected character in line 6 and column 47.

View File

@ -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 Мәтін мен суреттерді жасыру

1 Menu Мәзір
186 Add or edit links Сілтемелерді қосу немесе түзету
187 Create links from URLs URL-ден сілтемелер жасау
188 Remove all links Барлық сілтемелерді алып тастау
189 No links to remove Жоятын сілтеме жоқ
190 Removed 1 link 1 сілтеме жойылды
191 Removed {n} links Жойылған сілтемелер: {n}
192 Add Bates numbering… Bates нөмірлеуін қосу…
193 Add image… Сурет қосу…
194 Redact text and images Мәтін мен суреттерді жасыру

View File

@ -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

Can't render this file because it contains an unexpected character in line 6 and column 47.

View File

@ -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…

Can't render this file because it contains an unexpected character in line 9 and column 16.

View File

@ -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 Скрыть текст и изображения

Can't render this file because it contains an unexpected character in line 9 and column 16.

View File

@ -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

Can't render this file because it contains an unexpected character in line 9 and column 16.

View File

@ -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 టెక్స్ట్‌ను, చిత్రాలను రెడాక్ట్ చేయి

Can't render this file because it contains an unexpected character in line 13 and column 16.

View File

@ -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

Can't render this file because it has a wrong number of fields in line 7.

View File

@ -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 Вилучити текст і зображення

Can't render this file because it contains an unexpected character in line 6 and column 47.

View File

@ -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… 删除隐藏信息…

Can't render this file because it contains an unexpected character in line 9 and column 16.

View File

@ -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… 移除隱藏資訊…

Can't render this file because it contains an unexpected character in line 9 and column 16.

View File

@ -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).

View File

@ -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"));
}

View File

@ -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"