Issue #290: File ▸ Open Recent lists the recent files (#323)

* Issue #290: File ▸ Open Recent lists the recent files

The Home view kept a recent-files list but the File menu had no way to
reach it; Acrobat shows File ▸ Open Recent and the request asked for
exactly the menu wiring.

A new command file.open_recent renders as a submenu of the live list
right under Open… (disabled while the list is empty); each entry opens
its file or focuses the tab already showing it, which is now shared
with the Home view's list through the new open_recent() instead of a
second copy of that check. The command palette runs it as "open the
most recent file", or says there are none. Translations added to every
catalog (Japanese, Czech, Spanish, both Chinese and Brazilian
Portuguese are test-covered).

Test: commands.rs::the_open_recent_menu_lists_files_and_opens_one
(submenu lists a seeded recent file and clicking it opens a new active
tab on that path) and commands.rs::the_open_recent_menu_is_disabled
while_the_list_is_empty. Both failed before the change (no such menu).

Co-authored-by: Mawfyy <mawfy12@gmail.com>

* Build: cap cargo jobs and lighten the dev profile

On a 7.1 GiB machine the default one-job-per-core (12) with every
dependency at opt-level 2 plus full debuginfo swap-thrashes the box
frozen mid-build. jobs = 4 bounds peak memory to a few GiB and matches
GitHub runners (4 vCPUs), so CI is unaffected; dependencies drop to
opt-level 1 with debug = false, which halves their build cost and
keeps them fast enough for a usable debug app. Own crates keep their
opt-level and full debuginfo.

Verified: a from-scratch rebuild of all dependencies peaked at ~2 GiB
used with the machine responsive throughout; the perf.rs frame-budget
tests still pass at the lower dependency optimization.

Co-authored-by: Mawfyy <mawfy12@gmail.com>

* Keep the build configuration as on main; French for Open Recent

The jobs and dependency opt-level changes are a separate build-policy decision.
French is coverage-checked since #291; catalog checksums refreshed.

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

* French for Open Recent again, and catalog checksums refreshed

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

---------

