M3: restore last view settings when reopening documents (#1767)

A file opened again shows the page, zoom and page display it was last
shown with, as Acrobat's "Restore last view settings when reopening
documents" does: remembered by path (the last 100 files) when its tab
closes or settings are saved with it open, applied over the file's own
initial view, on by default and switched off in Preferences ▸ Documents
and view.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
mtlcl410
2026-10-24 10:09:47 +08:00
co-authored by Claude Fable 5.1
parent a2ec27b477
commit 946fb31eef
27 changed files with 248 additions and 1 deletions
+2
View File
@@ -348,6 +348,8 @@ Ranked; detail and the full milestone table in [docs/roadmap.md](docs/roadmap.md
- **2026-10-15 (community, M14, Polish signature panel):** A Polish signature panel listed its details in English: the catalog lacked every signature wording, the changes after signing they name and 34 UI literals. All 101 are translated; `polish_covers_ui_literals` and `polish_says_what_a_signature_says` keep them so. Estimates unchanged.
- **2026-10-14 (M14, Finnish localization):** Added Finnish as another interface and date language, with a 2,674-entry catalog covering every key of the other bundled catalogs and two-form plurals (partitive after numbers other than 1). Calendar dates use partitive month names (`14. lokakuuta 2026`). Coverage tests check commands/tools, scanned UI literals and bundled catalog keys. AI-assisted; native proofreading pending.
- **2026-10-14 (M14, Catalan localization):** Added Catalan as another interface and date language, with a 2,592-entry clean-room catalog, one/other plurals and coverage tests for commands, tools, UI literals and every key in the other catalogs. A native Catalan speaker (the contributor) has reviewed the translation. Estimates unchanged.
- **2026-10-15 (M3, #1767):** Restore last view settings when reopening documents, as Acrobat's preference does: a file opened again shows the page, zoom and page display it was last shown with (remembered for the last 100 files when a tab closes or settings are saved), over the file's own initial view; on by default, Preferences ▸ Documents and view turns it off.
- **2026-10-13 (M3, native-opening candidate):** Chosen tools survive preparation, conversion and password retries; startup intake waits for pending preparation. Regression tests cover completion, cancellation and document identity. Estimates unchanged.
- **2026-10-13 (community, M3, #531 touchpad momentum):** On Wayland a two-finger swipe keeps gliding after the fingers lift and slows to a stop, measured from the gesture's last 0.1 s and decaying with a 0.35 s time constant through the #759 1:1 path. Any wheel input, click, key, zoom or edge ends it (fingers resting on the touchpad don't yet: the vendored winit binds no Wayland hold gestures); single-page paging, macOS (which sends its own momentum), the web, and Animation effects Off never glide. Unit tests for the physics and two headless view tests. Estimates unchanged.
- **2026-10-13 (M3, native-open admission):** Later prepared replies stay queued while a modal or native picker owns the foreground. Picker completion, cancellation and abandonment wake retained opens. Regressions cover recipient ownership, exactly-once admission, source/history preservation and one-shot repaint requests. Estimates unchanged.
+1
View File
@@ -2193,6 +2193,7 @@ calendar date October أكتوبر
calendar date November نوفمبر
calendar date December ديسمبر
Reopen the files that were open when PdfCraft last closed إعادة فتح الملفات التي كانت مفتوحة عند آخر إغلاق لـ PdfCraft
Restore the last view (page, zoom and page display) when a document is opened again استعادة آخر عرض (الصفحة والتكبير وعرض الصفحات) عند فتح المستند مرة أخرى
Clear Recent Files مسح الملفات الأخيرة
Remove from Recent إزالة من الأخيرة
Remove {name} from Recent إزالة {name} من الأخيرة
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -2178,6 +2178,7 @@ signature pad Type Въвеждане
action wizard Start Старт
Margins (in) Полета (in)
Reopen the files that were open when PdfCraft last closed Отваряй отново файловете, които бяха отворени при последното затваряне на PdfCraft
Restore the last view (page, zoom and page display) when a document is opened again Възстановяване на последния изглед (страница, мащаб и показване на страниците) при повторно отваряне на документ
Clear Recent Files Изчисти последно отваряните файлове
Remove from Recent Премахни от последно отваряните
Remove {name} from Recent Премахни {name} от последно отваряните
Can't render this file because it contains an unexpected character in line 8 and column 16.
+1
View File
@@ -139,6 +139,7 @@
Fit one full page Celá jedna stránka
Used when a PDF doesn't ask for a layout or zoom. Continuous scrolling never snaps between pages. Používá se, když PDF nepožaduje rozložení ani zvětšení. Plynulé posouvání mezi stránkami neskáče.
Reopen the files that were open when PdfCraft last closed Znovu otevřít soubory, které byly otevřené při posledním zavření aplikace PdfCraft
Restore the last view (page, zoom and page display) when a document is opened again Při opětovném otevření dokumentu obnovit poslední zobrazení (stránku, zvětšení a rozložení stránek)
Reload documents when their files change on disk Znovu načítat dokumenty při změně jejich souborů na disku
Always use my default zoom and page layout Vždy používat mé výchozí zvětšení a rozvržení stránky
This file changed on disk. Reload shows the new version and drops your unsaved changes; Save replaces it with this version. Tento soubor se na disku změnil. Znovu načíst zobrazí novou verzi a zahodí neuložené změny; Uložit ji nahradí touto verzí.
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -2186,6 +2186,7 @@ signature pad Type Tippen
certificate Subject Zertifikatsinhaber
action wizard Start Starten
Reopen the files that were open when PdfCraft last closed Beim Start die Dateien wieder öffnen, die beim letzten Beenden von PdfCraft geöffnet waren
Restore the last view (page, zoom and page display) when a document is opened again Beim erneuten Öffnen eines Dokuments die letzte Ansicht (Seite, Zoom und Seitenanzeige) wiederherstellen
# Labels that reach tl!() through a variable (Fill & Sign buttons, link prompts, notices, history labels, checker and certificate rows)
Add {what} {what} hinzufügen
Can't render this file because it contains an unexpected character in line 6 and column 51.
+1
View File
@@ -1467,6 +1467,7 @@
Couldn't import the signature image: {e} Δεν ήταν δυνατή η εισαγωγή της εικόνας υπογραφής: {e}
Go to page… Μετάβαση στη σελίδα…
Reopen the files that were open when PdfCraft last closed Εκ νέου άνοιγμα των αρχείων που ήταν ανοιχτά κατά τον τελευταίο τερματισμό του PdfCraft
Restore the last view (page, zoom and page display) when a document is opened again Επαναφορά της τελευταίας προβολής (σελίδα, ζουμ και εμφάνιση σελίδων) όταν ένα έγγραφο ανοίγει ξανά
The PDF text fonts can't draw “{c}” ({code}). Choose supported text. Οι γραμματοσειρές κειμένου PDF δεν μπορούν να εμφανίσουν τον χαρακτήρα «{c}» ({code}). Επιλέξτε κείμενο που υποστηρίζεται.
Open arrow Ανοιχτή αιχμή βέλους
Closed arrow Κλειστή αιχμή βέλους
Can't render this file because it contains an unexpected character in line 482 and column 9.
+1
View File
@@ -2160,6 +2160,7 @@ action wizard Start Iniciar
Lines +/− Líneas +/−
@{login}: {prs} PRs, {commits} commits, +{added} / −{deleted} lines (Δ {delta}), +{bin_added} / −{bin_deleted} binary assets, {first} – {last} @{login}: {prs} PR, {commits} commits, +{added} / −{deleted} líneas (Δ {delta}), +{bin_added} / −{bin_deleted} recursos binarios, {first} – {last}
Reopen the files that were open when PdfCraft last closed Volver a abrir los archivos que estaban abiertos al cerrar PdfCraft por última vez
Restore the last view (page, zoom and page display) when a document is opened again Restaurar la última vista (página, zoom y presentación de páginas) al volver a abrir un documento
Add {what} Añadir {what}
Use {what} Usar {what}
Place saved {what} Colocar {what} desde lo guardado
Can't render this file because it contains an unexpected character in line 6 and column 60.
+1
View File
@@ -2041,6 +2041,7 @@
Default zoom Zoom par défaut
Used when a PDF doesn't ask for a layout or zoom. Continuous scrolling never snaps between pages. Utilisé lorsqu'un PDF ne demande ni présentation ni zoom. Le défilement continu ne saute jamais d'une page à l'autre.
Reopen the files that were open when PdfCraft last closed Rouvrir les fichiers qui étaient ouverts lors de la dernière fermeture de PdfCraft
Restore the last view (page, zoom and page display) when a document is opened again Rétablir la dernière vue (page, zoom et affichage des pages) à la réouverture d’un document
Reload documents when their files change on disk Recharger les documents lorsque leurs fichiers sont modifiés sur le disque
Always use my default zoom and page layout Toujours utiliser mon zoom et ma présentation de page par défaut
This file changed on disk. Reload shows the new version and drops your unsaved changes; Save replaces it with this version. Ce fichier a été modifié sur le disque. Recharger affiche la nouvelle version et abandonne vos modifications non enregistrées ; Enregistrer la remplace par cette version.
Can't render this file because it contains an unexpected character in line 6 and column 39.
+1
View File
@@ -2170,6 +2170,7 @@ comment menu Edit Szerkesztés
signature pad Type Gépelés
action wizard Start Indítás
Reopen the files that were open when PdfCraft last closed A PdfCraft legutóbbi bezárásakor megnyitott fájlok újranyitása
Restore the last view (page, zoom and page display) when a document is opened again A legutóbbi nézet (oldal, nagyítás és oldalmegjelenítés) visszaállítása a dokumentum újbóli megnyitásakor
Clear Recent Files Legutóbbi fájlok listájának törlése
Remove from Recent Eltávolítás a legutóbbiak közül
Remove {name} from Recent {name} eltávolítása a legutóbbiak közül
Can't render this file because it contains an unexpected character in line 7 and column 44.
+1
View File
@@ -2208,6 +2208,7 @@
Remove saved {what} Rimuovi l'elemento salvato: {what}
Remove {name} from Recent Rimuovi {name} dai recenti
Reopen the files that were open when PdfCraft last closed Riapri i file aperti alla chiusura precedente di PdfCraft
Restore the last view (page, zoom and page display) when a document is opened again Ripristina l’ultima vista (pagina, zoom e visualizzazione delle pagine) quando un documento viene riaperto
Reset page size Ripristina dimensione pagine
Reset to printer defaults Ripristina le impostazioni predefinite della stampante
Reverse closed arrow Freccia chiusa inversa
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -2106,6 +2106,7 @@
Go to page… ページへ移動…
For page sizes and margins. ページサイズと余白に使います。
Reopen the files that were open when PdfCraft last closed 前回 PdfCraft を閉じたときに開いていたファイルを再度開く
Restore the last view (page, zoom and page display) when a document is opened again 文書を再び開くときに最後の表示(ページ、ズーム、ページ表示)を復元する
The PDF text fonts can't draw “{c}” ({code}). Choose supported text. PDF テキスト用フォントでは「{c}」({code})を表示できません。対応している文字を使用してください。
The standard fonts can't draw “{c}” ({code}). Only Western European characters can be added as text for now. 標準フォントでは「{c}」({code})を表示できません。現在、テキストとして追加できるのは西ヨーロッパの文字のみです。
This field's font can't draw “{c}” ({code}), so PdfCraft shows ? in its place. The value is saved, and other PDF viewers redraw the field from it. このフィールドのフォントでは「{c}」({code})を表示できないため、PdfCraft では ? と表示されます。値は保存され、他の PDF ビューアーではこの値からフィールドが再描画されます。
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -2113,6 +2113,7 @@ calendar date October Қазан
calendar date November Қараша
calendar date December Желтоқсан
Reopen the files that were open when PdfCraft last closed PdfCraft соңғы рет жабылғанда ашық болған файлдарды қайта ашу
Restore the last view (page, zoom and page display) when a document is opened again Құжат қайта ашылғанда соңғы көріністі (бет, масштаб және бет көрсетілімі) қалпына келтіру
Clear Recent Files Соңғы файлдарды тазарту
Remove from Recent Соңғылардан алып тастау
Remove {name} from Recent {name} дегенді соңғылардан алып тастау
Can't render this file because it contains an unexpected character in line 859 and column 9.
+1
View File
@@ -987,6 +987,7 @@
Remove {n} pages Pašalinti puslapių: {n}
Reopen Closed Tab Vėl atverti užvertą kortelę
Reopen the files that were open when PdfCraft last closed Vėl atverti failus, kurie buvo atverti paskutinį kartą užveriant „PdfCraft“
Restore the last view (page, zoom and page display) when a document is opened again Iš naujo atidarant dokumentą atkurti paskutinį rodinį (puslapį, mastelį ir puslapių rodymą)
Repair log Taisymo žurnalas
Repeat overlay text Kartoti perdangos tekstą
Replace Image… Pakeisti paveikslėlį…
Can't render this file because it contains an unexpected character in line 406 and column 9.
+1
View File
@@ -2085,6 +2085,7 @@
Couldn't import the signature image: {e} Nie można zaimportować obrazu podpisu: {e}
Go to page… Przejdź do strony…
Reopen the files that were open when PdfCraft last closed Otwórz ponownie pliki otwarte przy ostatnim zamknięciu PdfCraft
Restore the last view (page, zoom and page display) when a document is opened again Przywracaj ostatni widok (stronę, powiększenie i układ stron) przy ponownym otwarciu dokumentu
The PDF text fonts can't draw “{c}” ({code}). Choose supported text. Czcionki tekstu PDF nie obsługują znaku „{c}” ({code}). Wybierz obsługiwany tekst.
The standard fonts can't draw “{c}” ({code}). Only Western European characters can be added as text for now. Standardowe czcionki nie obsługują znaku „{c}” ({code}). Na razie można dodawać tylko tekst ze znakami zachodnioeuropejskimi.
This field's font can't draw “{c}” ({code}), so PdfCraft shows ? in its place. The value is saved, and other PDF viewers redraw the field from it. Czcionka tego pola nie obsługuje znaku „{c}” ({code}), więc PdfCraft wyświetla w jego miejscu ?. Wartość została zapisana, a inne przeglądarki PDF odrysują pole na jej podstawie.
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -81,6 +81,7 @@
PdfCraft is drawing without a graphics processor, so it may be slow. Updating the graphics driver may help. O PdfCraft está desenhando sem processador gráfico, por isso pode ficar lento. Atualizar o driver de vídeo pode ajudar.
Go to page… Ir para a página…
Reopen the files that were open when PdfCraft last closed Reabrir os arquivos que estavam abertos quando o PdfCraft foi fechado pela última vez
Restore the last view (page, zoom and page display) when a document is opened again Restaurar a última visualização (página, zoom e exibição de páginas) ao reabrir um documento
Margins Margens
For page sizes and margins. Para tamanhos de página e margens.
Open Abrir
Can't render this file because it contains an unexpected character in line 9 and column 16.
+1
View File
@@ -2108,6 +2108,7 @@
Default zoom Масштаб по умолчанию
Used when a PDF doesn't ask for a layout or zoom. Continuous scrolling never snaps between pages. Используется, когда PDF не запрашивает макет или масштаб. Непрерывная прокрутка никогда не привязывается между страницами.
Reopen the files that were open when PdfCraft last closed Повторно открывать файлы, которые были открыты при последнем закрытии PdfCraft
Restore the last view (page, zoom and page display) when a document is opened again Восстанавливать последний вид (страницу, масштаб и показ страниц) при повторном открытии документа
Reload documents when their files change on disk Перезагружать документы при изменении их файлов на диске
Always use my default zoom and page layout Всегда использовать мой масштаб и макет страницы по умолчанию
This file changed on disk. Reload shows the new version and drops your unsaved changes; Save replaces it with this version. Этот файл был изменён на диске. «Перезагрузить» покажет новую версию и отбросит несохранённые изменения; «Сохранить» заменит её этой версией.
Can't render this file because it contains an unexpected character in line 9 and column 16.
+1
View File
@@ -1506,6 +1506,7 @@
Rename bookmark Preimenuj zaznamek
Rendering: hayro (bootstrap) · UI: egui · Icons: Lucide (ISC) · Fonts: Inter, JetBrains Mono, Dancing Script (OFL) Izris: hayro (bootstrap) · Vmesnik: egui · Ikone: Lucide (ISC) · Pisave: Inter, JetBrains Mono, Dancing Script (OFL)
Reopen the files that were open when PdfCraft last closed Ponovno odpri datoteke, ki so bile odprte ob zadnjem zaprtju PdfCrafta
Restore the last view (page, zoom and page display) when a document is opened again Ob ponovnem odprtju dokumenta obnovi zadnji pogled (stran, povečavo in prikaz strani)
Repair log Dnevnik popravil
Repairs Popravila
Repeat overlay text Ponovi besedilo prekrivanja
Can't render this file because it contains an unexpected character in line 9 and column 16.
+1
View File
@@ -2027,6 +2027,7 @@ action wizard Start ప్రారంభించు
Default zoom డిఫాల్ట్ జూమ్
Used when a PDF doesn't ask for a layout or zoom. Continuous scrolling never snaps between pages. PDF ఏ లేఅవుట్‌ను గానీ జూమ్‌ను గానీ కోరనప్పుడు ఉపయోగించబడుతుంది. నిరంతర స్క్రోలింగ్ పేజీల మధ్య ఎప్పుడూ స్నాప్ అవ్వదు.
Reopen the files that were open when PdfCraft last closed PdfCraftను చివరిసారి మూసివేసినప్పుడు తెరిచి ఉన్న ఫైల్‌లను మళ్లీ తెరువు
Restore the last view (page, zoom and page display) when a document is opened again పత్రాన్ని మళ్లీ తెరిచినప్పుడు చివరి వీక్షణను (పేజీ, జూమ్ మరియు పేజీ ప్రదర్శన) పునరుద్ధరించండి
Reload documents when their files change on disk డిస్క్‌లో వాటి ఫైల్‌లు మారినప్పుడు పత్రాలను మళ్లీ లోడ్ చేయి
Always use my default zoom and page layout ఎల్లప్పుడూ నా డిఫాల్ట్ జూమ్ మరియు పేజీ లేఅవుట్‌ను ఉపయోగించు
This file changed on disk. Reload shows the new version and drops your unsaved changes; Save replaces it with this version. ఈ ఫైల్ డిస్క్‌లో మారింది. రీలోడ్ కొత్త వెర్షన్‌ను చూపించి సేవ్ చేయని మార్పులను వదిలేస్తుంది; సేవ్ దాని స్థానంలో ఈ వెర్షన్‌ను ఉంచుతుంది.
Can't render this file because it contains an unexpected character in line 13 and column 16.
+1
View File
@@ -2139,6 +2139,7 @@ signature pad Type Yaz
certificate Subject Konu
action wizard Start Başlat
Reopen the files that were open when PdfCraft last closed PdfCraft en son kapatıldığında açık olan dosyaları yeniden aç
Restore the last view (page, zoom and page display) when a document is opened again Bir belge yeniden açıldığında son görünümü (sayfa, yakınlaştırma ve sayfa görünümü) geri yükle
Add {what} {what} ekle
Use {what} {what} kullan
Place saved {what} Kayıtlı {what} yerleştir
Can't render this file because it contains an unexpected character in line 858 and column 9.
+1
View File
@@ -2191,6 +2191,7 @@ calendar date October жовтня
calendar date November листопада
calendar date December грудня
Reopen the files that were open when PdfCraft last closed Знову відкривати файли, які були відкриті під час останнього закриття PdfCraft
Restore the last view (page, zoom and page display) when a document is opened again Відновлювати останній вигляд (сторінку, масштаб і показ сторінок) під час повторного відкриття документа
Clear Recent Files Очистити список нещодавніх файлів
Remove from Recent Вилучити з нещодавніх
Remove {name} from Recent Вилучити {name} з нещодавніх
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -2142,6 +2142,7 @@
Used when opening PDFs. An explicit launch or control mode takes precedence for the session. 用于打开 PDF 时。如果启动参数或控制通道明确指定了模式,则本次会话优先使用该模式。
Used when a PDF doesn't ask for a layout or zoom. Continuous scrolling never snaps between pages. PDF 未指定布局或缩放时使用。连续滚动时页面不会逐页吸附。
Reopen the files that were open when PdfCraft last closed 重新打开上次关闭 PdfCraft 时打开的文件
Restore the last view (page, zoom and page display) when a document is opened again 再次打开文档时恢复上次的视图(页面、缩放和页面显示)
Reload documents when their files change on disk 文件在磁盘上更改时重新加载对应的文档
Always use my default zoom and page layout 始终使用我的默认缩放和页面布局
This file changed on disk. Reload shows the new version and drops your unsaved changes; Save replaces it with this version. 此文件已在磁盘上更改。“重新加载”显示新版本并放弃未保存的更改;“保存”则用此版本替换它。
Can't render this file because it contains an unexpected character in line 9 and column 16.
+1
View File
@@ -2093,6 +2093,7 @@
Go to page… 轉到頁面…
For page sizes and margins. 用於頁面尺寸和邊距。
Reopen the files that were open when PdfCraft last closed 重新開啟上次關閉 PdfCraft 時開啟的檔案
Restore the last view (page, zoom and page display) when a document is opened again 再次開啟文件時還原上次的檢視(頁面、縮放與頁面顯示)
The PDF text fonts can't draw “{c}” ({code}). Choose supported text. PDF 文字字型無法顯示「{c}」({code})。請選擇支援的文字。
The standard fonts can't draw “{c}” ({code}). Only Western European characters can be added as text for now. 標準字型無法顯示「{c}」({code})。目前只能新增西歐字元作為文字。
This field's font can't draw “{c}” ({code}), so PdfCraft shows ? in its place. The value is saved, and other PDF viewers redraw the field from it. 此欄位的字型無法顯示「{c}」({code}),PdfCraft 會在該處顯示 ?。值已儲存,其他 PDF 閱讀器會依據該值重新繪製此欄位。
Can't render this file because it contains an unexpected character in line 9 and column 16.
+3
View File
@@ -391,6 +391,9 @@ pub(crate) fn preferences_body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tok
// The web build has no file paths to reopen.
#[cfg(not(target_arch = "wasm32"))]
ui.add(widgets::checkbox(&mut app.reopen_last_session, tl!("Reopen the files that were open when PdfCraft last closed")));
// Acrobat's "Restore last view settings when reopening documents" (#1767).
#[cfg(not(target_arch = "wasm32"))]
ui.add(widgets::checkbox(&mut app.restore_last_view, tl!("Restore the last view (page, zoom and page display) when a document is opened again")));
// The web build has no files on disk to watch.
#[cfg(not(target_arch = "wasm32"))]
ui.add(widgets::checkbox(&mut app.reload_changed_files, tl!("Reload documents when their files change on disk")));
+112
View File
@@ -0,0 +1,112 @@
//! Restore last view settings when reopening documents (Acrobat's Preferences ▸ Documents
//! option; #1767): the page, zoom and page display a file was last shown with, remembered by
//! its path when its tab is closed or settings are saved, and applied when the file is opened
//! again. Desktop only: a document without a file has nothing to be found by.
use serde::Serialize;
use crate::PdfCraftApp;
use crate::canvas::{self, ViewDefaults};
/// How many files' views are remembered (the most recently closed or saved first).
const MAX_REMEMBERED: usize = 100;
/// How a file was last shown.
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct LastView {
/// Absolute, as `last_session` keys files.
pub path: String,
/// 0-based.
pub page: usize,
/// As settings name it: `fit-width`, `fit-page` or a percentage (`ViewDefaults::zoom_name`).
pub zoom: String,
/// The page display token (`canvas::display_token`).
pub display: String,
}
/// As `last_session` keys files (and the web has no paths to remember).
#[cfg(not(target_arch = "wasm32"))]
fn absolute(path: &str) -> String {
std::path::absolute(path).map_or_else(|_| path.to_string(), |p| p.to_string_lossy().into_owned())
}
#[cfg(target_arch = "wasm32")]
fn absolute(path: &str) -> String {
path.to_string()
}
impl PdfCraftApp {
/// How view `index` shows its document now; `None` for a document without a file.
fn view_to_remember(&self, index: usize) -> Option<LastView> {
let view = self.views.get(index)?;
let path = self.session.get(view.id)?.path.as_deref()?;
let zoom = ViewDefaults { fit: view.fit, zoom: view.zoom, ..ViewDefaults::default() }.zoom_name();
Some(LastView { path: absolute(path), page: view.current, zoom, display: canvas::display_token(view.layout, view.continuous).to_string() })
}
/// Remember how view `index` shows its document (when its tab closes).
pub(crate) fn remember_view(&mut self, index: usize) {
if let Some(v) = self.view_to_remember(index) {
self.last_views.retain(|l| l.path != v.path);
self.last_views.insert(0, v);
self.last_views.truncate(MAX_REMEMBERED);
}
}
/// The remembered views as settings store them: the open views first, as they are now
/// (so quitting remembers them without closing their tabs), then the rest.
pub(crate) fn last_views_to_save(&self) -> Vec<LastView> {
let mut out: Vec<LastView> = (0..self.views.len()).filter_map(|i| self.view_to_remember(i)).collect();
for l in &self.last_views {
if !out.iter().any(|o| o.path == l.path) {
out.push(l.clone());
}
}
out.truncate(MAX_REMEMBERED);
out
}
/// Settings are untrusted: an entry without a usable path, zoom or display is dropped, and
/// the list is capped.
pub(crate) fn restore_last_views(&mut self, v: &serde_json::Value) {
let Some(list) = v.as_array() else { return };
self.last_views = list
.iter()
.take(MAX_REMEMBERED)
.filter_map(|e| {
let path = e["path"].as_str().filter(|p| !p.is_empty())?.to_string();
let zoom = e["zoom"].as_str().filter(|z| ViewDefaults::default().with_zoom(z).is_some())?.to_string();
let display = e["display"].as_str().filter(|d| canvas::parse_display_token(d).is_some())?.to_string();
let page = usize::try_from(e["page"].as_u64().unwrap_or(0)).unwrap_or(0);
Some(LastView { path, page, zoom, display })
})
.collect();
}
/// Show view `index` as its document's file was last shown, when the preference is on and
/// the file is remembered. Applied after the document's own initial view, which it
/// overrides: where the reader left off is what they asked to come back to.
pub(crate) fn apply_last_view(&mut self, index: usize) {
if !self.restore_last_view {
return;
}
let Some(view) = self.views.get(index) else { return };
let Some(doc) = self.session.get(view.id) else { return };
let Some(path) = doc.path.as_deref().map(absolute) else { return };
let pages = doc.info.pages.len();
let Some(last) = self.last_views.iter().find(|l| l.path == path).cloned() else { return };
let Some(view) = self.views.get_mut(index) else { return };
if let Some((layout, continuous)) = canvas::parse_display_token(&last.display) {
view.set_display(layout, continuous);
}
if let Some(defaults) = ViewDefaults::default().with_zoom(&last.zoom) {
match defaults.fit {
canvas::Fit::None => view.set_zoom(defaults.zoom),
fit => view.set_fit(fit),
}
}
if last.page < pages {
view.go_to_page(last.page);
}
}
}
+17
View File
@@ -99,6 +99,7 @@ mod home;
mod icon_data;
pub mod icons;
pub mod last_session;
pub mod last_view;
pub mod motion;
mod page_selection_ui;
mod pageboxes;
@@ -584,6 +585,11 @@ pub struct PdfCraftApp {
pub recent: Vec<RecentFile>,
/// Preferences: reopen the files that were open when PdfCraft last closed (#442).
pub reopen_last_session: bool,
/// Restore last view settings when reopening documents (Preferences ▸ Documents and view;
/// #1767): the page, zoom and page display a file was last shown with. On by default.
pub restore_last_view: bool,
/// How files were last shown, most recent first (see `last_view`).
pub last_views: Vec<last_view::LastView>,
/// The files open when PdfCraft last closed, read from the settings for
/// [`PdfCraftApp::reopen_last_files`].
pub last_session: last_session::LastSession,
@@ -985,6 +991,8 @@ impl PdfCraftApp {
all_tools_expanded: false,
recent: Vec::new(),
reopen_last_session: false,
restore_last_view: true,
last_views: Vec::new(),
last_session: Default::default(),
quit_session: None,
reload_changed_files: true,
@@ -1415,6 +1423,8 @@ impl PdfCraftApp {
if activate && initial.full_screen {
self.pending_full_screen = Some(id);
}
// Where the reader left off wins over the file's own initial view (#1767).
self.apply_last_view(self.views.len() - 1);
if activate
&& self.left_open
&& self.left == LeftPanel::Tool("organize")
@@ -2007,6 +2017,7 @@ impl PdfCraftApp {
if index >= self.views.len() {
return;
}
self.remember_view(index);
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()) {
@@ -2370,6 +2381,8 @@ impl PdfCraftApp {
"docking_right": self.right,
"recent": self.recent,
"reopen_last_session": self.reopen_last_session,
"restore_last_view": self.restore_last_view,
"last_views": self.last_views_to_save(),
// Kept only while the preference is on.
"last_session": self.reopen_last_session.then(|| self.session_to_save()),
"reload_changed_files": self.reload_changed_files,
@@ -2468,6 +2481,10 @@ impl PdfCraftApp {
if let Some(on) = v["reopen_last_session"].as_bool() {
self.reopen_last_session = on;
}
if let Some(on) = v["restore_last_view"].as_bool() {
self.restore_last_view = on;
}
self.restore_last_views(&v["last_views"]);
self.last_session = last_session::LastSession::from_json(&v["last_session"]);
if let Some(on) = v["system_title_bar"].as_bool() {
self.system_title_bar = on;
+80
View File
@@ -1570,3 +1570,83 @@ fn short_window_rail_buttons_scroll_clear_of_page_navigation() {
assert!(tags.bottom() <= page.top(), "Tags {tags:?} scrolls into view above the page field {page:?}");
assert_eq!(h.get(By::new().role(egui::accesskit::Role::TextInput).value("1")).rect(), page, "navigation stays put");
}
/// A PDF with `n` empty 200×300 pt pages.
fn pages_fixture(n: usize) -> Vec<u8> {
let mut objs: Vec<Vec<u8>> = vec![b"<< /Type /Catalog /Pages 2 0 R >>".to_vec()];
let kids: Vec<String> = (0..n).map(|i| format!("{} 0 R", 3 + i)).collect();
objs.push(format!("<< /Type /Pages /Kids [{}] /Count {n} /MediaBox [0 0 200 300] >>", kids.join(" ")).into_bytes());
for _ in 0..n {
objs.push(b"<< /Type /Page /Parent 2 0 R >>".to_vec());
}
let mut out = b"%PDF-1.7\n".to_vec();
let mut offsets = Vec::new();
for (i, o) in objs.iter().enumerate() {
offsets.push(out.len());
out.extend_from_slice(format!("{} 0 obj\n", i + 1).as_bytes());
out.extend_from_slice(o);
out.extend_from_slice(b"\nendobj\n");
}
let xref = out.len();
out.extend_from_slice(format!("xref\n0 {}\n0000000000 65535 f \n", objs.len() + 1).as_bytes());
for o in offsets {
out.extend_from_slice(format!("{o:010} 00000 n \n").as_bytes());
}
out.extend_from_slice(format!("trailer\n<< /Size {} /Root 1 0 R >>\nstartxref\n{xref}\n%%EOF\n", objs.len() + 1).as_bytes());
out
}
/// A file opened again shows the page, zoom and page display it was last shown with (#1767,
/// Acrobat's "Restore last view settings when reopening documents"): remembered when its tab
/// closes and when settings are saved with it open, applied over the file's own initial view,
/// and not at all with the preference off. Settings are untrusted: bad entries are ignored.
#[test]
fn a_file_opened_again_shows_its_last_view() {
use pdfcraft_ui_egui::canvas::{Fit, PageLayout};
let path = std::env::temp_dir().join(format!("pdfcraft-last-view-{}.pdf", std::process::id())).to_string_lossy().into_owned();
let p = path.clone();
let mut h = harness(move |app| app.open_bytes("doc.pdf", Some(p), pages_fixture(3)).expect("opens"));
let p = path.clone();
assert!(h.state().restore_last_view, "on by default");
{
let app = h.state_mut();
app.set_option("zoom", "50").unwrap();
let view = &mut app.views[0];
view.set_display(PageLayout::TwoUp, false);
view.go_to_page(2);
}
h.run_steps(3);
// Saved with the tab still open: quitting remembers it.
let saved_open = h.state().persist();
h.state_mut().close_tab(0);
let saved_closed = h.state().persist();
for (label, saved) in [("open", saved_open), ("closed", saved_closed)] {
let (saved, p) = (saved.clone(), p.clone());
let h = harness(move |app| {
app.restore(&saved);
app.open_bytes("doc.pdf", Some(p), pages_fixture(3)).expect("opens");
});
let v = &h.state().views[0];
assert_eq!((v.current, v.fit, v.zoom, v.layout, v.continuous), (2, Fit::None, 0.5, PageLayout::TwoUp, false), "{label}");
}
// The preference off: the defaults.
let saved = h.state().persist();
let p = path.clone();
let h = harness(move |app| {
app.restore(&saved);
app.restore_last_view = false;
app.open_bytes("doc.pdf", Some(p), pages_fixture(3)).expect("opens");
});
let v = &h.state().views[0];
assert_eq!((v.current, v.fit, v.layout, v.continuous), (0, Fit::Width, PageLayout::Single, true));
// Garbage in the settings is ignored, a usable entry among it is kept.
let mut app = PdfCraftApp::new();
app.restore(&format!(
r#"{{"restore_last_view": "yes", "last_views": [5, {{"path": ""}}, {{"path": "{}", "page": -1, "zoom": "huge", "display": "single"}}, {{"path": "{}", "page": 1, "zoom": "fit-page", "display": "two-up"}}]}}"#,
path.replace('\\', "/"),
path.replace('\\', "/")
));
assert!(app.restore_last_view, "a non-boolean keeps the default");
assert_eq!(app.last_views.len(), 1);
assert_eq!((app.last_views[0].page, app.last_views[0].zoom.as_str()), (1, "fit-page"));
}
+13 -1
View File
@@ -695,7 +695,19 @@ status = "shipped"
commands = []
tools = []
evidence = ["crates/ui-egui/tests/it/editing.rs::the_last_session_reopens_its_files_pages_and_active_tab", "crates/ui-egui/tests/it/editing.rs::quitting_with_unsaved_changes_still_remembers_every_file", "crates/ui-egui/tests/it/editing.rs::the_last_session_is_off_by_default_and_skips_missing_and_duplicate_files", "crates/ui-egui/src/last_session.rs::malformed_sessions_are_skipped_and_capped"]
notes = "Desktop, off by default (Preferences ▸ Documents and view, #442): the files open when PdfCraft last closed reopen at the page each showed, with the same tab in front; files named on the command line open after them, in front; files that are gone, and documents the Recovery dialog offers, are skipped. Quitting with unsaved changes still remembers every tab. Not yet: zoom, layout and scroll position within the page (#418); only the first encrypted file asks for its password (later ones stay closed), and a file opened after it dismisses that prompt; a save that fails partway through a quit can leave the remembered list from that quit; never-saved documents and the web build have no paths to reopen."
notes = "Desktop, off by default (Preferences ▸ Documents and view, #442): the files open when PdfCraft last closed reopen at the page each showed, with the same tab in front; files named on the command line open after them, in front; files that are gone, and documents the Recovery dialog offers, are skipped. Quitting with unsaved changes still remembers every tab. Zoom and page display come back through core.restore-last-view (#1767); not yet the scroll position within the page (#418); only the first encrypted file asks for its password (later ones stay closed), and a file opened after it dismisses that prompt; a save that fails partway through a quit can leave the remembered list from that quit; never-saved documents and the web build have no paths to reopen."
[[feature]]
id = "core.restore-last-view"
title = "Restore last view settings when reopening documents"
area = "A"
tier = "P2"
milestone = "M3"
status = "shipped"
commands = []
tools = []
evidence = ["crates/ui-egui/tests/it/ui.rs::a_file_opened_again_shows_its_last_view"]
notes = "Desktop, on by default (Preferences ▸ Documents and view, #1767): a file opened again shows the page, zoom and page display it was last shown with, remembered by path when its tab closes or when settings are saved with it open (the last 100 files), and applied over the file's own initial view. Not remembered: the view rotation (display only) and the scroll position within the page; Fit height comes back as Fit width (the settings name only two fits)."
[[feature]]
id = "core.usage-rights-preserve"