mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-25 10:39:01 -06:00
* 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:
co-authored by
Claude Opus 5.5
Mawfyy
Brandon Thomas
parent
e17833a64e
commit
dbe4b2dddb
+7
-7
@@ -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"
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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"));
|
||||
|
||||
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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());
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user