M14: Preferences sets how many recent files to keep (#5572) (#5675)

The list was fixed at 12. Preferences ▸ Documents and view now takes a
number from 0 to 50, with 12 as the default. At 0 Home has no Recent
section and File ▸ Open Recent is disabled.

The list is cut when Preferences closes, so the number can pass through
0 on its way to another. Until then Home and saved settings already
leave out what is past it. A settings file can't ask for more than 50.

The control channel and launch options take it as recent-files. The two
new strings go in every catalog as English copies.
This commit is contained in:
VoiceLessQ
2026-11-01 13:52:47 -05:00
committed by GitHub
parent 6818995a3c
commit 424ca0e972
49 changed files with 459 additions and 22 deletions
+2
View File
@@ -219,6 +219,8 @@ Ranked; detail and the full milestone table in [docs/roadmap.md](docs/roadmap.md
## Progress log
- **2026-11-01 (M14, #5572):** Preferences ▸ Documents and view sets how many recent files to keep, from 0 to 50 with 12 by default. At 0 Home has no Recent section and File ▸ Open Recent is disabled. The list is cut when Preferences closes, so the number can pass through 0 on its way to another.
- 2026-11-01: PR #2155 further review preserves inherited Form text state and soft-mask chronology during Arrange, composes crop-group moves in the proper scope, and uses affine/press-relative image resize plus a shared rotation pivot. No-op script buttons preserve hidden source text; paste coordinates, visible selection ownership, and Snapshot/Export permissions have dedicated regressions. Candidate only; estimates unchanged.
- 2026-11-01: PR #2155 latest follow-up handles explicit image-paste events without release-time guesses, refreshes the font-size field when a selection becomes mixed, and specifies mixed-script normalization with exact Undo restoration. Candidate only; estimates unchanged.
+1 -1
View File
@@ -10,7 +10,7 @@
//! --theme light|dark|system --language auto|<code> --mode all|read|edit|convert|sign --tool <catalogue id> --left open|closed
//! --organize on --fields on --dialog properties|shortcuts|about --palette <query> --home on
//! --cover on|off --default-layout single|continuous|two-up|two-page --default-continuous on|off --default-zoom fit-width|fit-page|<percent> --always-default-view on|off
//! --page-gaps on|off --default-page-gaps on|off
//! --page-gaps on|off --default-page-gaps on|off --recent-files 0..50
//! --combine-view grid|list --combine-zoom 60..200`
//!
//! `--new-window` opens a window of its own. Without it, on Windows, a launch that only names files
+1
View File
@@ -118,6 +118,7 @@ impl PdfCraftApp {
all_tools_query: String::new(),
all_tools_tab: (None, false),
recent: Vec::new(),
recent_limit: crate::RECENT_FILES_DEFAULT,
reopen_last_session: false,
restore_last_view: true,
last_views: Vec::new(),
+6
View File
@@ -170,6 +170,12 @@ impl PdfCraftApp {
_ => return Err("default-page-gaps must be on or off".into()),
};
}
("recent-files", _) => {
let max = crate::RECENT_FILES_MAX;
let limit = value.parse().ok().filter(|n| *n <= max).ok_or_else(|| format!("recent-files must be between 0 and {max}"))?;
self.recent_limit = limit;
self.recent.truncate(limit);
}
("cover", Some(v)) => {
let on = match value {
"on" => true,
+1 -1
View File
@@ -333,7 +333,7 @@ impl PdfCraftApp {
macos_association::repair(std::path::Path::new(&p));
self.recent.retain(|r| r.path != p);
self.recent.insert(0, RecentFile { name: name.to_string(), path: p, pages, size });
self.recent.truncate(12);
self.recent.truncate(self.recent_limit);
}
// What the form's scripts said while it opened (messages, errors) shows now, not with
// the next edit.
+7 -1
View File
@@ -78,7 +78,7 @@ impl PdfCraftApp {
let mut v = serde_json::json!({
"side_panels": self.side_panels.to_json(self.left_open),
"docking_right": self.right,
"recent": self.recent,
"recent": self.recent.iter().take(self.recent_limit).collect::<Vec<_>>(),
"reopen_last_session": self.reopen_last_session,
"restore_last_view": self.restore_last_view,
"last_views": self.last_views_to_save(),
@@ -139,6 +139,7 @@ impl PdfCraftApp {
});
// Set apart: one more key inside the macro above passes serde_json's recursion limit.
v["default_page_gaps"] = serde_json::Value::Bool(self.view_defaults.show_page_gaps);
v["recent_limit"] = serde_json::json!(self.recent_limit);
v["fill_text_color"] = serde_json::json!(self.fill_text_color);
v["redact_prefs"] = self.redact_defaults.to_json();
v["pinned_tools"] = serde_json::json!(self.pinned_tools);
@@ -181,6 +182,11 @@ impl PdfCraftApp {
let r: Vec<RecentFile> = r.into_iter().filter(|f| std::path::Path::new(&f.path).exists()).collect();
self.recent = r;
}
// Nonsense keeps the limit it had; settings from before the limit have none.
if let Some(limit) = v["recent_limit"].as_u64() {
self.recent_limit = usize::try_from(limit).map_or(crate::RECENT_FILES_MAX, |n| n.min(crate::RECENT_FILES_MAX));
}
self.recent.truncate(self.recent_limit);
self.restore_layout_prefs(&v);
self.restore_behavior_prefs(&v);
self.restore_view_prefs(&v);
+2
View File
@@ -1931,6 +1931,8 @@ fn finish_close(app: &mut PdfCraftApp, dialog: Dialog, modal_closed: bool, req:
// Protect / Remove security replace the Properties dialog.
let replaces = req.link_command.is_some_and(|c| c.starts_with("protect.")) || req.open_revision.is_some();
if modal_closed || req.close || replaces {
// Only now: the number in Preferences can pass through 0 on its way to the one wanted.
app.recent.truncate(app.recent_limit);
app.dialog = None;
app.props_draft = None;
app.view_draft = None;
+17 -14
View File
@@ -186,25 +186,28 @@ pub fn show(app: &mut PdfCraftApp, ui: &mut egui::Ui) {
crate::folders_ui::section(app, ui);
}
ui.add_space(26.0);
let mut clear = false;
ui.horizontal(|ui| {
ui.label(egui::RichText::new(tl!("Recent")).font(theme::semibold(17.0)));
if !app.recent.is_empty() {
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
let resp = widgets::ghost_button(ui, "trash-2", tl!("Clear"));
resp.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Button, true, tl!("Clear Recent Files")));
clear = resp.on_hover_text(tl!("Clear Recent Files")).clicked();
});
// Preferences can turn the list off (#5572).
if app.recent_limit > 0 {
ui.add_space(26.0);
ui.horizontal(|ui| {
ui.label(egui::RichText::new(tl!("Recent")).font(theme::semibold(17.0)));
if !app.recent.is_empty() {
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
let resp = widgets::ghost_button(ui, "trash-2", tl!("Clear"));
resp.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Button, true, tl!("Clear Recent Files")));
clear = resp.on_hover_text(tl!("Clear Recent Files")).clicked();
});
}
});
ui.add_space(8.0);
if app.recent.is_empty() {
ui.label(egui::RichText::new(tl!("Files you open in PdfCraft appear here. Drop a PDF anywhere to open it.")).color(t.text_muted));
}
});
ui.add_space(8.0);
if app.recent.is_empty() {
ui.label(egui::RichText::new(tl!("Files you open in PdfCraft appear here. Drop a PDF anywhere to open it.")).color(t.text_muted));
}
let mut open = None;
let mut remove = None;
for r in &app.recent {
for r in app.recent.iter().take(app.recent_limit) {
let (rect, resp) = ui.allocate_exact_size(vec2(ui.available_width(), 46.0), Sense::click());
resp.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Button, true, &r.name));
let press = theme::Press::track(ui, &resp);
+4
View File
@@ -20,6 +20,10 @@
# reorders each line with UAX #9, so preordering here would reverse right-to-left text twice.
# Spaces and a colon at either end of an entry stay at that end, next to what the code appends.
# Untranslated: English text until a translator fills these in.
Lowering this forgets the oldest files. At 0 there is no list. Lowering this forgets the oldest files. At 0 there is no list.
Recent files to keep Recent files to keep
Menu القائمة
File ملف
Edit تحرير
Can't render this file because it contains an unexpected character in line 6 and column 47.
+4
View File
@@ -7,6 +7,10 @@
# Product and technology names (PdfCraft, PDF, Acrobat JavaScript ...) stay in Latin letters;
# the trailing "…" of dialog commands is kept.
# Untranslated: English text until a translator fills these in.
Lowering this forgets the oldest files. At 0 there is no list. Lowering this forgets the oldest files. At 0 there is no list.
Recent files to keep Recent files to keep
About За програмата
Contributors Сътрудници
Models Модели
Can't render this file because it contains an unexpected character in line 8 and column 16.
+4
View File
@@ -9,6 +9,10 @@
# Product and technology names (PdfCraft, PDF, Acrobat JavaScript ...) are not translated;
# the trailing "…" of dialog commands is kept.
# Untranslated: English text until a translator fills these in.
Lowering this forgets the oldest files. At 0 there is no list. Lowering this forgets the oldest files. At 0 there is no list.
Recent files to keep Recent files to keep
Menu Menú
File Fitxer
Edit Edita
Can't render this file because it contains an unexpected character in line 7 and column 3.
+4
View File
@@ -20,6 +20,10 @@
# reorders each line with UAX #9, so preordering here would reverse right-to-left text twice.
# Spaces and a colon at either end of an entry stay at that end, next to what the code appends.
# Untranslated: English text until a translator fills these in.
Lowering this forgets the oldest files. At 0 there is no list. Lowering this forgets the oldest files. At 0 there is no list.
Recent files to keep Recent files to keep
Menu پێڕست
File فایل
Edit دەستکاریکردن
Can't render this file because it contains an unexpected character in line 6 and column 47.
+4
View File
@@ -19,6 +19,10 @@
# commands is kept. Messages with a count but no `@plural` entry use count-neutral wording
# ("Vybráno stránek: {n}"), because Czech has three plural forms.
# Untranslated: English text until a translator fills these in.
Lowering this forgets the oldest files. At 0 there is no list. Lowering this forgets the oldest files. At 0 there is no list.
Recent files to keep Recent files to keep
Menu Nabídka
File Soubor
Rotation: Otočení:
Can't render this file because it contains an unexpected character in line 6 and column 47.
+4
View File
@@ -9,6 +9,10 @@
# Product and technology names (PdfCraft, PDF, Acrobat JavaScript ...) are not translated;
# the trailing "…" of dialog commands is kept.
# Untranslated: English text until a translator fills these in.
Lowering this forgets the oldest files. At 0 there is no list. Lowering this forgets the oldest files. At 0 there is no list.
Recent files to keep Recent files to keep
Menu Menü
File Datei
Edit Bearbeiten
Can't render this file because it contains an unexpected character in line 6 and column 51.
+5
View File
@@ -2,6 +2,11 @@
# Partial, clean-room initial contribution. Entries not yet translated fall back to English.
# Translate from English meanings; retain placeholders and trailing ellipses.
# Greek plural forms: one (1) | other (0, 2+).
# Untranslated: English text until a translator fills these in.
Lowering this forgets the oldest files. At 0 there is no list. Lowering this forgets the oldest files. At 0 there is no list.
Recent files to keep Recent files to keep
Menu Μενού
File Αρχείο
Edit Επεξεργασία
1 # Greek (el) interface catalog. Columns: context <TAB> English source <TAB> Greek translation.
2 # Partial, clean-room initial contribution. Entries not yet translated fall back to English.
3 # Translate from English meanings; retain placeholders and trailing ellipses.
4 # Greek plural forms: one (1) | other (0, 2+).
5 # Untranslated: English text until a translator fills these in.
6
7
8
9
10
11
12
+4
View File
@@ -9,6 +9,10 @@
# Product and technology names (PdfCraft, PDF, Acrobat JavaScript ...) are not translated;
# the trailing "…" of dialog commands is kept.
# Untranslated: English text until a translator fills these in.
Lowering this forgets the oldest files. At 0 there is no list. Lowering this forgets the oldest files. At 0 there is no list.
Recent files to keep Recent files to keep
Menu Menú
File Archivo
Edit Edición
Can't render this file because it contains an unexpected character in line 6 and column 60.
+4
View File
@@ -19,6 +19,10 @@
# each rendered line. The Persian comma \u060c and question mark \u061f are used, and \u200c (ZWNJ) joins
# compound words.
# Untranslated: English text until a translator fills these in.
Lowering this forgets the oldest files. At 0 there is no list. Lowering this forgets the oldest files. At 0 there is no list.
Recent files to keep Recent files to keep
Menu منو
File فایل
Edit ویرایش
Can't render this file because it contains an unexpected character in line 6 and column 47.
+5
View File
@@ -16,6 +16,11 @@
# Labels use sentence case. Product and technology names (PdfCraft, PDF, OCR, JavaScript ...)
# stay in Latin letters; the trailing "…" of dialog commands is kept. Calendar headings use the
# nominative month (`Lokakuu 2026`); dates use the partitive (`14. lokakuuta 2026`).
# Untranslated: English text until a translator fills these in.
Lowering this forgets the oldest files. At 0 there is no list. Lowering this forgets the oldest files. At 0 there is no list.
Recent files to keep Recent files to keep
Menu Valikko
File Tiedosto
Edit Muokkaa
Can't render this file because it contains an unexpected character in line 5 and column 47.
+4
View File
@@ -9,6 +9,10 @@
# Product and technology names (PdfCraft, PDF, Acrobat JavaScript ...) are not translated;
# the trailing "…" of dialog commands is kept.
# Untranslated: English text until a translator fills these in.
Lowering this forgets the oldest files. At 0 there is no list. Lowering this forgets the oldest files. At 0 there is no list.
Recent files to keep Recent files to keep
Menu Menu
File Fichier
Edit Édition
Can't render this file because it contains an unexpected character in line 6 and column 39.
+5
View File
@@ -2,6 +2,11 @@
# context <TAB> English source <TAB> translation
# Clean-room translation written from the English source's meaning.
# License: MIT OR Apache-2.0.
# Untranslated: English text until a translator fills these in.
Lowering this forgets the oldest files. At 0 there is no list. Lowering this forgets the oldest files. At 0 there is no list.
Recent files to keep Recent files to keep
Menu मेनू
File फ़ाइल
Edit संपादित करें
1 # Hindi (hi) interface catalog. Columns, separated by a TAB:
2 # context <TAB> English source <TAB> translation
3 # Clean-room translation written from the English source's meaning.
4 # License: MIT OR Apache-2.0.
5 # Untranslated: English text until a translator fills these in.
6
7
8
9
10
11
12
+5
View File
@@ -9,6 +9,11 @@
# consulted or copied; add entries the same way.
# Product and technology names (PdfCraft, PDF, Acrobat JavaScript ...) are not translated;
# the trailing "…" of dialog commands is kept.
# Untranslated: English text until a translator fills these in.
Lowering this forgets the oldest files. At 0 there is no list. Lowering this forgets the oldest files. At 0 there is no list.
Recent files to keep Recent files to keep
Menu Menü
File Fájl
Edit Szerkesztés
Can't render this file because it contains an unexpected character in line 7 and column 44.
+5
View File
@@ -3,6 +3,11 @@
# Clean-room translations of PdfCraft's English UI labels. Use standard Indonesian computing
# terms; keep product and format names (PdfCraft, PDF, PDF/A, OCR, XFA, Word...) unchanged.
# Indonesian does not inflect nouns for number, so @plural entries list one form.
# Untranslated: English text until a translator fills these in.
Lowering this forgets the oldest files. At 0 there is no list. Lowering this forgets the oldest files. At 0 there is no list.
Recent files to keep Recent files to keep
Menu Menu
Edit Edit
Pages Halaman
1 # Indonesian (id) catalog. Columns, separated by a TAB: context <TAB> source <TAB> translation
3 # Clean-room translations of PdfCraft's English UI labels. Use standard Indonesian computing
4 # terms; keep product and format names (PdfCraft, PDF, PDF/A, OCR, XFA, Word...) unchanged.
5 # Indonesian does not inflect nouns for number, so @plural entries list one form.
6 # Untranslated: English text until a translator fills these in.
7
8
9
10
11
12
13
+4
View File
@@ -15,6 +15,10 @@
# Product and technology names (PdfCraft, PDF, Acrobat JavaScript ...) stay in Latin letters;
# the trailing "…" of dialog commands is kept.
# Untranslated: English text until a translator fills these in.
Lowering this forgets the oldest files. At 0 there is no list. Lowering this forgets the oldest files. At 0 there is no list.
Recent files to keep Recent files to keep
Menu Menu
File File
Edit Modifica
Can't render this file because it contains an unexpected character in line 6 and column 47.
+4
View File
@@ -15,6 +15,10 @@
# Product and technology names (PdfCraft, PDF, Acrobat JavaScript ...) stay in Latin letters;
# the trailing "…" of dialog commands is kept.
# Untranslated: English text until a translator fills these in.
Lowering this forgets the oldest files. At 0 there is no list. Lowering this forgets the oldest files. At 0 there is no list.
Recent files to keep Recent files to keep
Menu メニュー
File ファイル
Edit 編集
Can't render this file because it contains an unexpected character in line 6 and column 47.
+4
View File
@@ -1,3 +1,7 @@
# Untranslated: English text until a translator fills these in.
Lowering this forgets the oldest files. At 0 there is no list. Lowering this forgets the oldest files. At 0 there is no list.
Recent files to keep Recent files to keep
Menu Мәзір
File Файл
Edit Түзету
1 # Untranslated: English text until a translator fills these in. Menu Мәзір
1 # Untranslated: English text until a translator fills these in.
2
3
4
5 Menu Мәзір
6 File Файл
7 Edit Түзету
+5
View File
@@ -2,6 +2,11 @@
# context <TAB> English source <TAB> translation
# Clean-room translation written from the English source's meaning.
# License: MIT OR Apache-2.0.
# Untranslated: English text until a translator fills these in.
Lowering this forgets the oldest files. At 0 there is no list. Lowering this forgets the oldest files. At 0 there is no list.
Recent files to keep Recent files to keep
Menu 메뉴
File 파일
Edit 편집
1 # Korean (ko) interface catalog. Columns, separated by a TAB:
2 # context <TAB> English source <TAB> translation
3 # Clean-room translation written from the English source's meaning.
4 # License: MIT OR Apache-2.0.
5 # Untranslated: English text until a translator fills these in.
6
7
8
9
10
11
12
+4
View File
@@ -15,6 +15,10 @@
# Product and technology names (PdfCraft, PDF, Acrobat JavaScript ...) stay in Latin letters;
# the trailing "..." of dialog commands is kept.
# Untranslated: English text until a translator fills these in.
Lowering this forgets the oldest files. At 0 there is no list. Lowering this forgets the oldest files. At 0 there is no list.
Recent files to keep Recent files to keep
Menu ເມນູ
File ໄຟລ໌
Edit ແກ້ໄຂ
Can't render this file because it contains an unexpected character in line 6 and column 32.
+4
View File
@@ -4,6 +4,10 @@
# consult the Lietuvos Respublikos terminų bankas (terminai.vlkk.lt) when selecting terminology.
# Product and format names (PdfCraft, PDF, PDF/A, OCR, XFA, Word…) remain unchanged.
# Untranslated: English text until a translator fills these in.
Lowering this forgets the oldest files. At 0 there is no list. Lowering this forgets the oldest files. At 0 there is no list.
Recent files to keep Recent files to keep
Menu Meniu
Edit Redaguoti
Pages Puslapiai
1 # Lithuanian (lt) catalog. Columns, separated by a TAB: context <TAB> source <TAB> translation
4 # consult the Lietuvos Respublikos terminų bankas (terminai.vlkk.lt) when selecting terminology.
5 # Product and format names (PdfCraft, PDF, PDF/A, OCR, XFA, Word…) remain unchanged.
6 # Untranslated: English text until a translator fills these in.
7
8
9
10
11
12
13
+4
View File
@@ -7,6 +7,10 @@
# Product and technology names (PdfCraft, PDF, Acrobat JavaScript ...) are not translated;
# the trailing "…" of dialog commands is kept.
# Untranslated: English text until a translator fills these in.
Lowering this forgets the oldest files. At 0 there is no list. Lowering this forgets the oldest files. At 0 there is no list.
Recent files to keep Recent files to keep
Allow Tillat
Cancel Avbryt
Only allow this if you trust the source document. Tillat dette bare hvis du stoler på kildedokumentet.
Can't render this file because it contains an unexpected character in line 8 and column 16.
+4
View File
@@ -15,6 +15,10 @@
# Product and technology names (PdfCraft, PDF, Acrobat JavaScript ...) stay in Latin letters;
# the trailing "…" of dialog commands is kept.
# Untranslated: English text until a translator fills these in.
Lowering this forgets the oldest files. At 0 there is no list. Lowering this forgets the oldest files. At 0 there is no list.
Recent files to keep Recent files to keep
Menu Menu
File Plik
Edit Edycja
Can't render this file because it contains an unexpected character in line 6 and column 47.
+4
View File
@@ -8,6 +8,10 @@
# Product and technology names (PdfCraft, PDF, Acrobat JavaScript ...) are not translated;
# the trailing "…" of dialog commands is kept.
# Untranslated: English text until a translator fills these in.
Lowering this forgets the oldest files. At 0 there is no list. Lowering this forgets the oldest files. At 0 there is no list.
Recent files to keep Recent files to keep
Menu Menu
File Arquivo
Rotation: Rotação:
Can't render this file because it contains an unexpected character in line 9 and column 16.
+4
View File
@@ -9,6 +9,10 @@
# the trailing "…" of dialog commands is kept. Commands use the noun form ("Salvare", "Deschidere…"),
# messages address the user formally ("Alegeți", "dvs.").
# Untranslated: English text until a translator fills these in.
Lowering this forgets the oldest files. At 0 there is no list. Lowering this forgets the oldest files. At 0 there is no list.
Recent files to keep Recent files to keep
Menu Meniu
Edit Editare
Pages Pagini
Can't render this file because it contains an unexpected character in line 3 and column 110.
+4
View File
@@ -8,6 +8,10 @@
# Product and technology names (PdfCraft, PDF, Acrobat JavaScript ...) are not translated;
# the trailing "…" of dialog commands is kept.
# Untranslated: English text until a translator fills these in.
Lowering this forgets the oldest files. At 0 there is no list. Lowering this forgets the oldest files. At 0 there is no list.
Recent files to keep Recent files to keep
Menu Меню
File Файл
Edit Редактирование
Can't render this file because it contains an unexpected character in line 9 and column 16.
+4
View File
@@ -8,6 +8,10 @@
# Product and technology names (PdfCraft, PDF, Acrobat JavaScript ...) are not translated;
# the trailing "…" of dialog commands is kept.
# Untranslated: English text until a translator fills these in.
Lowering this forgets the oldest files. At 0 there is no list. Lowering this forgets the oldest files. At 0 there is no list.
Recent files to keep Recent files to keep
Menu Meni
File Datoteka
Edit Urejanje
Can't render this file because it contains an unexpected character in line 9 and column 16.
+4
View File
@@ -8,6 +8,10 @@
# Product and technology names (PdfCraft, PDF, Acrobat JavaScript ...) are not translated;
# the trailing "…" of dialog commands is kept.
# Untranslated: English text until a translator fills these in.
Lowering this forgets the oldest files. At 0 there is no list. Lowering this forgets the oldest files. At 0 there is no list.
Recent files to keep Recent files to keep
Menu Menyja
File Skedari
Edit Redakto
Can't render this file because it contains an unexpected character in line 9 and column 16.
+4
View File
@@ -12,6 +12,10 @@
# Product and technology names (PdfCraft, PDF, Acrobat JavaScript ...) are not translated;
# the trailing "…" of dialog commands is kept.
# Untranslated: English text until a translator fills these in.
Lowering this forgets the oldest files. At 0 there is no list. Lowering this forgets the oldest files. At 0 there is no list.
Recent files to keep Recent files to keep
Menu మెనూ
File ఫైల్
Edit సవరణ
Can't render this file because it contains an unexpected character in line 13 and column 16.
+4
View File
@@ -7,6 +7,10 @@
# names stay in Latin letters; the trailing ellipsis of dialog commands is kept. Thai has one
# plural form.
# Untranslated: English text until a translator fills these in.
Lowering this forgets the oldest files. At 0 there is no list. Lowering this forgets the oldest files. At 0 there is no list.
Recent files to keep Recent files to keep
Menu เมนู
File ไฟล์
Edit แก้ไข
1 # Thai (th) catalog. Columns, separated by a TAB: context <TAB> source <TAB> translation
7 # names stay in Latin letters; the trailing ellipsis of dialog commands is kept. Thai has one
8 # plural form.
9 # Untranslated: English text until a translator fills these in.
10
11
12
13
14
15
16
+4
View File
@@ -4,6 +4,10 @@
#
# Clean-room: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources.
# Untranslated: English text until a translator fills these in.
Lowering this forgets the oldest files. At 0 there is no list. Lowering this forgets the oldest files. At 0 there is no list.
Recent files to keep Recent files to keep
Menu Menü
File Dosya
Edit Düzen
1 # Turkish (tr) catalog. Format: see the header of ja.tsv
4 #
5 # Clean-room: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources.
6 # Untranslated: English text until a translator fills these in.
7
8
9
10
11
12
13
+4
View File
@@ -18,6 +18,10 @@
# Integer plurals (CLDR uk): one (1, 21) | few (2–4, 22–24) | many (0, 5–19); 11–14 take many.
# Commands use infinitives. Fixed-count templates use count-neutral wording.
# Untranslated: English text until a translator fills these in.
Lowering this forgets the oldest files. At 0 there is no list. Lowering this forgets the oldest files. At 0 there is no list.
Recent files to keep Recent files to keep
Menu Меню
File Файл
Edit Редагування
Can't render this file because it contains an unexpected character in line 6 and column 47.
+5
View File
@@ -7,6 +7,11 @@
# Plural rule: `plural_none` — every @plural row lists exactly one form.
# Context-sensitive wording and intentional unchanged strings are regression-tested in i18n/mod.rs.
#
# Untranslated: English text until a translator fills these in.
Lowering this forgets the oldest files. At 0 there is no list. Lowering this forgets the oldest files. At 0 there is no list.
Recent files to keep Recent files to keep
Menu Trình đơn
Edit Chỉnh sửa
Pages Trang
1 # Vietnamese (vi) catalog. Columns, separated by a TAB: context <TAB> source <TAB> translation
7 # Plural rule: `plural_none` — every @plural row lists exactly one form.
8 # Context-sensitive wording and intentional unchanged strings are regression-tested in i18n/mod.rs.
9 #
10 # Untranslated: English text until a translator fills these in.
11
12
13
14
15
16
17
+4
View File
@@ -8,6 +8,10 @@
# Product and technology names (PdfCraft, PDF, Acrobat JavaScript ...) stay in Latin letters;
# the trailing "…" of dialog commands is kept.
# Untranslated: English text until a translator fills these in.
Lowering this forgets the oldest files. At 0 there is no list. Lowering this forgets the oldest files. At 0 there is no list.
Recent files to keep Recent files to keep
# About, contributor/model credits, and multiline interface guidance.
About 关于
Contributors 贡献者
Can't render this file because it contains an unexpected character in line 9 and column 16.
+4
View File
@@ -8,6 +8,10 @@
# Product and technology names (PdfCraft, PDF, Acrobat JavaScript ...) stay in Latin letters;
# the trailing "…" of dialog commands is kept.
# Untranslated: English text until a translator fills these in.
Lowering this forgets the oldest files. At 0 there is no list. Lowering this forgets the oldest files. At 0 there is no list.
Recent files to keep Recent files to keep
Menu 選單
File 檔案
Edit 編輯
Can't render this file because it contains an unexpected character in line 9 and column 16.
+3 -1
View File
@@ -145,7 +145,7 @@ use state::CopyPage;
use state::OpenRequest;
pub use state::{
CustomDraft, Dialog, DuplicateDraft, LeftPanel, LinkOrigin, Mode, NumberDraft, OpenOutcome, PageClip, PagePropsDraft, PasswordPrompt,
PendingLink, PendingRemoteLink, PropsTab, QuickTool, RecentFile, RightPanel, UnlockPrompt,
PendingLink, PendingRemoteLink, PropsTab, QuickTool, RECENT_FILES_DEFAULT, RECENT_FILES_MAX, RecentFile, RightPanel, UnlockPrompt,
};
use state::{DocumentCopyJob, GridFile};
pub use tools::bulk_fields;
@@ -346,6 +346,8 @@ pub struct PdfCraftApp {
/// tab coming to the front (Home, a document, Combine files) puts the view away.
all_tools_tab: (Option<DocId>, bool),
pub recent: Vec<RecentFile>,
/// How many recent files to keep, 0 for no list (Preferences ▸ Documents and view, #5572).
pub recent_limit: usize,
/// 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;
+1 -1
View File
@@ -98,7 +98,7 @@ impl PdfCraftApp {
/// the Recovery dialog offers, so neither opens twice.
///
/// Only one password prompt can wait at a time: the first encrypted file asks for its
/// password and later ones stay closed (they are in the recent-files list).
/// password and later ones stay closed (in the recent-files list, if there is one).
pub fn reopen_last_files(&mut self, skip: &[String]) {
let last = std::mem::take(&mut self.last_session);
#[cfg(not(target_arch = "wasm32"))]
+5
View File
@@ -365,6 +365,11 @@ impl UnlockPrompt {
}
}
/// How many recent files are kept until Preferences says otherwise.
pub const RECENT_FILES_DEFAULT: usize = 12;
/// The most Preferences can ask to keep.
pub const RECENT_FILES_MAX: usize = 50;
#[derive(serde::Serialize, serde::Deserialize)]
pub struct RecentFile {
pub name: String,
+9
View File
@@ -492,6 +492,15 @@ pub(crate) fn preferences_body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tok
}
});
ui.label(egui::RichText::new(tl!("For page sizes and margins.")).small().color(t.text_muted));
// The web build has no file paths to list.
#[cfg(not(target_arch = "wasm32"))]
{
ui.horizontal(|ui| {
let label = ui.label(tl!("Recent files to keep"));
ui.add(egui::DragValue::new(&mut app.recent_limit).range(0..=crate::RECENT_FILES_MAX)).labelled_by(label.id);
});
ui.label(egui::RichText::new(tl!("Lowering this forgets the oldest files. At 0 there is no list.")).small().color(t.text_muted));
}
// 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")));
+1
View File
@@ -67,6 +67,7 @@ mod prepare;
mod print;
mod protect;
mod reading_colors;
mod recent_files;
mod recovery;
mod redact;
mod reload;
+257
View File
@@ -0,0 +1,257 @@
//! Preferences ▸ Recent files to keep (#5572): how many files the recent list holds, from none
//! up to 50, on every path that adds to it, shows it or saves it.
use egui::Key;
use egui::accesskit::Role;
use egui_kittest::Harness;
use egui_kittest::kittest::Queryable;
use pdfcraft_ui_egui::{Dialog, PdfCraftApp, RECENT_FILES_DEFAULT, RecentFile};
use crate::support;
const PDF: &[u8] = include_bytes!("../data/form.pdf");
fn recent_file(name: &str) -> RecentFile {
RecentFile { name: name.into(), path: format!("/nowhere/{name}"), pages: 1, size: 0 }
}
fn names(app: &PdfCraftApp) -> Vec<&str> {
app.recent.iter().map(|r| r.name.as_str()).collect()
}
fn app_with(files: &[&str]) -> PdfCraftApp {
let mut app = PdfCraftApp::new();
app.set_option("language", "en").unwrap();
app.recent.extend(files.iter().map(|n| recent_file(n)));
app
}
/// Tall enough that the whole of Home is on screen without scrolling.
fn harness(limit: usize, files: &[&str]) -> Harness<'static, PdfCraftApp> {
let files: Vec<String> = files.iter().map(ToString::to_string).collect();
let mut h = Harness::builder().with_size(egui::vec2(1400.0, 1600.0)).build_eframe(move |_cc| {
let mut app = app_with(&files.iter().map(String::as_str).collect::<Vec<_>>());
app.recent_limit = limit;
app
});
h.run_steps(4);
h
}
/// Open a document with a file behind it, as a file on disk is recorded.
fn open(app: &mut PdfCraftApp, name: &str) {
app.open_bytes(name, Some(format!("/nowhere/{name}")), PDF.to_vec()).unwrap();
}
/// Typed the way a person does: click the field, type, Enter.
fn type_limit(h: &mut Harness<'static, PdfCraftApp>, value: &str) {
h.get_by_role_and_label(Role::SpinButton, "Recent files to keep").click();
h.run_steps(2);
h.key_press_modifiers(egui::Modifiers::COMMAND, Key::A);
h.run_steps(1);
h.event(egui::Event::Text(value.into()));
h.run_steps(1);
h.key_press(Key::Enter);
h.run_steps(3);
}
#[test]
fn the_list_keeps_twelve_files_until_preferences_says_otherwise() {
let mut app = app_with(&[]);
assert_eq!(app.recent_limit, 12);
for n in 0..=RECENT_FILES_DEFAULT {
open(&mut app, &format!("{n}.pdf"));
}
assert_eq!(app.recent.len(), 12);
assert_eq!(app.recent[0].name, "12.pdf", "newest first");
assert_eq!(app.recent[11].name, "1.pdf", "the oldest of the thirteen went");
}
#[test]
fn opening_a_file_keeps_the_newest_up_to_the_limit() {
let mut app = app_with(&[]);
app.recent_limit = 2;
for name in ["first.pdf", "second.pdf", "third.pdf"] {
open(&mut app, name);
}
assert_eq!(names(&app), ["third.pdf", "second.pdf"]);
open(&mut app, "second.pdf");
assert_eq!(names(&app), ["second.pdf", "third.pdf"], "a file opened again moves up and loses nothing");
}
#[test]
fn at_zero_opening_a_file_records_nothing() {
let mut app = app_with(&[]);
app.recent_limit = 0;
open(&mut app, "first.pdf");
assert!(app.recent.is_empty());
let saved: serde_json::Value = serde_json::from_str(&app.persist()).unwrap();
assert_eq!(saved["recent"], serde_json::json!([]), "and the saved list is empty");
}
#[test]
fn at_zero_home_has_no_recent_section() {
let h = harness(0, &[]);
assert!(h.query_by_label("Recent").is_none(), "no heading");
assert!(h.query_by_label_contains("Files you open in PdfCraft appear here").is_none(), "and no promise of a list");
h.get_by_label_contains("PdfCraft works offline");
}
#[test]
fn above_zero_an_empty_list_still_says_what_it_is_for() {
let h = harness(1, &[]);
h.get_by_label("Recent");
h.get_by_label_contains("Files you open in PdfCraft appear here");
}
#[test]
fn at_zero_the_menu_and_the_palette_have_nothing_to_open() {
let mut h = harness(0, &[]);
support::open_menu(&mut h, "File");
assert!(h.query_by(|n| n.label().as_deref() == Some("Open Recent") && n.is_disabled()).is_some(), "Open Recent is disabled");
assert!(h.state_mut().execute("file.open_recent"));
assert!(h.state().toast.as_ref().is_some_and(|(m, _)| m.contains("No recent files")));
assert!(h.state().views.is_empty(), "and nothing opened");
}
#[test]
fn the_longest_list_scrolls_in_the_smallest_window_down_to_clear() {
let files: Vec<String> = (0..50).map(|n| format!("{n}.pdf")).collect();
let mut h = Harness::builder().with_size(egui::vec2(1000.0, 520.0)).build_eframe({
let files = files.clone();
move |_cc| {
let mut app = app_with(&files.iter().map(String::as_str).collect::<Vec<_>>());
app.recent_limit = 50;
app.open_bytes("doc.pdf", None, PDF.to_vec()).unwrap();
app
}
});
h.run_steps(4);
support::open_menu(&mut h, "File");
support::menu_row(&h, "Open Recent").hover();
h.run_steps(3);
let foot = |h: &Harness<'static, PdfCraftApp>| support::menu_row(h, "Clear Recent Files").rect().max.y;
assert!(foot(&h) > 520.0, "fifty rows run past the smallest window");
support::menu_row(&h, "1.pdf").hover();
h.run_steps(2);
h.event(egui::Event::MouseWheel {
unit: egui::MouseWheelUnit::Point,
delta: egui::vec2(0.0, -5000.0),
phase: egui::TouchPhase::Move,
modifiers: egui::Modifiers::NONE,
source: egui::MouseWheelSource::Unknown,
});
h.run_steps(8);
assert!(foot(&h) <= 520.0, "the wheel brings the foot of the menu into view");
support::menu_row(&h, "Clear Recent Files").click();
h.run_steps(3);
assert!(h.state().recent.is_empty());
}
#[test]
fn preferences_cuts_the_list_when_it_closes_and_not_before() {
let mut h = harness(12, &["first.pdf", "second.pdf", "third.pdf"]);
h.state_mut().dialog = Some(Dialog::Preferences);
h.run_steps(4);
type_limit(&mut h, "0");
assert_eq!(h.state().recent_limit, 0);
assert_eq!(h.state().recent.len(), 3, "0 on the way to another number forgets nothing");
assert!(h.query_by_label("first.pdf").is_none(), "though Home already leaves them out");
type_limit(&mut h, "2");
assert_eq!(h.state().recent.len(), 3);
h.get_by_label("OK").click();
h.run_steps(4);
assert_eq!(h.state().dialog, None);
assert_eq!(names(h.state()), ["first.pdf", "second.pdf"], "the newest two stay");
}
#[test]
fn escape_closes_preferences_with_the_same_result_as_ok() {
let mut h = harness(12, &["first.pdf", "second.pdf"]);
h.state_mut().dialog = Some(Dialog::Preferences);
h.run_steps(4);
type_limit(&mut h, "1");
h.key_press(Key::Escape);
h.run_steps(4);
assert_eq!(h.state().dialog, None);
assert_eq!(names(h.state()), ["first.pdf"]);
}
#[test]
fn the_field_does_not_go_past_fifty() {
let mut h = harness(12, &[]);
h.state_mut().dialog = Some(Dialog::Preferences);
h.run_steps(4);
type_limit(&mut h, "999");
assert_eq!(h.state().recent_limit, 50);
}
#[test]
fn settings_saved_while_preferences_is_open_already_leave_the_rest_out() {
let mut app = app_with(&["first.pdf", "second.pdf"]);
app.recent_limit = 1;
let saved = app.persist();
assert!(saved.contains("first.pdf"));
assert!(!saved.contains("second.pdf"));
}
#[test]
fn the_limit_is_saved_and_restored_and_cuts_a_longer_list() {
let dir = std::env::temp_dir().join(format!("pdfcraft-recent-limit-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let paths: Vec<String> = ["a.pdf", "b.pdf", "c.pdf"]
.iter()
.map(|n| {
let path = dir.join(n);
std::fs::write(&path, PDF).unwrap();
path.to_string_lossy().into_owned()
})
.collect();
let entries: Vec<RecentFile> = paths.iter().map(|p| RecentFile { name: "x.pdf".into(), path: p.clone(), pages: 1, size: 0 }).collect();
let json = serde_json::json!({ "recent": entries, "recent_limit": 2 }).to_string();
let mut restored = PdfCraftApp::new();
restored.restore(&json);
assert_eq!(restored.recent_limit, 2);
let kept: Vec<&str> = restored.recent.iter().map(|r| r.path.as_str()).collect();
assert_eq!(kept, [paths[0].as_str(), paths[1].as_str()]);
let mut again = PdfCraftApp::new();
again.restore(&restored.persist());
assert_eq!(again.recent_limit, 2);
assert_eq!(again.recent.len(), 2);
std::fs::remove_dir_all(&dir).unwrap();
}
#[test]
fn a_settings_file_can_not_ask_for_a_limit_that_is_not_one() {
for (saved, want) in [
("{}", 12),
("{\"recent_limit\": null}", 12),
("{\"recent_limit\": \"3\"}", 12),
("{\"recent_limit\": -1}", 12),
("{\"recent_limit\": 2.5}", 12),
("{\"recent_limit\": 0}", 0),
("{\"recent_limit\": 50}", 50),
("{\"recent_limit\": 51}", 50),
("{\"recent_limit\": 18446744073709551615}", 50),
] {
let mut app = PdfCraftApp::new();
app.restore(saved);
assert_eq!(app.recent_limit, want, "{saved}");
}
}
#[test]
fn the_control_channel_sets_the_limit_and_refuses_what_it_can_not_be() {
let mut app = app_with(&["first.pdf", "second.pdf"]);
for bad in ["51", "-1", "x", "", "1.5"] {
let error = app.set_option("recent-files", bad).unwrap_err();
assert!(error.contains("between 0 and 50"), "{bad}: {error}");
}
assert_eq!((app.recent_limit, app.recent.len()), (12, 2), "a refused value changes nothing");
app.set_option("recent-files", "50").unwrap();
assert_eq!((app.recent_limit, app.recent.len()), (50, 2));
app.set_option("recent-files", "1").unwrap();
assert_eq!(names(&app), ["first.pdf"], "cut at once, with no dialog to wait for");
app.set_option("recent-files", "0").unwrap();
assert!(app.recent.is_empty());
}
+3 -3
View File
@@ -9957,11 +9957,11 @@ title = "Preferences: documents (autosave interval, recent count)"
area = "N"
tier = "P1"
milestone = "M14"
status = "planned"
status = "partial"
commands = []
tools = []
evidence = []
notes = ""
evidence = ["crates/ui-egui/tests/it/recent_files.rs::preferences_cuts_the_list_when_it_closes_and_not_before", "crates/ui-egui/tests/it/recent_files.rs::at_zero_home_has_no_recent_section", "crates/ui-egui/tests/it/recent_files.rs::a_settings_file_can_not_ask_for_a_limit_that_is_not_one"]
notes = "Preferences ▸ Documents and view: Recent files to keep, from 0 to 50 with 12 by default (#5572). At 0 Home has no Recent section and File ▸ Open Recent is disabled; Restore the last view keeps its own record of paths under its own switch. Still missing: the autosave interval (recovery snapshots run every AUTOSAVE_SECS, 60 s) and the rest of Acrobat's Documents page."
[[feature]]
id = "misc.prefs-commenting"