Co-authored-by: Mawfyy <mawfy12@gmail.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Mawfyy
2026-10-08 16:46:22 -04:00
committed by GitHub
co-authored by Claude Opus 5.5 Mawfyy Brandon Thomas
parent e17833a64e
commit dbe4b2dddb
14 changed files with 108 additions and 16 deletions
+7 -7
View File
@@ -19,7 +19,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 = "2d0c3c882f0d842230b32834150df3307111b3b91d3c1c403001c78b8f4202df"
sha256 = "8792b00c7e9a12a247cbf6d4758d8583972f861010f4e4ed52949978e5a380e7"
[[asset]]
path = "crates/ui-egui/src/i18n/zh-hant.tsv"
@@ -30,7 +30,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 = "bff6a243f40151a20a5082e02a1e7b588e46fca5f7b616ca618e57576b5218e3"
sha256 = "9429421177790a0492b1513102823a9aa5dbd34c7a850a737792602d4d19b8d9"
[[asset]]
path = "crates/ui-egui/src/i18n/cs.tsv"
@@ -41,7 +41,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 = "92cd3816ae6f0cf59244f4836cc95a3b12961c88d219dae593fd60dbc6eb8de0"
sha256 = "9a655daa584e3a5d049dbaaa30229ae7a931984b0f1c7f0177fde2ccdd6804ca"
[[asset]]
path = "crates/ui-egui/src/i18n/pt-br.tsv"
@@ -52,7 +52,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 = "0cd7b7d0696323e281f7a23ef7511785b6a519c94ddb9d89c2468bcdc44bbc84"
sha256 = "777a7f441950714a61d41e5a44405209aa85bfdd4b46e27090544eb9122d715f"
[[asset]]
path = "crates/ui-egui/src/i18n/es.tsv"
@@ -63,7 +63,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 = "de653ffb92e6b99e8d390e6c21f5f13ecef198e21be3045e159b86073fbe7f4e"
sha256 = "cb3759b57445a3948a707aaf9423338053c7e7ad68f19f52583527dae3dfabee"
[[asset]]
path = "crates/ui-egui/src/i18n/fr.tsv"
@@ -74,7 +74,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 = "415aa786c8afc8095be14ac9d74b864e968326699ce0130fede5616ccf9e70ad"
sha256 = "b131025cb6c14135440968f08ab6334d70dd3b20271539471878d3825ecc9629"
[[asset]]
path = "crates/ui-egui/src/i18n/ja.tsv"
@@ -85,7 +85,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 = "d3a89359bdb7c8af830a49a1b574312c65f757e4f69cf221e06f27ea0c0e7373"
sha256 = "c9879375eed52003bb16790e687808fe0d50dd9405390be383eab66cd513291c"
[[asset]]
path = "assets/icons/accessibility.svg"
+1
View File
@@ -149,6 +149,7 @@ const HELP: Option<&str> = Some("Help");
/// Every command, in menu order.
pub const COMMANDS: &[CommandSpec] = &[
c("file.open", "Open…", FILE, Some(Shortcut::cmd("O")), Nothing, "folder-open"),
c("file.open_recent", "Open Recent", FILE, None, Nothing, "clock"),
c("create.blank", "New blank PDF", FILE, None, Nothing, "file-plus-2"),
c("measure.distance", "Measure distance", None, None, Annotate, "ruler"),
c("measure.perimeter", "Measure perimeter", None, None, Annotate, "ruler"),
+27
View File
@@ -80,6 +80,11 @@ impl PdfCraftApp {
let targets = active.map(|i| self.views[i].target_pages()).unwrap_or_default();
match id {
"file.open" => self.open_dialog(),
"file.open_recent" => match self.recent.first().map(|r| r.path.clone()) {
// The palette runs commands without a submenu: open the most recent file.
Some(p) => self.open_recent(&p),
None => self.notify_tr("No recent files"),
},
"page.combine" => self.open_combine_tab(),
"file.save" => {
self.save_active(SaveTarget::InPlace);
@@ -551,6 +556,28 @@ pub(crate) fn registry_menu(app: &mut PdfCraftApp, ui: &mut egui::Ui, menu: &str
for spec in commands::menu(menu) {
let label = commands::current_label(spec, &app.session, app.active_ids().map(|(_, id)| id));
let label = crate::i18n::menu_label(spec.id, &label);
// Open Recent is a submenu of the live recent list, not one action: disabled while the
// list is empty, otherwise each entry opens its file (or focuses the tab showing it).
if spec.id == "file.open_recent" {
if app.recent.is_empty() {
ui.add_enabled(false, egui::Button::new(label));
continue;
}
let mut open: Option<String> = None;
ui.menu_button(label, |ui| {
for r in &app.recent {
if ui.button(&r.name).on_hover_text(&r.path).clicked() {
open = Some(r.path.clone());
ui.close();
}
}
});
if let Some(p) = open {
app.open_recent(&p);
ui.close();
}
continue;
}
let shortcut = spec.shortcut.map(|s| s.label(mac)).unwrap_or_default();
let enabled = app.command_enabled(spec);
let resp = ui.add_enabled(enabled, egui::Button::new(label).shortcut_text(shortcut));
+1 -6
View File
@@ -130,12 +130,7 @@ pub fn show(app: &mut PdfCraftApp, ui: &mut egui::Ui) {
}
}
if let Some(p) = open {
if let Some(i) = app.views.iter().position(|v| app.session.get(v.id).and_then(|d| d.path.as_deref()) == Some(p.as_str())) {
app.active = Some(i);
} else {
#[cfg(not(target_arch = "wasm32"))]
app.open_path(&p);
}
app.open_recent(&p);
}
ui.add_space(20.0);
widgets::section_title(ui, tl!("Privacy"));
+2
View File
@@ -28,6 +28,8 @@
Enable Acrobat JavaScript Povolit Acrobat JavaScript
OK OK
Open… Otevřít…
Open Recent Otevřít nedávné soubory
No recent files Žádné nedávné soubory
New blank PDF Nový prázdný dokument PDF
Create PDF from file… Vytvořit PDF ze souboru…
Create PDF from images… Vytvořit PDF z obrázků…
Can't render this file because it contains an unexpected character in line 7 and column 81.
+2
View File
@@ -21,6 +21,8 @@
Identity Identidad
Name on new comments Nombre en los comentarios nuevos
Open… Abrir…
Open Recent Abrir recientes
No recent files No hay archivos recientes
New blank PDF Nuevo PDF en blanco
Create PDF from file… Crear PDF a partir de un archivo…
Create PDF from images… Crear PDF a partir de imágenes…
Can't render this file because it contains an unexpected character in line 6 and column 60.
+2
View File
@@ -1991,3 +1991,5 @@
{n} selected {n} sélectionnés
Choose a PNG or JPEG image (up to 4 MiB). Transparency is preserved. Choisissez une image PNG ou JPEG (jusqu’à 4 Mio). La transparence est conservée.
Couldn't import the signature image: {e} Impossible d’importer l’image de la signature : {e}
Open Recent Ouvrir un fichier récent
No recent files Aucun fichier récent
Can't render this file because it contains an unexpected character in line 6 and column 39.
+2
View File
@@ -27,6 +27,8 @@
Identity 個人情報
Name on new comments 新しい注釈の作成者名
Open… 開く…
Open Recent 最近のファイルを開く
No recent files 最近のファイルがありません
New blank PDF 空白の PDF を作成
Create PDF from file… ファイルから PDF を作成…
Create PDF from images… 画像から PDF を作成…
Can't render this file because it contains an unexpected character in line 6 and column 47.
+2
View File
@@ -20,6 +20,8 @@
Identity Identidade
Name on new comments Nome nos novos comentários
Open… Abrir…
Open Recent Abrir recentes
No recent files Nenhum arquivo recente
New blank PDF Novo PDF em branco
Create PDF from file… Criar PDF a partir de arquivo…
Create PDF from images… Criar PDF a partir de imagens…
Can't render this file because it contains an unexpected character in line 9 and column 16.
+2
View File
@@ -61,6 +61,8 @@
Identity 身份
Name on new comments 新注释的作者姓名
Open… 打开…
Open Recent 打开最近的文件
No recent files 没有最近的文件
Open 打开
Open file 打开文件
Home 主页
Can't render this file because it contains an unexpected character in line 9 and column 16.
+2
View File
@@ -20,6 +20,8 @@
Identity 個人資訊
Name on new comments 新註解的作者名稱
Open… 開啟…
Open Recent 開啟最近的檔案
No recent files 沒有最近的檔案
Open 開啟
Open file 開啟檔案
Home 首頁
Can't render this file because it contains an unexpected character in line 9 and column 16.
+11
View File
@@ -884,6 +884,17 @@ impl PdfCraftApp {
}
}
/// Open a file from a recent list: focus the tab already showing it, else open it (File ▸
/// Open Recent and the Home view's list share this).
pub fn open_recent(&mut self, path: &str) {
if let Some(i) = self.views.iter().position(|v| self.session.get(v.id).and_then(|d| d.path.as_deref()) == Some(path)) {
self.active = Some(i);
} else {
#[cfg(not(target_arch = "wasm32"))]
self.open_path(path);
}
}
#[cfg(not(target_arch = "wasm32"))]
pub fn open_path(&mut self, path: &str) {
let name = std::path::Path::new(path).file_name().map(|n| n.to_string_lossy().into_owned()).unwrap_or_else(|| path.to_string());
+44
View File
@@ -194,6 +194,50 @@ fn the_pages_menu_comes_from_the_registry() {
assert_eq!(app.session.get(app.views[0].id).unwrap().info.pages.len(), 2);
}
#[test]
fn the_open_recent_menu_lists_files_and_opens_one() {
let dir = std::env::temp_dir().join(format!("pdfcraft-recent-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("recent.pdf");
std::fs::write(&path, fixture(2)).unwrap();
let path = path.to_string_lossy().into_owned();
let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe({
let path = path.clone();
move |_cc| {
let mut app = PdfCraftApp::new();
app.open_bytes("doc.pdf", None, fixture(3)).unwrap();
app.recent.push(pdfcraft_ui_egui::RecentFile { name: "recent.pdf".into(), path, pages: 2, size: 0 });
app
}
});
h.run_steps(4);
h.get_by_label("Menu").click();
h.run_steps(2);
h.get_by_label("File ⏵").hover();
h.run_steps(3);
h.get_by_label("Open Recent ⏵").hover();
h.run_steps(3);
h.get_by_label_contains("recent.pdf").click();
h.run_steps(4);
let app = h.state();
assert_eq!(app.views.len(), 2, "the recent file opened in a new tab");
let active = app.active.unwrap();
assert_eq!(app.session.get(app.views[active].id).and_then(|d| d.path.as_deref()), Some(path.as_str()), "the active tab is the recent file");
}
#[test]
fn the_open_recent_menu_is_disabled_while_the_list_is_empty() {
let mut h = harness();
h.get_by_label("Menu").click();
h.run_steps(2);
h.get_by_label("File ⏵").hover();
h.run_steps(3);
assert!(
h.query_by(|n| n.label().as_deref() == Some("Open Recent") && n.is_disabled()).is_some(),
"Open Recent is disabled while no file has been opened"
);
}
#[test]
fn the_shortcuts_dialog_lists_the_real_bindings() {
let mut h = harness();
+3 -3
View File
@@ -656,10 +656,10 @@ area = "A"
tier = "P1"
milestone = "M3"
status = "shipped"
commands = []
commands = ["file.open_recent"]
tools = []
evidence = ["crates/ui-egui/tests/ui.rs::persistence_round_trips_and_tolerates_garbage"]
notes = "never reads other apps' lists"
evidence = ["crates/ui-egui/tests/ui.rs::persistence_round_trips_and_tolerates_garbage", "crates/ui-egui/tests/commands.rs::the_open_recent_menu_lists_files_and_opens_one", "crates/ui-egui/tests/commands.rs::the_open_recent_menu_is_disabled_while_the_list_is_empty"]
notes = "never reads other apps' lists; File ▸ Open Recent lists them (#290), an already-open file focuses its tab instead of reopening it"
[[feature]]
id = "core.usage-rights-preserve"