M3: add PDF tab context menu (fixes #1233) (#1266)

* feat(ui): add PDF tab context menu (#1233)

* Tab menu: reveal through D-Bus off the UI thread

`dbus-send --print-reply --reply-timeout=2000` (and the xdg-open fallback)
could block the frame for 2 s or more; on Linux and FreeBSD the reveal now runs
on a `pdfcraft-reveal` thread and a failure is logged. The existence check stays
on the UI thread and is still reported; Windows and macOS launches are
unchanged. Refreshes the catalog hashes after the main merge.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: srt8hellcat2-ui <[email protected]>
Co-authored-by: Brandon Thomas <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
This commit is contained in:
authored and GitHub committed 2026-10-12 03:15:02 -04:00
1 parent 7183bf4264
commit 06480e395d
25 files changed
+452 -22

No files matched your search

+20 -20
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 = "d7ad8f46f105638aff5b91423fe74077af5bf1f3246eeb9e066df8e4df0e222e"
sha256 = "f158bd9cc2557593365171f28f76fc38b95e7ce07faf2ff8b651d99a540f82a6"
# 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 = "7df6e3aebce85e8eaac46465ab46d0c8447f314cdcdbe41ba4fc0188a2105f14"
sha256 = "26c2f0ead5f4dd87d946ba11b4c2b8d442e87d54b2ec8dc15f7abfe187eccc3f"
[[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 = "49eccf2decfc53724250e685bb448c41ccadcf6906fa56ba9b13d8047329ab99"
sha256 = "658f1c4a24029fd767cabecb8f8f3be2eacc0d09f710eae0e7aceb03cc628f32"
[[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 = "6abec13d351d38a12e4634f64bbf6b884c8102af1f97da7e08cbcd29de14a5ba"
sha256 = "640636e260b8bc726d40d6d7101698640de5028df0d67935a16741491fb3cb5f"
[[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 = "afa953e566e7b9533df28845b52a768f77c7d8aa254cd3bbe1928ddf28019efa"
sha256 = "bf92cc2ebf7a1a7ae1f7c4fd270ca7643530bc6f8b38d5cba4da3594498f2889"
[[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 = "1de0258b9b1ea7c375ef71d8c4cbeeebbc0f976cb6aead04bdd1d06d18f7c717"
sha256 = "7c12f6bf221a654ce22e77fc723e621daf8dd52ceafd6bff075b7b070d484aa2"
[[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 = "59ac5e705d8d74273ce774cb2dd8020313277ae2a376ae2967031d60ae590745"
sha256 = "3d16bf1a86f7d8108ec64569e18db00eafba174092f27ca93c8281ffb93a8c39"
[[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 = "36e814e0bec4fab77c7a0face806fa14269c1964b9850d19765d4b41b4503300"
sha256 = "64cfe3444b8c0cf1bc91938567b52ed52fccd9124e2a567f8ac2d710510b7c30"
[[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 = "22c8d5335c7b7be2e7a735e94b076ab9aead4817335953387f1122691b00e04e"
sha256 = "7e1c2cebada9fd1d065d10496e77e6c0ae4b08ef9145292b1ce216fe476036ba"
[[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 = "3c2740ffdf370e1dc1cd3fbfd4da05185a0d247233fcb143c63c2f560a072cd2"
sha256 = "4ed39c62cf09008372d5f079458bb7ab80ed62786c58e8193ded3d35e8fea521"
[[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 = "755fadd87a23d56ff3319ed58b4651b386603a86ceed1a74919403a9a1f5698b"
sha256 = "92ceee115cfb37b94dd7c5aad674896d3529b1f5637b1fd9529458b7d91ab8df"
[[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 = "3455f5598efb906ad12921cb9264761d7eb8f180d26ef02e8c3ab041d5f63f76"
sha256 = "1e1524a817f7ee2b53aa7d48e0d755173578060d2fae9fa3d9c7ecd18ec3b0f0"
[[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 = "0f9ae32a12fd0285d5e11a523019f410894533669a20ba16ad6ded4907ee8f55"
sha256 = "faee788eefd418d93caded400a81ea33a5ec5ea20ae62bf36f344a694027c7b5"
[[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 = "3e1386f1e8c6412a5a685b127b4871c5ea5e7e1c91428a1a402dca61b785f103"
sha256 = "d88ae2bc2ad294ef9daa783768feb9827c3a90bf60f0c022cf714ddd8d5e2606"
[[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 = "134d6c9787407e8634c2749adcf40c8f15f89e6262e902547f8ef0022e548f72"
sha256 = "203245e46fdddd99a6e9c2fac1b05db6f6e3a3347369a2d0dc5846dcd0293c32"
[[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 = "6dfc2b9e312a888fd0a11426129d38654e40f59888c2c90b1be1751b5893daa3"
sha256 = "d9e8acae91cb85b22abd2f8e7d285709d200f8b9b60e09370e3c1919328607c0"
[[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 = "46f7addc24737c5700b8dff69392d16b24c67b92f0bf92444b806f3f9e107195"
sha256 = "bcb35726d63d4f154df5a389d3be46b53b6de9836e79d79b5fb13cd5aa9c5157"
[[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 = "a680284dc350985b9f0742e258ff6e8d05e2d30513a0d9444c915d250ba1ed9a"
sha256 = "7550018b4737b4ee689d38c31b5ea2093e26a18ccd717661b80a68778a0fb652"
[[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 = "1a1db9cd9a6d3ba59eeb739a8961e57183320dd11ca9f8750c6683ed5bc243a3"
sha256 = "45580772203b30915881c8945d33e6edc6363d7b5583fc0eff5acface163b37b"
[[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 = "499b2ea8dbb49c4d59f4ac360e72a64ac4f3eb2760521c4e9b8f135297180823"
sha256 = "a9c8fd94278a41acda913e7d3242da627701207a1ac187f2bf2b5a9fddcf6aa8"
+2
View File
@@ -133,6 +133,8 @@ Search the whole document as you type, step through matches with <kbd>⌘G</kbd>
## Navigate long documents
Right-click a PDF tab to close it, reopen the last closed file-backed tab, or reveal its file. Closing still asks about unsaved changes. Reopening reads the saved file from disk; it does not restore discarded edits. Reveal selects the file in Explorer or Finder. On Linux and FreeBSD it requests `org.freedesktop.FileManager1.ShowItems`, falling back to opening the containing directory with `xdg-open` when that service is unavailable. Reopen and Reveal are unavailable on the web; Reveal is also unavailable for pathless documents.
Bookmarks, page thumbnails and the document's own page labels (i, ii, 1, 2…) keep you oriented in long documents. Search bookmark titles in the Bookmarks panel to find nested entries even when their parents are collapsed. Matches keep their ancestors for context; Clear restores the unfiltered tree without changing its expansion state.
<table>
+25 -2
View File
@@ -88,6 +88,7 @@ pub fn tab_strip(app: &mut PdfCraftApp, ui: &mut egui::Ui) {
data.insert_temp(id, state);
changed
});
let mut action = None;
let mut close = None;
ui.scope(|ui| {
ui.style_mut().always_scroll_the_only_direction = true;
@@ -119,12 +120,34 @@ pub fn tab_strip(app: &mut PdfCraftApp, ui: &mut egui::Ui) {
if changed && app.active == Some(i) && !app.combine_showing() {
response.scroll_to_me(Some(Align::Center));
}
let id = app.views[i].id;
let reveal = cfg!(not(target_arch = "wasm32")) && doc.path.is_some();
response.context_menu(|ui| {
if ui.button(tl!("Close Tab")).clicked() {
action = Some(crate::tab_context::Action::Close(id));
ui.close();
}
if ui.add_enabled(!app.closed_tabs.is_empty(), egui::Button::new(tl!("Reopen Closed Tab"))).clicked() {
action = Some(crate::tab_context::Action::Reopen);
ui.close();
}
let label = if cfg!(target_os = "macos") { tl!("Reveal in Finder") } else { tl!("Reveal in File Explorer") };
if ui.add_enabled(reveal, egui::Button::new(label)).clicked() {
action = Some(crate::tab_context::Action::Reveal(id));
ui.close();
}
});
if response.clicked() {
app.active = Some(i);
}
}
if let Some(i) = close {
app.request_close_tab(i);
if let Some(i) = close
&& let Some(view) = app.views.get(i)
{
action.get_or_insert(crate::tab_context::Action::Close(view.id));
}
if let Some(action) = action {
app.tab_action(action);
}
if app.combine_tab.open {
// After the document tabs; its index can't clash with theirs.
+6
View File
@@ -2337,6 +2337,12 @@ animation effects Off معطّلة
Ink sensitivity حساسية الحبر
No signature was found in the frame. Move the frame over the signature or raise the ink sensitivity. لم يُعثر على توقيع داخل الإطار. حرّك الإطار فوق التوقيع أو ارفع حساسية الحبر.
Couldn't use the photo: {e} تعذّر استخدام الصورة: {e}
# PDF tab context menu (#1233)
Close Tab إغلاق علامة التبويب
Reopen Closed Tab إعادة فتح علامة التبويب المغلقة
Reveal in Finder إظهار في Finder
Reveal in File Explorer إظهار في مستكشف الملفات
Other colour… لون آخر…
Recent colours الألوان الأخيرة
Couldn't follow linked PDF: {reason} تعذّر فتح ملف PDF المرتبط: {reason}
Can't render this file because it contains an unexpected character in line 6 and column 47.
+6
View File
@@ -2279,6 +2279,12 @@ animation effects Off Изключени
Ink sensitivity Чувствителност към мастило
No signature was found in the frame. Move the frame over the signature or raise the ink sensitivity. В рамката не е открит подпис. Преместете рамката върху подписа или увеличете чувствителността към мастило.
Couldn't use the photo: {e} Снимката не може да се използва: {e}
# PDF tab context menu (#1233)
Close Tab Затваряне на раздела
Reopen Closed Tab Отваряне на затворения раздел
Reveal in Finder Показване във Finder
Reveal in File Explorer Показване във файловия мениджър
Other colour… Друг цвят…
Recent colours Последни цветове
Couldn't follow linked PDF: {reason} Свързаният PDF не можа да бъде отворен: {reason}
Can't render this file because it contains an unexpected character in line 8 and column 16.
+6
View File
@@ -2390,6 +2390,12 @@ animation effects Off Vypnuto
Ink sensitivity Citlivost na inkoust
No signature was found in the frame. Move the frame over the signature or raise the ink sensitivity. V rámečku nebyl nalezen žádný podpis. Přesuňte rámeček na podpis nebo zvyšte citlivost na inkoust.
Couldn't use the photo: {e} Fotografii nelze použít: {e}
# PDF tab context menu (#1233)
Close Tab Zavřít kartu
Reopen Closed Tab Znovu otevřít zavřenou kartu
Reveal in Finder Zobrazit ve Finderu
Reveal in File Explorer Zobrazit v Průzkumníku souborů
Other colour… Jiná barva…
Recent colours Nedávné barvy
Couldn't follow linked PDF: {reason} Odkazovaný PDF nelze otevřít: {reason}
Can't render this file because it contains an unexpected character in line 6 and column 47.
+6
View File
@@ -2317,6 +2317,12 @@ animation effects Off Aus
Ink sensitivity Tintenempfindlichkeit
No signature was found in the frame. Move the frame over the signature or raise the ink sensitivity. Im Rahmen wurde keine Unterschrift gefunden. Ziehen Sie den Rahmen über die Unterschrift oder erhöhen Sie die Tintenempfindlichkeit.
Couldn't use the photo: {e} Das Foto kann nicht verwendet werden: {e}
# PDF tab context menu (#1233)
Close Tab Tab schließen
Reopen Closed Tab Geschlossenen Tab wieder öffnen
Reveal in Finder Im Finder anzeigen
Reveal in File Explorer Im Datei-Explorer anzeigen
Other colour… Andere Farbe…
Recent colours Zuletzt verwendete Farben
Couldn't follow linked PDF: {reason} Verknüpfte PDF konnte nicht geöffnet werden: {reason}
Can't render this file because it contains an unexpected character in line 6 and column 51.
+6
View File
@@ -2321,6 +2321,12 @@ animation effects Off Ανενεργά
Ink sensitivity Ευαισθησία μελανιού
No signature was found in the frame. Move the frame over the signature or raise the ink sensitivity. Δεν βρέθηκε υπογραφή στο πλαίσιο. Μετακινήστε το πλαίσιο πάνω από την υπογραφή ή αυξήστε την ευαισθησία μελανιού.
Couldn't use the photo: {e} Δεν ήταν δυνατή η χρήση της φωτογραφίας: {e}
# PDF tab context menu (#1233)
Close Tab Κλείσιμο καρτέλας
Reopen Closed Tab Επαναφορά κλειστής καρτέλας
Reveal in Finder Εμφάνιση στο Finder
Reveal in File Explorer Εμφάνιση στη διαχείριση αρχείων
Other colour… Άλλο χρώμα…
Recent colours Πρόσφατα χρώματα
Reading colours Χρώματα ανάγνωσης
Can't render this file because it contains an unexpected character in line 479 and column 12.
+6
View File
@@ -2435,6 +2435,12 @@ animation effects Off Desactivados
Ink sensitivity Sensibilidad de la tinta
No signature was found in the frame. Move the frame over the signature or raise the ink sensitivity. No se encontró ninguna firma en el marco. Mueva el marco sobre la firma o aumente la sensibilidad de la tinta.
Couldn't use the photo: {e} No se puede usar la foto: {e}
# PDF tab context menu (#1233)
Close Tab Cerrar pestaña
Reopen Closed Tab Reabrir pestaña cerrada
Reveal in Finder Mostrar en Finder
Reveal in File Explorer Mostrar en el explorador de archivos
Other colour… Otro color…
Recent colours Colores recientes
Reading colours Colores de lectura
Can't render this file because it contains an unexpected character in line 6 and column 60.
+6
View File
@@ -2316,6 +2316,12 @@ animation effects Off Désactivés
Ink sensitivity Sensibilité à l’encre
No signature was found in the frame. Move the frame over the signature or raise the ink sensitivity. Aucune signature trouvée dans le cadre. Placez le cadre sur la signature ou augmentez la sensibilité à l’encre.
Couldn't use the photo: {e} Impossible d’utiliser la photo : {e}
# PDF tab context menu (#1233)
Close Tab Fermer l’onglet
Reopen Closed Tab Rouvrir l’onglet fermé
Reveal in Finder Afficher dans le Finder
Reveal in File Explorer Afficher dans l’explorateur de fichiers
Other colour… Autre couleur…
Recent colours Couleurs récentes
Couldn't follow linked PDF: {reason} Impossible de suivre le PDF lié : {reason}
Can't render this file because it contains an unexpected character in line 6 and column 39.
+6
View File
@@ -2315,6 +2315,12 @@ animation effects Off Ki
Ink sensitivity Tintaérzékenység
No signature was found in the frame. Move the frame over the signature or raise the ink sensitivity. A keretben nem található aláírás. Húzza a keretet az aláírásra, vagy növelje a tintaérzékenységet.
Couldn't use the photo: {e} A fénykép nem használható: {e}
# PDF tab context menu (#1233)
Close Tab Lap bezárása
Reopen Closed Tab Bezárt lap újranyitása
Reveal in Finder Megjelenítés a Finderben
Reveal in File Explorer Megjelenítés a fájlkezelőben
Other colour… Egyéb szín…
Recent colours Legutóbbi színek
Couldn't follow linked PDF: {reason} A hivatkozott PDF nem nyitható meg: {reason}
Can't render this file because it contains an unexpected character in line 7 and column 44.
+6
View File
@@ -2325,6 +2325,12 @@ animation effects Off Disattivati
Ink sensitivity Sensibilità all’inchiostro
No signature was found in the frame. Move the frame over the signature or raise the ink sensitivity. Nessuna firma trovata nella cornice. Sposta la cornice sulla firma o aumenta la sensibilità all’inchiostro.
Couldn't use the photo: {e} Impossibile usare la foto: {e}
# PDF tab context menu (#1233)
Close Tab Chiudi scheda
Reopen Closed Tab Riapri scheda chiusa
Reveal in Finder Mostra nel Finder
Reveal in File Explorer Mostra in Esplora file
Other colour… Altro colore…
Recent colours Colori recenti
Couldn't follow linked PDF: {reason} Impossibile aprire il PDF collegato: {reason}
Can't render this file because it contains an unexpected character in line 6 and column 47.
+6
View File
@@ -2316,6 +2316,12 @@ animation effects Off オフ
Ink sensitivity インクの感度
No signature was found in the frame. Move the frame over the signature or raise the ink sensitivity. 枠内に署名が見つかりません。枠を署名に合わせるか、インクの感度を上げてください。
Couldn't use the photo: {e} 写真を使用できません: {e}
# PDF tab context menu (#1233)
Close Tab タブを閉じる
Reopen Closed Tab 閉じたタブを再度開く
Reveal in Finder Finderで表示
Reveal in File Explorer ファイルエクスプローラーで表示
Other colour… その他の色…
Recent colours 最近使った色
Couldn't follow linked PDF: {reason} リンク先の PDF を開けませんでした: {reason}
Can't render this file because it contains an unexpected character in line 6 and column 47.
+6
View File
@@ -2304,6 +2304,12 @@ animation effects Off Өшірулі
Ink sensitivity Сия сезімталдығы
No signature was found in the frame. Move the frame over the signature or raise the ink sensitivity. Жақтауда қолтаңба табылмады. Жақтауды қолтаңбаның үстіне жылжытыңыз немесе сия сезімталдығын арттырыңыз.
Couldn't use the photo: {e} Суретті пайдалану мүмкін болмады: {e}
# PDF tab context menu (#1233)
Close Tab Қойындыны жабу
Reopen Closed Tab Жабылған қойындыны қайта ашу
Reveal in Finder Finder ішінде көрсету
Reveal in File Explorer Файл шолғышында көрсету
Other colour… Басқа түс…
Recent colours Соңғы түстер
Reading colours Оқу түстері
Can't render this file because it contains an unexpected character in line 820 and column 12.
+6
View File
@@ -2285,6 +2285,12 @@ animation effects Off Wyłączone
Ink sensitivity Czułość na atrament
No signature was found in the frame. Move the frame over the signature or raise the ink sensitivity. W ramce nie znaleziono podpisu. Przesuń ramkę na podpis lub zwiększ czułość na atrament.
Couldn't use the photo: {e} Nie można użyć zdjęcia: {e}
# PDF tab context menu (#1233)
Close Tab Zamknij kartę
Reopen Closed Tab Otwórz ponownie zamkniętą kartę
Reveal in Finder Pokaż w Finderze
Reveal in File Explorer Pokaż w Eksploratorze plików
Other colour… Inny kolor…
Recent colours Ostatnie kolory
Reading colours Kolory do czytania
Can't render this file because it contains an unexpected character in line 6 and column 47.
+6
View File
@@ -2330,6 +2330,12 @@ animation effects Off Desativados
Ink sensitivity Sensibilidade à tinta
No signature was found in the frame. Move the frame over the signature or raise the ink sensitivity. Nenhuma assinatura encontrada no quadro. Mova o quadro sobre a assinatura ou aumente a sensibilidade à tinta.
Couldn't use the photo: {e} Não foi possível usar a foto: {e}
# PDF tab context menu (#1233)
Close Tab Fechar aba
Reopen Closed Tab Reabrir aba fechada
Reveal in Finder Mostrar no Finder
Reveal in File Explorer Mostrar no explorador de arquivos
Other colour… Outra cor…
Recent colours Cores recentes
Couldn't follow linked PDF: {reason} Não foi possível abrir o PDF vinculado: {reason}
Can't render this file because it contains an unexpected character in line 9 and column 16.
+6
View File
@@ -2229,6 +2229,12 @@ animation effects Off Выкл
Ink sensitivity Чувствительность к чернилам
No signature was found in the frame. Move the frame over the signature or raise the ink sensitivity. В рамке не найдена подпись. Наведите рамку на подпись или увеличьте чувствительность к чернилам.
Couldn't use the photo: {e} Не удалось использовать фото: {e}
# PDF tab context menu (#1233)
Close Tab Закрыть вкладку
Reopen Closed Tab Открыть закрытую вкладку
Reveal in Finder Показать в Finder
Reveal in File Explorer Показать в проводнике
Other colour… Другой цвет…
Recent colours Недавние цвета
Couldn't follow linked PDF: {reason} Не удалось открыть связанный PDF: {reason}
Can't render this file because it contains an unexpected character in line 9 and column 16.
+6
View File
@@ -2321,6 +2321,12 @@ animation effects Off Izklopljeni
Ink sensitivity Občutljivost za črnilo
No signature was found in the frame. Move the frame over the signature or raise the ink sensitivity. V okvirju ni bilo najdenega podpisa. Premaknite okvir nad podpis ali povečajte občutljivost za črnilo.
Couldn't use the photo: {e} Fotografije ni bilo mogoče uporabiti: {e}
# PDF tab context menu (#1233)
Close Tab Zapri zavihek
Reopen Closed Tab Znova odpri zaprti zavihek
Reveal in Finder Prikaži v Finderju
Reveal in File Explorer Prikaži v upravljalniku datotek
Other colour… Druga barva…
Recent colours Nedavne barve
Couldn't follow linked PDF: {reason} Povezanega PDF-ja ni bilo mogoče odpreti: {reason}
Can't render this file because it contains an unexpected character in line 9 and column 16.
+6
View File
@@ -2218,6 +2218,12 @@ animation effects Off ఆఫ్
Ink sensitivity ఇంక్ సున్నితత్వం
No signature was found in the frame. Move the frame over the signature or raise the ink sensitivity. ఫ్రేమ్‌లో సంతకం కనబడలేదు. ఫ్రేమ్‌ను సంతకం మీదకు జరపండి లేదా ఇంక్ సున్నితత్వాన్ని పెంచండి.
Couldn't use the photo: {e} ఫోటోను ఉపయోగించలేకపోయాం: {e}
# PDF tab context menu (#1233)
Close Tab ట్యాబ్‌ను మూసివేయి
Reopen Closed Tab మూసివేసిన ట్యాబ్‌ను మళ్లీ తెరువు
Reveal in Finder Finderలో చూపు
Reveal in File Explorer ఫైల్ ఎక్స్‌ప్లోరర్‌లో చూపు
Other colour… ఇతర రంగు…
Recent colours ఇటీవలి రంగులు
Reading colours చదవడానికి రంగులు
Can't render this file because it contains an unexpected character in line 13 and column 16.
+6
View File
@@ -2302,6 +2302,12 @@ animation effects Off Kapalı
Ink sensitivity Mürekkep hassasiyeti
No signature was found in the frame. Move the frame over the signature or raise the ink sensitivity. Çerçevede imza bulunamadı. Çerçeveyi imzanın üzerine taşıyın veya mürekkep hassasiyetini artırın.
Couldn't use the photo: {e} Fotoğraf kullanılamadı: {e}
# PDF tab context menu (#1233)
Close Tab Sekmeyi kapat
Reopen Closed Tab Kapatılan sekmeyi yeniden aç
Reveal in Finder Finder’da göster
Reveal in File Explorer Dosya Gezgini’nde göster
Other colour… Diğer renk…
Recent colours Son kullanılan renkler
Reading colours Okuma renkleri
Can't render this file because it contains an unexpected character in line 819 and column 12.
+6
View File
@@ -2338,6 +2338,12 @@ animation effects Off Вимкнено
Ink sensitivity Чутливість до чорнила
No signature was found in the frame. Move the frame over the signature or raise the ink sensitivity. У рамці не знайдено підпису. Наведіть рамку на підпис або збільште чутливість до чорнила.
Couldn't use the photo: {e} Не вдалося використати фото: {e}
# PDF tab context menu (#1233)
Close Tab Закрити вкладку
Reopen Closed Tab Відкрити закриту вкладку
Reveal in Finder Показати у Finder
Reveal in File Explorer Показати у файловому провіднику
Other colour… Інший колір…
Recent colours Нещодавні кольори
Couldn't follow linked PDF: {reason} Не вдалося відкрити пов’язаний PDF: {reason}
Can't render this file because it contains an unexpected character in line 6 and column 47.
+6
View File
@@ -2280,6 +2280,12 @@ animation effects Off 关
Ink sensitivity 墨迹灵敏度
No signature was found in the frame. Move the frame over the signature or raise the ink sensitivity. 框内未找到签名。请将框移到签名上,或提高墨迹灵敏度。
Couldn't use the photo: {e} 无法使用该照片:{e}
# PDF tab context menu (#1233)
Close Tab 关闭标签页
Reopen Closed Tab 重新打开已关闭的标签页
Reveal in Finder 在 Finder 中显示
Reveal in File Explorer 在文件资源管理器中显示
Other colour… 其他颜色…
Recent colours 最近使用的颜色
Couldn't follow linked PDF: {reason} 无法打开链接的 PDF:{reason}
Can't render this file because it contains an unexpected character in line 9 and column 16.
+6
View File
@@ -2179,6 +2179,12 @@ animation effects Off 關
Ink sensitivity 墨跡靈敏度
No signature was found in the frame. Move the frame over the signature or raise the ink sensitivity. 框內找不到簽名。請將框移到簽名上,或提高墨跡靈敏度。
Couldn't use the photo: {e} 無法使用此相片:{e}
# PDF tab context menu (#1233)
Close Tab 關閉分頁
Reopen Closed Tab 重新開啟已關閉的分頁
Reveal in Finder 在 Finder 中顯示
Reveal in File Explorer 在檔案總管中顯示
Other colour… 其他顏色…
Recent colours 最近使用的顏色
Reading colours 閱讀色彩
Can't render this file because it contains an unexpected character in line 9 and column 16.
+10
View File
@@ -122,6 +122,7 @@ mod recovery;
pub mod scroll_speed;
#[cfg(not(target_arch = "wasm32"))]
mod system_fonts;
mod tab_context;
pub mod theme;
pub mod ui_scale;
pub mod updates;
@@ -434,6 +435,7 @@ pub struct PdfCraftApp {
pub(crate) pending_panel_reveals: Vec<docking::Panel>,
pub session: Session,
pub views: Vec<DocView>,
pub(crate) closed_tabs: Vec<String>,
/// `None` shows the Home tab.
pub active: Option<usize>,
pub mode: Mode,
@@ -784,6 +786,7 @@ impl PdfCraftApp {
Self {
session: Session::new(),
views: Vec::new(),
closed_tabs: Vec::new(),
active: None,
mode: Mode::AllTools,
default_mode: Mode::AllTools,
@@ -1430,6 +1433,13 @@ impl PdfCraftApp {
return;
}
let id = self.views.remove(index).id;
#[cfg(not(target_arch = "wasm32"))]
if let Some(path) = self.session.get(id).and_then(|d| d.path.clone()) {
if self.closed_tabs.len() == 10 {
self.closed_tabs.remove(0);
}
self.closed_tabs.push(path);
}
self.forget_recovery(id);
self.session.close(id);
self.active = match self.active {
+275
View File
@@ -0,0 +1,275 @@
//! PDF-tab actions. Closed history contains paths only, never bytes or passwords.
use pdfcraft_engine::DocId;
pub(crate) enum Action {
Close(DocId),
Reopen,
Reveal(DocId),
}
impl crate::PdfCraftApp {
pub(crate) fn tab_action(&mut self, action: Action) {
match action {
Action::Close(id) => {
if let Some(i) = self.views.iter().position(|v| v.id == id) {
self.request_close_tab(i);
}
}
Action::Reopen => {
#[cfg(not(target_arch = "wasm32"))]
if let Some(path) = self.closed_tabs.last().cloned() {
// A tab may already have been reopened through File > Open.
if let Some(index) = self.views.iter().position(|v| self.session.get(v.id).is_some_and(|d| d.path.as_deref() == Some(&path))) {
self.active = Some(index);
self.closed_tabs.pop();
} else {
let tabs = self.views.len();
self.open_path(&path);
// open_path reports errors through notifications, not a Result.
// Keep the entry until an actual document tab has been opened.
if self.views.len() > tabs {
self.closed_tabs.pop();
}
}
}
}
Action::Reveal(id) => {
#[cfg(not(target_arch = "wasm32"))]
if let Some(path) = self.session.get(id).and_then(|d| d.path.clone())
&& let Err(e) = reveal(std::path::Path::new(&path))
{
self.notify_error(e);
}
#[cfg(target_arch = "wasm32")]
let _ = id;
}
}
}
}
#[cfg(not(target_arch = "wasm32"))]
fn reveal(path: &std::path::Path) -> Result<(), String> {
// Absolute paths also prevent a filename starting with '-' becoming an option.
let path = std::path::absolute(path).map_err(|e| e.to_string())?;
if !path.try_exists().map_err(|e| e.to_string())? {
return Err("The file no longer exists".into());
}
let platform = std::env::consts::OS;
let run = |program: &str, args: &[std::ffi::OsString]| {
std::process::Command::new(program).args(args).output().map(|o| o.status.success()).map_err(|e| e.to_string())
};
if matches!(platform, "linux" | "freebsd") {
// `dbus-send --print-reply` waits up to 2 s for the file manager (then xdg-open may run),
// so it runs off the UI thread; a failure there is only logged.
let spawned = std::thread::Builder::new().name("pdfcraft-reveal".into()).spawn(move || {
if let Err(e) = reveal_via(platform, &path, run) {
log::warn!("reveal {}: {e}", path.display());
}
});
return spawned.map(|_| ()).map_err(|e| e.to_string());
}
reveal_via(platform, &path, run)
}
#[cfg(not(target_arch = "wasm32"))]
fn reveal_via(
platform: &str,
path: &std::path::Path,
mut run: impl FnMut(&str, &[std::ffi::OsString]) -> Result<bool, String>,
) -> Result<(), String> {
let (program, args) = reveal_command(platform, path)?;
if run(program, &args).unwrap_or(false) {
return Ok(());
}
if matches!(platform, "linux" | "freebsd") {
// FileManager1 is optional. If absent, open the containing folder with xdg-open.
let parent = path.parent().ok_or("The file has no parent directory")?;
if run("xdg-open", &[parent.as_os_str().into()])? {
return Ok(());
}
}
Err("Could not reveal the file in the system file manager".into())
}
#[cfg(not(target_arch = "wasm32"))]
fn reveal_command(platform: &str, path: &std::path::Path) -> Result<(&'static str, Vec<std::ffi::OsString>), String> {
match platform {
"windows" => {
// Explorer uses /select,<object>, rather than a separate option value.
let mut selection = std::ffi::OsString::from("/select,");
selection.push(path);
Ok(("explorer.exe", vec![selection]))
}
"macos" => Ok(("open", vec!["-R".into(), path.as_os_str().into()])),
"linux" | "freebsd" => {
let mut uri = String::from("array:string:file://");
for b in path.as_os_str().as_encoded_bytes() {
if b.is_ascii_alphanumeric() || matches!(b, b'/' | b'-' | b'_' | b'.' | b'~') {
uri.push(char::from(*b));
} else {
use std::fmt::Write as _;
let _ = write!(uri, "%{b:02X}");
}
}
Ok((
"dbus-send",
vec![
"--session".into(),
"--print-reply".into(),
"--reply-timeout=2000".into(),
"--dest=org.freedesktop.FileManager1".into(),
"/org/freedesktop/FileManager1".into(),
"org.freedesktop.FileManager1.ShowItems".into(),
uri.into(),
"string:".into(),
],
))
}
_ => Err("File reveal is not available on this platform".into()),
}
}
#[cfg(all(test, not(target_arch = "wasm32")))]
mod tests {
use super::*;
const PDF: &[u8] = include_bytes!("../tests/data/form.pdf");
#[test]
fn confirmed_close_uses_id_and_cancel_keeps_history_empty() {
let mut app = crate::PdfCraftApp::new();
app.open_bytes("first.pdf", Some("/tmp/first.pdf".into()), PDF.to_vec()).unwrap();
let first = app.views[0].id;
app.open_bytes("second.pdf", None, PDF.to_vec()).unwrap();
app.session.apply(first, pdfcraft_engine::Edit::InsertBlankPage { at: 0, width: 300.0, height: 400.0 }).unwrap();
app.tab_action(Action::Close(first));
assert_eq!(app.views.len(), 2);
assert!(app.closed_tabs.is_empty());
app.resolve_close(&egui::Context::default(), None);
assert!(app.closed_tabs.is_empty());
app.tab_action(Action::Close(first));
app.resolve_close(&egui::Context::default(), Some(false));
assert_eq!(app.views.len(), 1);
assert_eq!(app.closed_tabs, ["/tmp/first.pdf"]);
assert_eq!(app.active, Some(0));
app.close_tab(0);
assert_eq!(app.closed_tabs.len(), 1, "pathless tabs are not recorded");
}
#[test]
fn reopen_reads_saved_file_and_history_is_bounded() {
use std::io::Write as _;
let nonce = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_nanos();
let path = std::env::temp_dir().join(format!("pdfcraft-tab-{}-{nonce}-年度 report.pdf", std::process::id()));
let mut file = std::fs::OpenOptions::new().write(true).create_new(true).open(&path).unwrap();
file.write_all(PDF).unwrap();
drop(file);
let mut app = crate::PdfCraftApp::new();
let path_text = path.to_str().unwrap();
for _ in 0..12 {
app.open_path(path_text);
app.close_tab(0);
}
assert_eq!(app.closed_tabs.len(), 10);
app.tab_action(Action::Reopen);
assert_eq!(app.views.len(), 1);
let doc = app.session.get(app.views[0].id).unwrap();
assert_eq!(doc.path.as_deref(), Some(path_text));
assert!(!doc.dirty);
assert_eq!(app.closed_tabs.len(), 9);
std::fs::remove_file(path).unwrap();
}
#[test]
fn failed_reopen_keeps_history_and_retry_does_not_duplicate_tabs() {
use std::io::Write as _;
let nonce = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_nanos();
let path = std::env::temp_dir().join(format!("pdfcraft-retry-{}-{nonce}.pdf", std::process::id()));
let text = path.to_str().unwrap();
let mut file = std::fs::OpenOptions::new().create_new(true).write(true).open(&path).unwrap();
file.write_all(PDF).unwrap();
drop(file);
let mut app = crate::PdfCraftApp::new();
app.open_path(text);
app.close_tab(0);
std::fs::remove_file(&path).unwrap();
app.tab_action(Action::Reopen);
assert!(app.views.is_empty());
assert_eq!(app.closed_tabs, [text]);
std::fs::write(&path, PDF).unwrap();
app.tab_action(Action::Reopen);
assert_eq!(app.views.len(), 1);
assert!(app.closed_tabs.is_empty());
app.closed_tabs.push(text.into());
app.tab_action(Action::Reopen);
assert_eq!(app.views.len(), 1, "an already-open file is selected, not duplicated");
assert!(app.closed_tabs.is_empty());
std::fs::remove_file(path).unwrap();
}
#[test]
fn unavailable_file_manager_falls_back_safely_and_reports_failure() {
let path = std::path::Path::new("/tmp/年度 & reports/file.pdf");
let mut calls = Vec::new();
reveal_via("linux", path, |program, args| {
calls.push((program.to_string(), args.to_vec()));
if program == "dbus-send" { Err("FileManager1 unavailable".into()) } else { Ok(true) }
})
.unwrap();
assert_eq!(calls.len(), 2);
assert_eq!(calls[1], ("xdg-open".into(), vec![std::ffi::OsString::from("/tmp/年度 & reports")]));
assert!(reveal_via("linux", path, |_, _| Ok(false)).is_err());
assert!(reveal_via("linux", path, |_, _| Err("command missing".into())).is_err());
}
#[test]
fn reveal_arguments_preserve_spaces_and_unicode() {
let path = std::path::Path::new("/tmp/年度 report.pdf");
let (exe, args) = reveal_command("macos", path).unwrap();
assert_eq!(exe, "open");
assert_eq!(args, [std::ffi::OsString::from("-R"), path.as_os_str().into()]);
let (exe, args) = reveal_command("windows", path).unwrap();
assert_eq!(exe, "explorer.exe");
assert_eq!(args, [std::ffi::OsString::from("/select,/tmp/年度 report.pdf")]);
let special = std::path::Path::new(r"C:\Reports\年度 report, #1 & (draft).pdf");
let (_, windows) = reveal_command("windows", special).unwrap();
assert_eq!(windows, [std::ffi::OsString::from(r"/select,C:\Reports\年度 report, #1 & (draft).pdf")]);
let (exe, args) = reveal_command("linux", path).unwrap();
assert_eq!(exe, "dbus-send");
assert_eq!(args[6], "array:string:file:///tmp/%E5%B9%B4%E5%BA%A6%20report.pdf");
let (_, args) = reveal_command("linux", std::path::Path::new("/tmp/年度 report,#&'$.pdf")).unwrap();
assert_eq!(args[6], "array:string:file:///tmp/%E5%B9%B4%E5%BA%A6%20report%2C%23%26%27%24.pdf");
assert_eq!(args[5], "org.freedesktop.FileManager1.ShowItems");
assert_eq!(args[7], "string:");
}
}
#[cfg(all(test, not(target_arch = "wasm32")))]
mod ui_tests {
use crate::PdfCraftApp;
use egui_kittest::{
Harness,
kittest::{NodeT, Queryable},
};
#[test]
fn inactive_tab_menu_closes_target_and_disables_unavailable_actions() {
let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(|_| {
let mut app = PdfCraftApp::new();
app.set_option("language", "en").unwrap();
for name in ["first.pdf", "second.pdf"] {
app.open_bytes(name, None, include_bytes!("../tests/data/form.pdf").to_vec()).unwrap();
}
app
});
h.run_steps(8);
h.get_by_role_and_label(egui::accesskit::Role::Tab, "first.pdf").click_secondary();
h.run_steps(2);
assert!(h.get_by_label("Reopen Closed Tab").accesskit_node().is_disabled());
assert!(h.get_by_label("Reveal in File Explorer").accesskit_node().is_disabled());
assert_eq!(h.state().active, Some(1));
h.get_by_label("Close Tab").click();
h.run_steps(3);
assert_eq!(h.state().views.len(), 1);
assert_eq!(h.state().session.get(h.state().views[0].id).unwrap().name, "second.pdf");
}
}